diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala new file mode 100644 index 000000000..f0a56c126 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/Avatar.scala @@ -0,0 +1,206 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects + +import net.psforever.objects.definition.{AvatarDefinition, ImplantDefinition} +import net.psforever.objects.equipment.EquipmentSize +import net.psforever.types.{CertificationType, CharacterGender, ImplantType, PlanetSideEmpire} + +import scala.annotation.tailrec +import scala.collection.mutable + +class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : CharacterGender.Value, val head : Int, val voice : Int) { + /** Battle Experience Points */ + private var bep : Long = 0 + /** Command Experience Points */ + private var cep : Long = 0 + /** Certifications */ + private val certs : mutable.Set[CertificationType.Value] = mutable.Set[CertificationType.Value]() + /** Implants
+ * Unlike other objects, the maximum number of `ImplantSlots` are built into the `Avatar`. + * Additionally, implants do not have tightly-coupled "`Definition` objects" that explain a formal implant object. + * The `ImplantDefinition` objects themselves are moved around as if they were the implants. + * The terms externally used for the states of process is "installed" and "uninstalled." + * @see `ImplantSlot` + * @see `DetailedCharacterData.implants` + */ + private val implants : Array[ImplantSlot] = Array.fill[ImplantSlot](3)(new ImplantSlot) + /** Loadouts */ + private val loadouts : Array[Option[Loadout]] = Array.fill[Option[Loadout]](10)(None) + /** Locker (fifth inventory slot) */ + private val locker : LockerContainer = LockerContainer() + + def BEP : Long = bep + + def BEP_=(battleExperiencePoints : Long) : Long = { + bep = math.max(0L, math.min(battleExperiencePoints, 4294967295L)) + BEP + } + + def Certifications : mutable.Set[CertificationType.Value] = certs + + def CEP : Long = cep + + def CEP_=(commandExperiencePoints : Long) : Long = { + cep = math.max(0L, math.min(commandExperiencePoints, 4294967295L)) + CEP + } + + /** + * Retrieve the three implant slots for this player. + * @return an `Array` of `ImplantSlot` objects + */ + def Implants : Array[ImplantSlot] = implants + + /** + * What kind of implant is installed into the given slot number? + * @see `ImplantType` + * @param slot the slot number + * @return the tye of implant + */ + def Implant(slot : Int) : ImplantType.Value = { + if(-1 < slot && slot < implants.length) { implants(slot).Implant } else { ImplantType.None } + } + + /** + * Given a new implant, assign it into a vacant implant slot on this player.
+ *
+ * The implant must be unique in terms of which implants have already been assigned to this player. + * Multiple of a type of implant being assigned at once is not supported. + * Additionally, the implant is inserted into the earliest yet-unknown but vacant slot. + * Implant slots are vacant by just being unlocked or by having their previous implant uninstalled. + * @param implant the implant being installed + * @return the index of the `ImplantSlot` where the implant was installed + */ + def InstallImplant(implant : ImplantDefinition) : Option[Int] = { + implants.find({p => p.Installed.contains(implant) || p.Implant == implant.Type}) match { //try to find the installed implant + case None => + recursiveFindImplantInSlot(implants.iterator, ImplantType.None) match { //install in a free slot + case Some(slot) => + implants(slot).Implant = implant + Some(slot) + case None => + None + } + case Some(_) => + None + } + } + + /** + * Remove a specific implant from a player's allocated installed implants.
+ *
+ * Due to the exclusiveness of installed implants, + * any implant slot with a matching `Definition` can be uninstalled safely. + * (There will never be any doubles.) + * This operation can lead to an irregular pattern of installed and uninstalled `ImplantSlot` objects. + * Despite that breach of pattern, the logic here is consistent as demonstrated by the client and by packets. + * The client also assigns and removes implants based on slot numbers that only express availability of a "slot." + * @see `AvatarImplantMessage.implantSlot` + * @param implantType the type of implant being uninstalled + * @return the index of the `ImplantSlot` where the implant was found and uninstalled + */ + def UninstallImplant(implantType : ImplantType.Value) : Option[Int] = { + recursiveFindImplantInSlot(implants.iterator, implantType) match { + case Some(slot) => + implants(slot).Implant = None + Some(slot) + case None => + None + } + } + + /** + * Locate the index of the encountered implant type. + * Functional implants may be exclusive in as far as the input `Iterator`'s source is concerned, + * but any number of `ImplantType.None` values are alway allowed in the source in any order. + * @param iter an `Iterator` of `ImplantSlot` objects + * @param implantType the target implant being sought + * @param index a defaulted index value representing the structure underlying the `Iterator` param + * @return the index where the target implant is installed + */ + @tailrec private def recursiveFindImplantInSlot(iter : Iterator[ImplantSlot], implantType : ImplantType.Value, index : Int = 0) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + val slot = iter.next + if(slot.Unlocked && slot.Implant == implantType) { + Some(index) + } + else { + recursiveFindImplantInSlot(iter, implantType, index + 1) + } + } + } + + def ResetAllImplants() : Unit = { + implants.foreach(slot => { + slot.Installed match { + case Some(_) => + slot.Initialized = false + case None => ; + } + }) + } + + def SaveLoadout(owner : Player, label : String, line : Int) : Unit = { + if(line > -1 && line < 10) { + loadouts(line) = Some(Loadout.Create(owner, label)) + } + } + + def LoadLoadout(line : Int) : Option[Loadout] = loadouts.lift(line).getOrElse(None) + + def DeleteLoadout(line : Int) : Unit = { + loadouts(line) = None + } + + def Locker : LockerContainer = locker + + def FifthSlot : EquipmentSlot = { + new OffhandEquipmentSlot(EquipmentSize.Inventory) { + Equipment = locker + } + } + + def Definition : AvatarDefinition = Avatar.definition + + /* + Merit Commendations and Ribbons + */ +// private var tosRibbon : MeritCommendation.Value = MeritCommendation.None +// private var upperRibbon : MeritCommendation.Value = MeritCommendation.None +// private var middleRibbon : MeritCommendation.Value = MeritCommendation.None +// private var lowerRibbon : MeritCommendation.Value = MeritCommendation.None + + def canEqual(other: Any): Boolean = other.isInstanceOf[Avatar] + + override def equals(other : Any) : Boolean = other match { + case that: Avatar => + (that canEqual this) && + name == that.name && + faction == that.faction && + sex == that.sex && + head == that.head && + voice == that.voice + case _ => + false + } + + override def hashCode() : Int = { + val state = Seq(name, faction, sex, head, voice) + state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + } + + override def toString: String = Avatar.toString(this) +} + +object Avatar { + final private val definition : AvatarDefinition = new AvatarDefinition(121) + + def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : Int) : Avatar = { + new Avatar(name, faction, sex, head, voice) + } + + def toString(avatar : Avatar) : String = s"${avatar.faction} ${avatar.name}" +} diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 14c8fbfe6..db4c14fef 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -487,7 +487,7 @@ object GlobalDefinitions { */ val order_terminal = new OrderTerminalDefinition - val ams_respawn_tube = new SpawnTubeDefinition(49) { Name = "ams_respawn_tube" } + val ams_respawn_tube = new SpawnTubeDefinition(49) val matrix_terminalc = new MatrixTerminalDefinition(519) @@ -509,6 +509,12 @@ object GlobalDefinitions { val vehicle_terminal_combined = new VehicleTerminalCombinedDefinition + val spawn_terminal = new MatrixTerminalDefinition(812) + + val respawn_tube = new SpawnTubeDefinition(732) + + val respawn_tube_tower = new SpawnTubeDefinition(733) + val spawn_pad = new VehicleSpawnPadDefinition val mb_locker = new LockerDefinition @@ -643,7 +649,7 @@ object GlobalDefinitions { * @param faction the faction * @return the `ToolDefinition` for the launcher */ - def AntiVehicular(faction : PlanetSideEmpire.Value) : ToolDefinition = { + def AntiVehicularLauncher(faction : PlanetSideEmpire.Value) : ToolDefinition = { faction match { case PlanetSideEmpire.TR => striker case PlanetSideEmpire.NC => hunterseeker @@ -683,6 +689,17 @@ object GlobalDefinitions { } } + def isMaxArms(tdef : ToolDefinition) : Boolean = { + tdef match { + case `trhev_dualcycler` | `nchev_scattercannon` | `vshev_quasar` + | `trhev_pounder` | `nchev_falcon` | `vshev_comet` + | `trhev_burster` | `nchev_sparrow` | `vshev_starfire` => + true + case _ => + false + } + } + def AIMAX(faction : PlanetSideEmpire.Value) : ToolDefinition = { faction match { case PlanetSideEmpire.TR => trhev_dualcycler diff --git a/common/src/main/scala/net/psforever/objects/LivePlayerList.scala b/common/src/main/scala/net/psforever/objects/LivePlayerList.scala index c0e398568..af1ab40ed 100644 --- a/common/src/main/scala/net/psforever/objects/LivePlayerList.scala +++ b/common/src/main/scala/net/psforever/objects/LivePlayerList.scala @@ -1,9 +1,6 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.packet.game.PlanetSideGUID - -import scala.annotation.tailrec import scala.collection.concurrent.{Map, TrieMap} /** @@ -12,157 +9,42 @@ import scala.collection.concurrent.{Map, TrieMap} */ private class LivePlayerList { /** key - the session id; value - a `Player` object */ - private val sessionMap : Map[Long, Player] = new TrieMap[Long, Player] - /** the index of the List corresponds to zone number 1-32 with 0 being "Nowhere" */ - /** each mapping: key - the global unique identifier; value - the session id */ - private val zoneMap : List[Map[Int, Long]] = List.fill(33)(new TrieMap[Int,Long]) + private val sessionMap : Map[Long, Avatar] = new TrieMap[Long, Avatar] - def WorldPopulation(predicate : ((_, Player)) => Boolean) : List[Player] = { + def WorldPopulation(predicate : ((_, Avatar)) => Boolean) : List[Avatar] = { sessionMap.filter(predicate).values.toList } - def ZonePopulation(zone : Int, predicate : ((_, Player)) => Boolean) : List[Player] = { - zoneMap.lift(zone) match { - case Some(map) => - val list = map.values.toList - sessionMap.filter({ case ((sess, _)) => list.contains(sess) }).filter(predicate).values.toList + def Add(sessionId : Long, avatar : Avatar) : Boolean = { + sessionMap.values.find(char => char.equals(avatar)) match { case None => - Nil - } - } - - def Add(sessionId : Long, player : Player) : Boolean = { - sessionMap.values.find(char => char.equals(player)) match { - case None => - sessionMap.putIfAbsent(sessionId, player).isEmpty + sessionMap.putIfAbsent(sessionId, avatar).isEmpty case Some(_) => false } } - def Remove(sessionId : Long) : Option[Player] = { - sessionMap.remove(sessionId) match { - case Some(char) => - zoneMap.foreach(zone => { - recursiveRemoveSession(zone.iterator, sessionId) match { - case Some(guid) => - zone.remove(guid) - case None => ; - } - }) - Some(char) - case None => - None - } + def Remove(sessionId : Long) : Option[Avatar] = { + sessionMap.remove(sessionId) } - @tailrec private def recursiveRemoveSession(iter : Iterator[(Int, Long)], sessionId : Long) : Option[Int] = { - if(!iter.hasNext) { - None - } - else { - val (guid : Int, sess : Long) = iter.next - if(sess == sessionId) { - Some(guid) - } - else { - recursiveRemoveSession(iter, sessionId) - } - } - } - - def Get(zone : Int, guid : PlanetSideGUID) : Option[Player] = { - Get(zone, guid.guid) - } - - def Get(zone : Int, guid : Int) : Option[Player] = { - zoneMap.lift(zone) match { - case Some(map) => - map.get(guid) match { - case Some(sessionId) => - sessionMap.get(sessionId) - case _ => - None - } - case None => - None - } - } - - def Assign(zone: Int, sessionId : Long, guid : PlanetSideGUID) : Boolean = Assign(zone, sessionId, guid.guid) - - def Assign(zone : Int, sessionId : Long, guid : Int) : Boolean = { - sessionMap.get(sessionId) match { - case Some(_) => - zoneMap.lift(zone) match { - case Some(zn) => - AssignToZone(zn, sessionId, guid) - case None => - false - } - - case None => - false - } - } - - private def AssignToZone(zone : Map[Int, Long], sessionId : Long, guid : Int) : Boolean = { - zone.get(guid) match { - case Some(_) => - false - case None => - zone(guid) = sessionId - true - } - } - - def Drop(zone : Int, guid : PlanetSideGUID) : Option[Player] = Drop(zone, guid.guid) - - def Drop(zone : Int, guid : Int) : Option[Player] = { - zoneMap.lift(zone) match { - case Some(map) => - map.remove(guid) match { - case Some(sessionId) => - sessionMap.get(sessionId) - case None => - None - } - case None => - None - } - } - - def Shutdown : List[Player] = { + def Shutdown : List[Avatar] = { val list = sessionMap.values.toList sessionMap.clear - zoneMap.foreach(map => map.clear()) list } } /** * A class for storing `Player` mappings for users that are currently online. - * The mapping system is tightly coupled between the `Player` class and to an instance of `WorldSessionActor`. - * Looser couplings exist between the instance of `WorldSessionActor` and a given `Player`'s globally unique id. - * These looser couplings are zone-specific. - * Though the user may have local knowledge of the zone they inhabit on their `Player` object, - * it should not be trusted.
+ * The mapping system is tightly coupled between the `Avatar` class and to an instance of `WorldSessionActor`. *
* Use:
- * 1) When a users logs in during `WorldSessionActor`, associate that user's session id and the character.
- *        `LivePlayerList.Add(session, player)`
- * 2) When that user's chosen character is declared his avatar using `SetCurrentAvatarMessage`, - * also associate the user's session with their current GUID.
- *        `LivePlayerList.Assign(zone, session, guid)`
- * 3) Repeat the previous step for as many times the user's GUID changes, especially during the aforementioned condition.
- * 4a) In between the previous two steps, a user's character may be referenced by their current GUID.
- *        `LivePlayerList.Get(zone, guid)`
- * 4b) Also in between those same previous steps, a range of characters may be queried based on provided statistics.
+ * 1) When a users logs in during `WorldSessionActor`, associate that user's session id and their character (avatar).
+ *        `LivePlayerList.Add(session, avatar)`
+ * 2) In between the previous two steps, a range of characters may be queried based on provided statistics.
*        `LivePlayerList.WorldPopulation(...)`
- *        `LivePlayerList.ZonePopulation(zone, ...)`
- * 5) When the user navigates away from a region completely, their entry is forgotten.
- *        `LivePlayerList.Drop(zone, guid)`
- * 6) When the user leaves the game entirely, his character's entries are removed from the mappings.
+ * 3) When the user leaves the game entirely, his character's entry is removed from the mapping.
*        `LivePlayerList.Remove(session)` */ object LivePlayerList { @@ -179,100 +61,28 @@ object LivePlayerList { * @param predicate the conditions for filtering the live `Player`s * @return a list of users's `Player`s that fit the criteria */ - def WorldPopulation(predicate : ((_, Player)) => Boolean) : List[Player] = Instance.WorldPopulation(predicate) - - /** - * Given some criteria, examine the mapping of user characters for a zone and find the ones that fulfill the requirements.
- *
- * Note the signature carefully. - * A two-element tuple is checked, but only the second element of that tuple - a `Player` - is eligible for being queried. - * The first element is ignored. - * Even a predicate as simple as `{ case ((x : Long, _)) => x > 0 }` will not work for that reason. - * @param zone the number of the zone - * @param predicate the conditions for filtering the live `Player`s - * @return a list of users's `Player`s that fit the criteria - */ - def ZonePopulation(zone : Int, predicate : ((_, Player)) => Boolean) : List[Player] = Instance.ZonePopulation(zone, predicate) + def WorldPopulation(predicate : ((_, Avatar)) => Boolean) : List[Avatar] = Instance.WorldPopulation(predicate) /** * Create a mapped entry between the user's session and a user's character. * Neither the player nor the session may exist in the current mappings if this is to work. * @param sessionId the session - * @param player the character + * @param avatar the character * @return `true`, if the session was association was made; `false`, otherwise */ - def Add(sessionId : Long, player : Player) : Boolean = Instance.Add(sessionId, player) + def Add(sessionId : Long, avatar : Avatar) : Boolean = Instance.Add(sessionId, avatar) /** * Remove all entries related to the given session identifier from the mappings. - * The player no longer counts as "online." - * This function cleans up __all__ associations - those created by `Add`, and those created by `Assign`. + * The character no longer counts as "online." * @param sessionId the session * @return any character that was afffected by the mapping removal */ - def Remove(sessionId : Long) : Option[Player] = Instance.Remove(sessionId) - - /** - * Get a user's character from the mappings. - * @param zone the number of the zone - * @param guid the current GUID of the character - * @return the character, if it can be found using the GUID - */ - def Get(zone : Int, guid : PlanetSideGUID) : Option[Player] = Instance.Get(zone, guid) - - /** - * Get a user's character from the mappings. - * @param zone the number of the zone - * @param guid the current GUID of the character - * @return the character, if it can be found using the GUID - */ - def Get(zone : Int, guid : Int) : Option[Player] = Instance.Get(zone, guid) - - /** - * Given a session that maps to a user's character, create a mapping between the character's current GUID and the session. - * If the user already has a GUID in the mappings, remove it and assert the new one. - * @param zone the number of the zone - * @param sessionId the session - * @param guid the GUID to associate with the character; - * technically, it has already been assigned and should be findable using `{character}.GUID.guid` - * @return `true`, if the mapping was created; - * `false`, if the session can not be found or if the character's GUID doesn't match the one provided - */ - def Assign(zone : Int, sessionId : Long, guid : PlanetSideGUID) : Boolean = Instance.Assign(zone, sessionId, guid) - - /** - * Given a session that maps to a user's character, create a mapping between the character's current GUID and the session. - * If the user already has a GUID in the mappings, remove it and assert the new one. - * @param zone the number of the zone - * @param sessionId the session - * @param guid the GUID to associate with the character; - * technically, it has already been assigned and should be findable using `{character}.GUID.guid` - * @return `true`, if the mapping was created; - * `false`, if the session can not be found or if the character's GUID doesn't match the one provided - */ - def Assign(zone : Int, sessionId : Long, guid : Int) : Boolean = Instance.Assign(zone, sessionId, guid) - - /** - * Given a GUID, remove any record of it. - * @param zone the number of the zone - * @param guid a GUID associated with the character; - * it does not have to be findable using `{character}.GUID.guid` - * @return any `Player` that may have been associated with this GUID - */ - def Drop(zone : Int, guid : PlanetSideGUID) : Option[Player] = Instance.Drop(zone, guid) - - /** - * Given a GUID, remove any record of it. - * @param zone the number of the zone - * @param guid a GUID associated with the character; - * it does not have to be findable using `{character}.GUID.guid` - * @return any `Player` that may have been associated with this GUID - */ - def Drop(zone : Int, guid : Int) : Option[Player] = Instance.Drop(zone, guid) + def Remove(sessionId : Long) : Option[Avatar] = Instance.Remove(sessionId) /** * Hastily remove all mappings and ids. * @return an unsorted list of the characters that were still online */ - def Shutdown : List[Player] = Instance.Shutdown + def Shutdown : List[Avatar] = Instance.Shutdown } diff --git a/common/src/main/scala/net/psforever/objects/Loadout.scala b/common/src/main/scala/net/psforever/objects/Loadout.scala index 1d2a2d2c9..ec997d0a0 100644 --- a/common/src/main/scala/net/psforever/objects/Loadout.scala +++ b/common/src/main/scala/net/psforever/objects/Loadout.scala @@ -8,14 +8,6 @@ import net.psforever.types.ExoSuitType import scala.annotation.tailrec -//trait Loadout { -// def Label : String -// def VisibleSlots : List[Loadout.SimplifiedEntry] -// def Inventory : List[Loadout.SimplifiedEntry] -// def ExoSuit : ExoSuitType.Value -// def Subtype : Int -//} - /** * From a `Player` their current exo-suit and their `Equipment`, retain a set of instructions to reconstruct this arrangement.
*
@@ -113,7 +105,9 @@ object Loadout { /** * A basic `Trait` connecting all of the `Equipment` blueprints. */ - sealed trait Simplification + sealed trait Simplification { + def definition : ObjectDefinition + } /** * An entry in the `Loadout`, wrapping around a slot index and what is in the slot index. @@ -125,17 +119,17 @@ object Loadout { /** * The simplified form of an `AmmoBox`. - * @param adef the `AmmoBoxDefinition` that describes this future object + * @param definition the `AmmoBoxDefinition` that describes this future object * @param capacity the amount of ammunition, if any, to initialize; * if `None`, then the previous `AmmoBoxDefinition` will be referenced for the amount later */ - final case class ShorthandAmmoBox(adef : AmmoBoxDefinition, capacity : Int) extends Simplification + final case class ShorthandAmmoBox(definition : AmmoBoxDefinition, capacity : Int) extends Simplification /** * The simplified form of a `Tool`. - * @param tdef the `ToolDefinition` that describes this future object + * @param definition the `ToolDefinition` that describes this future object * @param ammo the blueprints to construct the correct number of ammunition slots in the `Tool` */ - final case class ShorthandTool(tdef : ToolDefinition, ammo : List[ShorthandAmmoSlot]) extends Simplification + final case class ShorthandTool(definition : ToolDefinition, ammo : List[ShorthandAmmoSlot]) extends Simplification /** * The simplified form of a `Tool` `FireMode` * @param ammoIndex the index that points to the type of ammunition this slot currently uses @@ -144,19 +138,19 @@ object Loadout { final case class ShorthandAmmoSlot(ammoIndex : Int, ammo : ShorthandAmmoBox) /** * The simplified form of a `ConstructionItem`. - * @param cdef the `ConstructionItemDefinition` that describes this future object + * @param definition the `ConstructionItemDefinition` that describes this future object */ - final case class ShorthandConstructionItem(cdef : ConstructionItemDefinition) extends Simplification + final case class ShorthandConstructionItem(definition : ConstructionItemDefinition) extends Simplification /** * The simplified form of a `SimpleItem`. - * @param sdef the `SimpleItemDefinition` that describes this future object + * @param definition the `SimpleItemDefinition` that describes this future object */ - final case class ShorthandSimpleItem(sdef : SimpleItemDefinition) extends Simplification + final case class ShorthandSimpleItem(definition : SimpleItemDefinition) extends Simplification /** * The simplified form of a `Kit`. - * @param kdef the `KitDefinition` that describes this future object + * @param definition the `KitDefinition` that describes this future object */ - final case class ShorthandKit(kdef : KitDefinition) extends Simplification + final case class ShorthandKit(definition : KitDefinition) extends Simplification def DetermineSubtype(player : Player) : Int = { if(player.ExoSuit == ExoSuitType.MAX) { diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index 6da1a64c6..1250051b4 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.definition.{AvatarDefinition, ImplantDefinition} +import net.psforever.objects.definition.AvatarDefinition import net.psforever.objects.equipment.{Equipment, EquipmentSize} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} import net.psforever.objects.serverobject.affinity.FactionAffinity @@ -9,15 +9,9 @@ import net.psforever.packet.game.PlanetSideGUID import net.psforever.types._ import scala.annotation.tailrec -import scala.collection.mutable import scala.util.{Success, Try} -class Player(private val name : String, - private val faction : PlanetSideEmpire.Value, - private val sex : CharacterGender.Value, - private val head : Int, - private val voice : Int - ) extends PlanetSideGameObject with FactionAffinity with Container { +class Player(private val core : Avatar) extends PlanetSideGameObject with FactionAffinity with Container { private var alive : Boolean = false private var backpack : Boolean = false private var health : Int = 0 @@ -29,46 +23,20 @@ class Player(private val name : String, private var exosuit : ExoSuitType.Value = ExoSuitType.Standard private val freeHand : EquipmentSlot = new OffhandEquipmentSlot(EquipmentSize.Inventory) private val holsters : Array[EquipmentSlot] = Array.fill[EquipmentSlot](5)(new EquipmentSlot) - private val fifthSlot : EquipmentSlot = new OffhandEquipmentSlot(EquipmentSize.Inventory) private val inventory : GridInventory = GridInventory() private var drawnSlot : Int = Player.HandsDownSlot private var lastDrawnSlot : Int = Player.HandsDownSlot - private val loadouts : Array[Option[Loadout]] = Array.fill[Option[Loadout]](10)(None) - - private var bep : Long = 0 - private var cep : Long = 0 - private val certifications : mutable.Set[CertificationType.Value] = mutable.Set[CertificationType.Value]() - /** - * Unlike other objects, the maximum number of `ImplantSlots` are built into the `Player`. - * Additionally, "implants" do not have tightly-coupled "`Definition` objects" that explain a formal implant object. - * The `ImplantDefinition` objects themselves are moved around as if they were the implants. - * The term internally used for this process is "installed" and "uninstalled." - * @see `ImplantSlot` - * @see `DetailedCharacterData.implants` - * @see `AvatarConverter.MakeImplantEntries` - */ - private val implants : Array[ImplantSlot] = Array.fill[ImplantSlot](3)(new ImplantSlot) - -// private var tosRibbon : MeritCommendation.Value = MeritCommendation.None -// private var upperRibbon : MeritCommendation.Value = MeritCommendation.None -// private var middleRibbon : MeritCommendation.Value = MeritCommendation.None -// private var lowerRibbon : MeritCommendation.Value = MeritCommendation.None - private var facingYawUpper : Float = 0f private var crouching : Boolean = false private var jumping : Boolean = false private var cloaked : Boolean = false private var backpackAccess : Option[PlanetSideGUID] = None - private var admin : Boolean = false - private var spectator : Boolean = false - private var vehicleSeated : Option[PlanetSideGUID] = None private var vehicleOwned : Option[PlanetSideGUID] = None private var continent : String = "home2" //the zone id - private val playerDef : AvatarDefinition = Player.definition //TODO could be a var //SouNourS things /** Last medkituse. */ @@ -76,23 +44,20 @@ class Player(private val name : String, var death_by : Int = 0 var lastSeenStreamMessage : Array[Long] = Array.fill[Long](65535)(0L) var lastShotSeq_time : Int = -1 - /** The player is shooting. */ - var shooting : Boolean = false /** From PlanetsideAttributeMessage */ var PlanetsideAttribute : Array[Long] = Array.ofDim(120) Player.SuitSetup(this, ExoSuit) - fifthSlot.Equipment = new LockerContainer //the fifth slot is the player's "locker" - def Name : String = name + def Name : String = core.name - def Faction : PlanetSideEmpire.Value = faction + def Faction : PlanetSideEmpire.Value = core.faction - def Sex : CharacterGender.Value = sex + def Sex : CharacterGender.Value = core.sex - def Voice : Int = voice + def Head : Int = core.head - def Head : Int = head + def Voice : Int = core.voice def isAlive : Boolean = alive @@ -173,9 +138,7 @@ class Player(private val name : String, holsters(slot) } else if(slot == 5) { - new OffhandEquipmentSlot(EquipmentSize.Inventory) { - Equipment = fifthSlot.Equipment - } + core.FifthSlot } else if(slot == Player.FreeHandSlot) { freeHand @@ -189,7 +152,7 @@ class Player(private val name : String, def Inventory : GridInventory = inventory - def Locker : LockerContainer = fifthSlot.Equipment.get.asInstanceOf[LockerContainer] + def Locker : LockerContainer = core.Locker def Fit(obj : Equipment) : Option[Int] = { recursiveHolsterFit(holsters.iterator, obj.Size) match { @@ -220,20 +183,6 @@ class Player(private val name : String, } } - def Equip(slot : Int, obj : Equipment) : Boolean = { - if(-1 < slot && slot < 5) { - holsters(slot).Equipment = obj - true - } - else if(slot == Player.FreeHandSlot) { - freeHand.Equipment = obj - true - } - else { - inventory += slot -> obj - } - } - def FreeHand = freeHand def FreeHand_=(item : Option[Equipment]) : Option[Equipment] = { @@ -243,33 +192,22 @@ class Player(private val name : String, FreeHand.Equipment } - def SaveLoadout(label : String, line : Int) : Unit = { - loadouts(line) = Some(Loadout.Create(this, label)) - } - - def LoadLoadout(line : Int) : Option[Loadout] = loadouts(line) - - def DeleteLoadout(line : Int) : Unit = { - loadouts(line) = None - } - def Find(obj : Equipment) : Option[Int] = Find(obj.GUID) def Find(guid : PlanetSideGUID) : Option[Int] = { - findInHolsters(holsters.iterator, guid) match { + findInHolsters(holsters.iterator, guid) + .orElse(findInInventory(inventory.Items.values.iterator, guid)) match { case Some(index) => Some(index) case None => - findInInventory(inventory.Items.values.iterator, guid) match { - case Some(index) => - Some(index) - case None => - if(freeHand.Equipment.isDefined && freeHand.Equipment.get.GUID == guid) { - Some(Player.FreeHandSlot) - } - else { - None - } + if(Locker.Find(guid).isDefined) { + Some(5) + } + else if(freeHand.Equipment.isDefined && freeHand.Equipment.get.GUID == guid) { + Some(Player.FreeHandSlot) + } + else { + None } } } @@ -349,27 +287,13 @@ class Player(private val name : String, exosuit = suit } - def BEP : Long = bep + def LoadLoadout(line : Int) : Option[Loadout] = core.LoadLoadout(line) - def BEP_=(battleExperiencePoints : Long) : Long = { - bep = math.max(0L, math.min(battleExperiencePoints, 4294967295L)) - BEP - } + def BEP : Long = core.BEP - def CEP : Long = cep + def CEP : Long = core.CEP - def CEP_=(commandExperiencePoints : Long) : Long = { - cep = math.max(0L, math.min(commandExperiencePoints, 4294967295L)) - CEP - } - - def Certifications : mutable.Set[CertificationType.Value] = certifications - - /** - * Retrieve the three implant slots for this player. - * @return an `Array` of `ImplantSlot` objects - */ - def Implants : Array[ImplantSlot] = implants + def Certifications : Set[CertificationType.Value] = core.Certifications.toSet /** * What kind of implant is installed into the given slot number? @@ -377,91 +301,17 @@ class Player(private val name : String, * @param slot the slot number * @return the tye of implant */ - def Implant(slot : Int) : ImplantType.Value = { - if(-1 < slot && slot < implants.length) { implants(slot).Implant } else { ImplantType.None } - } + def Implant(slot : Int) : ImplantType.Value = core.Implant(slot) /** - * Given a new implant, assign it into a vacant implant slot on this player.
- *
- * The implant must be unique in terms of which implants have already been assigned to this player. - * Multiple of a type of implant being assigned at once is not supported. - * Additionally, the implant is inserted into the earliest yet-unknown but vacant slot. - * Implant slots are vacant by just being unlocked or by having their previous implant uninstalled. - * @param implant the implant being installed - * @return the index of the `ImplantSlot` where the implant was installed + * A read-only `Array` of tuples representing important information about all unlocked implant slots. + * @return a maximum of three implant types, initialization times, and active flags */ - def InstallImplant(implant : ImplantDefinition) : Option[Int] = { - implants.find({p => p.Installed.contains(implant)}) match { //try to find the installed implant - case None => - recursiveFindImplantInSlot(implants.iterator, ImplantType.None) match { //install in a free slot - case Some(slot) => - implants(slot).Implant = implant - Some(slot) - case None => - None - } - case Some(_) => - None - } + def Implants : Array[(ImplantType.Value, Long, Boolean)] = { + core.Implants.takeWhile(_.Unlocked).map( implant => { (implant.Implant, implant.MaxTimer, implant.Active) }) } - /** - * Remove a specific implant from a player's allocated installed implants.
- *
- * Due to the exclusiveness of installed implants, - * any implant slot with a matching `Definition` can be uninstalled safely. - * (There will never be any doubles.) - * This operation can lead to an irregular pattern of installed and uninstalled `ImplantSlot` objects. - * Despite that breach of pattern, the logic here is consistent as demonstrated by the client and by packets. - * The client also assigns and removes implants based on slot numbers that only express availability of a "slot." - * @see `AvatarImplantMessage.implantSlot` - * @param implantType the type of implant being uninstalled - * @return the index of the `ImplantSlot` where the implant was found and uninstalled - */ - def UninstallImplant(implantType : ImplantType.Value) : Option[Int] = { - recursiveFindImplantInSlot(implants.iterator, implantType) match { - case Some(slot) => - implants(slot).Implant = None - Some(slot) - case None => - None - } - } - - /** - * Locate the index of the encountered implant type. - * Functional implants may be exclusive in as far as the input `Iterator`'s source is concerned, - * but any number of `ImplantType.None` values are alway allowed in the source in any order. - * @param iter an `Iterator` of `ImplantSlot` objects - * @param implantType the target implant being sought - * @param index a defaulted index value representing the structure underlying the `Iterator` param - * @return the index where the target implant is installed - */ - @tailrec private def recursiveFindImplantInSlot(iter : Iterator[ImplantSlot], implantType : ImplantType.Value, index : Int = 0) : Option[Int] = { - if(!iter.hasNext) { - None - } - else { - val slot = iter.next - if(slot.Unlocked && slot.Implant == implantType) { - Some(index) - } - else { - recursiveFindImplantInSlot(iter, implantType, index + 1) - } - } - } - - def ResetAllImplants() : Unit = { - implants.foreach(slot => { - slot.Installed match { - case Some(_) => - slot.Initialized = false - case None => ; - } - }) - } + def ResetAllImplants() : Unit = core.ResetAllImplants() def FacingYawUpper : Float = facingYawUpper @@ -524,10 +374,6 @@ class Player(private val name : String, isBackpack && (backpackAccess.isEmpty || backpackAccess.contains(player.GUID)) } - def Admin : Boolean = admin - - def Spectator : Boolean = spectator - def VehicleSeated : Option[PlanetSideGUID] = vehicleSeated def VehicleSeated_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = VehicleSeated_=(Some(guid)) @@ -553,52 +399,34 @@ class Player(private val name : String, Continent } - def Definition : AvatarDefinition = playerDef - - override def toString : String = { - Player.toString(this) - } + def Definition : AvatarDefinition = core.Definition def canEqual(other: Any): Boolean = other.isInstanceOf[Player] override def equals(other : Any) : Boolean = other match { case that: Player => (that canEqual this) && - name == that.name && - faction == that.faction && - sex == that.sex && - voice == that.voice && - head == that.head + core == that.core case _ => false } override def hashCode() : Int = { - val state = Seq(name, faction, sex, voice, head) - state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + core.hashCode() } + + override def toString : String = Player.toString(this) } object Player { - final private val definition : AvatarDefinition = new AvatarDefinition(121) + final val LockerSlot : Int = 5 final val FreeHandSlot : Int = 250 final val HandsDownSlot : Int = 255 - def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : Int) : Player = { - new Player(name, faction, sex, head, voice) + def apply(core : Avatar) : Player = { + new Player(core) } -// /** -// * Change the type of `AvatarDefinition` is used to define the player. -// * @param player the player -// * @param avatarDef the player's new definition entry -// * @return the changed player -// */ -// def apply(player : Player, avatarDef : AvatarDefinition) : Player = { -// player.playerDef = avatarDef -// player -// } - def SuitSetup(player : Player, eSuit : ExoSuitType.Value) : Unit = { val esuitDef : ExoSuitDefinition = ExoSuitDefinition.Select(eSuit) //exosuit @@ -611,37 +439,11 @@ object Player { (0 until 5).foreach(index => { player.Slot(index).Size = esuitDef.Holster(index) }) } - def Administrate(player : Player, isAdmin : Boolean) : Player = { - player.admin = isAdmin - player - } - - def Spectate(player : Player, isSpectator : Boolean) : Player = { - player.spectator = isSpectator - player - } - - def Release(player : Player) : Player = { + def Respawn(player : Player) : Player = { if(player.Release) { - val obj = new Player(player.Name, player.Faction, player.Sex, player.Voice, player.Head) + val obj = new Player(player.core) obj.VehicleOwned = player.VehicleOwned obj.Continent = player.Continent - //hand over loadouts - (0 until 10).foreach(index => { - obj.loadouts(index) = player.loadouts(index) - }) - //hand over implants - (0 until 3).foreach(index => { - if(obj.Implants(index).Unlocked = player.Implants(index).Unlocked) { - obj.Implants(index).Implant = player.Implants(index).Installed - } - }) - //hand over knife - obj.Slot(4).Equipment = player.Slot(4).Equipment - player.Slot(4).Equipment = None - //hand over ??? - obj.fifthSlot.Equipment = player.fifthSlot.Equipment - player.fifthSlot.Equipment = None obj } else { @@ -650,7 +452,7 @@ object Player { } def toString(obj : Player) : String = { - val name : String = if(obj.VehicleSeated.isDefined) { s"[${obj.name}, ${obj.VehicleSeated.get.guid}]" } else { obj.Name } - s"[player $name, ${obj.Faction} (${obj.Health}/${obj.MaxHealth})(${obj.Armor}/${obj.MaxArmor})]" + val guid = if(obj.HasGUID) { s" ${obj.Continent}-${obj.GUID.guid}" } else { "" } + s"${obj.core}$guid ${obj.Health}/${obj.MaxHealth} ${obj.Armor}/${obj.MaxArmor}" } } 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 3d8bda7e2..094980fc7 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 @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.definition.converter -import net.psforever.objects.{EquipmentSlot, ImplantSlot, Player} +import net.psforever.objects.{EquipmentSlot, Player} import net.psforever.objects.equipment.Equipment 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} @@ -136,18 +136,12 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = { 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(implant) => - if(slot.Initialized) { - ImplantEntry(slot.Implant, None) - } - else { - ImplantEntry(slot.Implant, Some(implant.Initialization.toInt)) - } - case None => - ImplantEntry(ImplantType.None, None) + obj.Implants.map({ case(implant, initialization, active) => + if(initialization == 0) { + ImplantEntry(implant, None) + } + else { + ImplantEntry(implant, Some(math.max(0,initialization).toInt)) } }).toList } @@ -157,14 +151,14 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { * @param iter an `Iterator` of `ImplantSlot` objects * @return the effect of an active implant */ - @tailrec private def recursiveMakeImplantEffects(iter : Iterator[ImplantSlot]) : Option[ImplantEffects.Value] = { + @tailrec private def recursiveMakeImplantEffects(iter : Iterator[(ImplantType.Value, Long, Boolean)]) : Option[ImplantEffects.Value] = { if(!iter.hasNext) { None } else { - val slot = iter.next - if(slot.Active) { - slot.Implant match { + val(implant, _, active) = iter.next + if(active) { + implant match { case ImplantType.AdvancedRegen => Some(ImplantEffects.RegenEffects) case ImplantType.DarklightVision => diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala new file mode 100644 index 000000000..68b7df90d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala @@ -0,0 +1,124 @@ +// 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, DrawnSlot, InternalSlot, InventoryData, PlacementData, RibbonBars} +import net.psforever.types.{CharacterGender, GrenadeState, Vector3} + +import scala.annotation.tailrec +import scala.util.{Failure, Success, Try} + +class CorpseConverter extends AvatarConverter { + override def ConstructorData(obj : Player) : Try[CharacterData] = + Failure(new Exception("CorpseConverter should not be used to generate CharacterData")) + + override def DetailedConstructorData(obj : Player) : Try[DetailedCharacterData] = { + Success( + DetailedCharacterData( + MakeAppearanceData(obj), + 0, 0, 0, 0, 0, 0, 0, + Nil, Nil, Nil, Nil, + None, + InventoryData((MakeHolsters(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)), + DrawnSlot.None + ) + ) + } + + /** + * Compose some data from a `Player` into a representation common to both `CharacterData` and `DetailedCharacterData`. + * @param obj the `Player` game object + * @return the resulting `CharacterAppearanceData` + */ + private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = { + CharacterAppearanceData( + PlacementData(obj.Position, Vector3(0,0, obj.Orientation.z)), + BasicCharacterData(obj.Name, obj.Faction, CharacterGender.Male, 0, 0), + 0, + false, + false, + obj.ExoSuit, + "", + 0, + true, + obj.Orientation.y, //TODO is this important? + 0, + true, + GrenadeState.None, + false, + false, + false, + RibbonBars() + ) + } + + /** + * 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. + * It will always be "`Detailed`". + * @param obj the `Player` game object + * @return a list of all items that were in the inventory in decoded packet form + */ + private def MakeInventory(obj : Player) : List[InternalSlot] = { + obj.Inventory.Items + .map({ + case(_, item) => + val equip : Equipment = item.obj + BuildEquipment(item.start, equip) + }).toList + } + + /** + * Given a player with equipment holsters, convert the contents of those holsters into converted-decoded packet data. + * The decoded packet form is determined by the function in the parameters as both `0x17` and `0x18` conversions are available, + * with exception to the contents of the fifth slot. + * The fifth slot is only represented if the `Player` is an `0x18` type. + * @param obj the `Player` game object + * @return a list of all items that were in the holsters in decoded packet form + */ + private def MakeHolsters(obj : Player) : List[InternalSlot] = { + recursiveMakeHolsters(obj.Holsters().iterator) + } + + /** + * 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 + * @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 :+ BuildEquipment(index, equip), + index + 1 + ) + } + else { + recursiveMakeHolsters(iter, list, index + 1) + } + } + } + + /** + * A builder method for turning an object into `0x17` decoded packet form. + * @param index the position of the object + * @param equip the game object + * @return the game object in decoded packet form + */ + private def BuildEquipment(index : Int, equip : Equipment) : InternalSlot = { + InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get) + } +} + +object CorpseConverter { + val converter = new CorpseConverter +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala index 5a0ba6300..81f3f2b65 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -22,7 +22,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { PlanetSideGUID(0) //if(obj.Owner.isDefined) { obj.Owner.get } else { PlanetSideGUID(0) } //TODO is this really Owner? ), 0, - obj.Health / obj.MaxHealth * 255, //TODO not precise + 255 * obj.Health / obj.MaxHealth, //TODO not precise false, false, obj.DeploymentState, false, diff --git a/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala b/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala index 95d92bced..2ea7fb4a5 100644 --- a/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala +++ b/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala @@ -1,7 +1,13 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.guid -import net.psforever.objects.vehicles.Utility +import akka.actor.ActorRef +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.{EquipmentSlot, LockerContainer, Player, Tool, Vehicle} +import net.psforever.objects.inventory.Container + +import scala.annotation.tailrec /** * The basic compiled tasks for assigning (registering) and revoking (unregistering) globally unique identifiers.
@@ -14,17 +20,13 @@ import net.psforever.objects.vehicles.Utility * It will get passed from the more complicated functions down into the less complicated functions, * until it has found the basic number assignment functionality.
*
- * All functions produce a `TaskResolver.GiveTask` container object that is expected to be used by a `TaskResolver`. - * These "task containers" can also be unpackaged into their tasks, sorted into other containers, - * and combined with other "task containers" to enact more complicated sequences of operations. + * All functions produce a `TaskResolver.GiveTask` container object + * or a list of `TaskResolver.GiveTask` container objects that is expected to be used by a `TaskResolver` `Actor`. + * These "task containers" can also be unpackaged into their component tasks, sorted into other containers, + * and combined with other tasks to enact more complicated sequences of operations. + * Almost all tasks have an explicit registering and an unregistering activity defined for it. */ object GUIDTask { - import akka.actor.ActorRef - import net.psforever.objects.entity.IdentifiableEntity - import net.psforever.objects.equipment.Equipment - import net.psforever.objects.{EquipmentSlot, Player, Tool, Vehicle} - - import scala.annotation.tailrec /** * Construct tasking that registers an object with a globally unique identifier selected from a pool of numbers.
*
@@ -76,9 +78,34 @@ object GUIDTask { TaskResolver.GiveTask(RegisterObjectTask(obj).task, ammoTasks) } + /** + * Construct tasking that registers a `LockerContainer` object + * with a globally unique identifier selected from a pool of numbers. + * @param obj the object being registered + * @param guid implicit reference to a unique number system + * @see `GUIDTask.UnregisterLocker` + * @return a `TaskResolver.GiveTask` message + */ + def RegisterLocker(obj : LockerContainer)(implicit guid : ActorRef) : TaskResolver.GiveTask = { + TaskResolver.GiveTask(RegisterObjectTask(obj).task, RegisterInventory(obj)) + } + + /** + * Construct tasking that registers the objects that are within the given container's inventory + * with a globally unique identifier selected from a pool of numbers for each object. + * @param container the storage unit in which objects can be found + * @param guid implicit reference to a unique number system + * @see `GUID.UnregisterInventory`
+ * `Container` + * @return a list of `TaskResolver.GiveTask` messages + */ + def RegisterInventory(container : Container)(implicit guid : ActorRef) : List[TaskResolver.GiveTask] = { + container.Inventory.Items.values.map(entry => { RegisterEquipment(entry.obj)}).toList + } + /** * Construct tasking that registers an object with a globally unique identifier selected from a pool of numbers, - * after determining whether the object is complex (`Tool`) or simple.
+ * after determining whether the object is complex (`Tool` or `Locker`) or is simple.
*
* The objects in this case are specifically `Equipment`, a subclass of the basic register-able `IdentifiableEntity`. * About five subclasses of `Equipment` exist, but they decompose into two groups - "complex objects" and "simple objects." @@ -96,6 +123,8 @@ object GUIDTask { obj match { case tool : Tool => RegisterTool(tool) + case locker : LockerContainer => + RegisterLocker(locker) case _ => RegisterObjectTask(obj) } @@ -118,17 +147,24 @@ object GUIDTask { * @return a `TaskResolver.GiveTask` message */ def RegisterAvatar(tplayer : Player)(implicit guid : ActorRef) : TaskResolver.GiveTask = { - import net.psforever.objects.LockerContainer - import net.psforever.objects.inventory.InventoryItem - val holsterTasks = recursiveHolsterTaskBuilding(tplayer.Holsters().iterator, RegisterEquipment) - val fifthHolsterTask = tplayer.Slot(5).Equipment match { - case Some(locker) => - RegisterObjectTask(locker) :: locker.asInstanceOf[LockerContainer].Inventory.Items.map({ case((_ : Int, entry : InventoryItem)) => RegisterEquipment(entry.obj)}).toList - case None => - List.empty[TaskResolver.GiveTask]; - } - val inventoryTasks = tplayer.Inventory.Items.map({ case((_ : Int, entry : InventoryItem)) => RegisterEquipment(entry.obj)}) - TaskResolver.GiveTask(RegisterObjectTask(tplayer).task, holsterTasks ++ fifthHolsterTask ++ inventoryTasks) + val holsterTasks = VisibleSlotTaskBuilding(tplayer.Holsters(), RegisterEquipment) + val lockerTask = List(RegisterLocker(tplayer.Locker)) + val inventoryTasks = RegisterInventory(tplayer) + TaskResolver.GiveTask(RegisterObjectTask(tplayer).task, holsterTasks ++ lockerTask ++ inventoryTasks) + } + + /** + * Construct tasking that registers an object with a globally unique identifier selected from a pool of numbers, as a `Player`.
+ *
+ * Similar to `RegisterAvatar` but the locker components are skipped. + * @param tplayer the `Player` object being registered + * @param guid implicit reference to a unique number system + * @return a `TaskResolver.GiveTask` message + */ + def RegisterPlayer(tplayer : Player)(implicit guid : ActorRef) : TaskResolver.GiveTask = { + val holsterTasks = VisibleSlotTaskBuilding(tplayer.Holsters(), RegisterEquipment) + val inventoryTasks = RegisterInventory(tplayer) + TaskResolver.GiveTask(GUIDTask.RegisterObjectTask(tplayer)(guid).task, holsterTasks ++ inventoryTasks) } /** @@ -149,10 +185,9 @@ object GUIDTask { * @return a `TaskResolver.GiveTask` message */ def RegisterVehicle(vehicle : Vehicle)(implicit guid : ActorRef) : TaskResolver.GiveTask = { - import net.psforever.objects.inventory.InventoryItem - val weaponTasks = vehicle.Weapons.map({ case(_ : Int, entry : EquipmentSlot) => RegisterEquipment(entry.Equipment.get)}).toList - val utilTasks = Vehicle.EquipmentUtilities(vehicle.Utilities).map({case (_ : Int, util : Utility) => RegisterObjectTask(util())}).toList - val inventoryTasks = vehicle.Trunk.Items.map({ case((_ : Int, entry : InventoryItem)) => RegisterEquipment(entry.obj)}) + val weaponTasks = VisibleSlotTaskBuilding(vehicle.Weapons.values, RegisterEquipment) + val utilTasks = Vehicle.EquipmentUtilities(vehicle.Utilities).values.map(util => { RegisterObjectTask(util())}).toList + val inventoryTasks = RegisterInventory(vehicle) TaskResolver.GiveTask(RegisterObjectTask(vehicle).task, weaponTasks ++ utilTasks ++ inventoryTasks) } @@ -201,9 +236,33 @@ object GUIDTask { TaskResolver.GiveTask(UnregisterObjectTask(obj).task, ammoTasks) } + /** + * Construct tasking that unregisters a `LockerContainer` object from a globally unique identifier system. + * @param obj the object being unregistered + * @param guid implicit reference to a unique number system + * @see `GUIDTask.RegisterLocker` + * @return a `TaskResolver.GiveTask` message + */ + def UnregisterLocker(obj : LockerContainer)(implicit guid : ActorRef) : TaskResolver.GiveTask = { + TaskResolver.GiveTask(UnregisterObjectTask(obj).task, UnregisterInventory(obj)) + } + + /** + * Construct tasking that unregisters the objects that are within the given container's inventory + * from a globally unique identifier system. + * @param container the storage unit in which objects can be found + * @param guid implicit reference to a unique number system + * @see `GUIDTask.RegisterInventory`
+ * `Container` + * @return a list of `TaskResolver.GiveTask` messages + */ + def UnregisterInventory(container : Container)(implicit guid : ActorRef) : List[TaskResolver.GiveTask] = { + container.Inventory.Items.values.map(entry => { UnregisterEquipment(entry.obj)}).toList + } + /** * Construct tasking that unregisters an object from a globally unique identifier system - * after determining whether the object is complex (`Tool`) or simple.
+ * after determining whether the object is complex (`Tool` or `Locker`) or is simple.
*
* This task performs an operation that reverses the effect of `RegisterEquipment`. * @param obj the `Equipment` object being unregistered @@ -215,6 +274,8 @@ object GUIDTask { obj match { case tool : Tool => UnregisterTool(tool) + case locker : LockerContainer => + UnregisterLocker(locker) case _ => UnregisterObjectTask(obj) } @@ -230,36 +291,58 @@ object GUIDTask { * @return a `TaskResolver.GiveTask` message */ def UnregisterAvatar(tplayer : Player)(implicit guid : ActorRef) : TaskResolver.GiveTask = { - import net.psforever.objects.LockerContainer - import net.psforever.objects.inventory.InventoryItem - val holsterTasks = recursiveHolsterTaskBuilding(tplayer.Holsters().iterator, UnregisterEquipment) - val inventoryTasks = tplayer.Inventory.Items.map({ case((_ : Int, entry : InventoryItem)) => UnregisterEquipment(entry.obj)}) - val fifthHolsterTask = tplayer.Slot(5).Equipment match { - case Some(locker) => - UnregisterObjectTask(locker) :: locker.asInstanceOf[LockerContainer].Inventory.Items.map({ case((_ : Int, entry : InventoryItem)) => UnregisterEquipment(entry.obj)}).toList - case None => - List.empty[TaskResolver.GiveTask]; - } - TaskResolver.GiveTask(UnregisterObjectTask(tplayer).task, holsterTasks ++ fifthHolsterTask ++ inventoryTasks) + val holsterTasks = VisibleSlotTaskBuilding(tplayer.Holsters(), UnregisterEquipment) + val lockerTask = List(UnregisterLocker(tplayer.Locker)) + val inventoryTasks = UnregisterInventory(tplayer) + TaskResolver.GiveTask(UnregisterObjectTask(tplayer).task, holsterTasks ++ lockerTask ++ inventoryTasks) } -/** - * Construct tasking that unregisters a `Vehicle` object from a globally unique identifier system.
- *
- * This task performs an operation that reverses the effect of `RegisterVehicle`. - * @param vehicle the `Vehicle` object being unregistered - * @param guid implicit reference to a unique number system - * @see `GUIDTask.RegisterVehicle` - * @return a `TaskResolver.GiveTask` message - */ + /** + * Construct tasking that unregisters a portion of a `Player` object from a globally unique identifier system.
+ *
+ * Similar to `UnregisterAvatar` but the locker components are skipped. + * This task performs an operation that reverses the effect of `RegisterPlayer`. + * @param tplayer the `Player` object being unregistered + * @param guid implicit reference to a unique number system + * @see `GUIDTask.RegisterAvatar` + * @return a `TaskResolver.GiveTask` message + */ + def UnregisterPlayer(tplayer : Player)(implicit guid : ActorRef) : TaskResolver.GiveTask = { + val holsterTasks = VisibleSlotTaskBuilding(tplayer.Holsters(), UnregisterEquipment) + val inventoryTasks = UnregisterInventory(tplayer) + TaskResolver.GiveTask(GUIDTask.UnregisterObjectTask(tplayer).task, holsterTasks ++ inventoryTasks) + } + + /** + * Construct tasking that unregisters a `Vehicle` object from a globally unique identifier system.
+ *
+ * This task performs an operation that reverses the effect of `RegisterVehicle`. + * @param vehicle the `Vehicle` object being unregistered + * @param guid implicit reference to a unique number system + * @see `GUIDTask.RegisterVehicle` + * @return a `TaskResolver.GiveTask` message + */ def UnregisterVehicle(vehicle : Vehicle)(implicit guid : ActorRef) : TaskResolver.GiveTask = { - import net.psforever.objects.inventory.InventoryItem - val weaponTasks = vehicle.Weapons.map({ case(_ : Int, entry : EquipmentSlot) => UnregisterTool(entry.Equipment.get.asInstanceOf[Tool]) }).toList - val utilTasks = Vehicle.EquipmentUtilities(vehicle.Utilities).map({case (_ : Int, util : Utility) => UnregisterObjectTask(util())}).toList - val inventoryTasks = vehicle.Trunk.Items.map({ case((_ : Int, entry : InventoryItem)) => UnregisterEquipment(entry.obj)}) + val weaponTasks = VisibleSlotTaskBuilding(vehicle.Weapons.values, UnregisterEquipment) + val utilTasks = Vehicle.EquipmentUtilities(vehicle.Utilities).values.map(util => { UnregisterObjectTask(util())}).toList + val inventoryTasks = UnregisterInventory(vehicle) TaskResolver.GiveTask(UnregisterObjectTask(vehicle).task, weaponTasks ++ utilTasks ++ inventoryTasks) } + /** + * Construct tasking that allocates work upon encountered `Equipment` objects + * in reference to a globally unique identifier system of a pool of numbers. + * "Visible slots" are locations that can be viewed by multiple observers across a number of clients. + * @param list an `Iterable` sequence of `EquipmentSlot` objects that may or may not have equipment + * @param func the function used to build tasking from any discovered `Equipment`; + * strictly either `RegisterEquipment` or `UnregisterEquipment` + * @param guid implicit reference to a unique number system + * @return a list of `TaskResolver.GiveTask` messages + */ + def VisibleSlotTaskBuilding(list : Iterable[EquipmentSlot], func : ((Equipment)=>TaskResolver.GiveTask))(implicit guid : ActorRef) : List[TaskResolver.GiveTask] = { + recursiveVisibleSlotTaskBuilding(list.iterator, func) + } + /** * Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item. * Use `func` on any discovered `Equipment` to transform items into tasking, and add the tasking to a `List`. @@ -267,18 +350,19 @@ object GUIDTask { * @param func the function used to build tasking from any discovered `Equipment`; * strictly either `RegisterEquipment` or `UnregisterEquipment` * @param list a persistent `List` of `Equipment` tasking + * @see `VisibleSlotTaskBuilding` * @return a `List` of `Equipment` tasking */ - @tailrec private def recursiveHolsterTaskBuilding(iter : Iterator[EquipmentSlot], func : ((Equipment)=>TaskResolver.GiveTask), list : List[TaskResolver.GiveTask] = Nil)(implicit guid : ActorRef) : List[TaskResolver.GiveTask] = { + @tailrec private def recursiveVisibleSlotTaskBuilding(iter : Iterator[EquipmentSlot], func : ((Equipment)=>TaskResolver.GiveTask), list : List[TaskResolver.GiveTask] = Nil)(implicit guid : ActorRef) : List[TaskResolver.GiveTask] = { if(!iter.hasNext) { list } else { iter.next.Equipment match { case Some(item) => - recursiveHolsterTaskBuilding(iter, func, list :+ func(item)) + recursiveVisibleSlotTaskBuilding(iter, func, list :+ func(item)) case None => - recursiveHolsterTaskBuilding(iter, func, list) + recursiveVisibleSlotTaskBuilding(iter, func, list) } } } 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 index fcd16c173..31699543a 100644 --- a/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala +++ b/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala @@ -287,12 +287,7 @@ class UniqueNumberSystem(private val guid : NumberPoolHub, private val poolActor * @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") - } + poolActors(poolName) ! NumberPoolActor.ReturnNumber(number, Some(Long.MinValue)) } /** @@ -316,12 +311,7 @@ class UniqueNumberSystem(private val guid : NumberPoolHub, private val poolActor * @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") - } + poolActors(poolName) ! NumberPoolActor.GetSpecificNumber(number, Some(Long.MinValue)) } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/ServerObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/ServerObjectBuilder.scala index abc00e160..de9d15c84 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/ServerObjectBuilder.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/ServerObjectBuilder.scala @@ -18,7 +18,7 @@ import net.psforever.objects.guid.NumberPoolHub * a closed number space, which is also the `Zone`. * It utilizes those qualities of the enclosing region to construct the entity within that region.
*
- * Example: `ServerObjectBuilder(n, function)` + * Example: `ServerObjectBuilder(n, function)`
* Example: `new ServerBuilderObject[A](n, function)`, where `function` is a `(Int,Context)=>A` * @see `ZoneMap` * @see `Zone.Init` @@ -28,7 +28,7 @@ import net.psforever.objects.guid.NumberPoolHub * can be inferred from the output of `constructor` */ class ServerObjectBuilder[A <: PlanetSideServerObject](private val id : Int, - private val constructor : (Int, ActorContext) => A + private val constructor : ServerObjectBuilder.ConstructorType[A] ) { /** * Instantiate and configure the given server object. @@ -49,6 +49,8 @@ class ServerObjectBuilder[A <: PlanetSideServerObject](private val id : Int, } object ServerObjectBuilder { + type ConstructorType[A <: PlanetSideServerObject] = (Int, ActorContext)=>A + /** * Overloaded constructor. * @param id the unqiue id that will be assigned to this entity @@ -56,7 +58,7 @@ object ServerObjectBuilder { * @tparam A any object that extends from PlanetSideServerObject that will be produced by this class * @return a `ServerObjectBuilder` object */ - def apply[A <: PlanetSideServerObject](id : Int, constructor : (Int, ActorContext) => A) : ServerObjectBuilder[A] = { + def apply[A <: PlanetSideServerObject](id : Int, constructor : ConstructorType[A]) : ServerObjectBuilder[A] = { new ServerObjectBuilder[A](id, constructor) } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index 84a14c2c0..844da483b 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -6,14 +6,19 @@ import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.PlanetSideEmpire +import net.psforever.types.{PlanetSideEmpire, Vector3} -class Building(private val id : Int, private val zone : Zone) extends PlanetSideServerObject { +class Building(private val mapId : Int, private val zone : Zone, private val buildingType : StructureType.Value) extends PlanetSideServerObject { + /** + * The mapId is the identifier number used in BuildingInfoUpdateMessage. + * The modelId is the identifier number used in SetEmpireMessage. + */ + private var modelId : Option[Int] = None private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var amenities : List[Amenity] = List.empty GUID = PlanetSideGUID(0) - def Id : Int = id + def Id : Int = mapId def Faction : PlanetSideEmpire.Value = faction @@ -32,25 +37,44 @@ class Building(private val id : Int, private val zone : Zone) extends PlanetSide def Zone : Zone = zone + def ModelId : Int = modelId.getOrElse(Id) + + def ModelId_=(id : Int) : Int = ModelId_=(Some(id)) + + def ModelId_=(id : Option[Int]) : Int = { + modelId = id + ModelId + } + + def BuildingType : StructureType.Value = buildingType + def Definition: ObjectDefinition = Building.BuildingDefinition } object Building { - final val NoBuilding : Building = new Building(0, Zone.Nowhere) { + final val NoBuilding : Building = new Building(0, Zone.Nowhere, StructureType.Platform) { override def Faction_=(faction : PlanetSideEmpire.Value) : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL override def Amenities_=(obj : Amenity) : List[Amenity] = Nil } final val BuildingDefinition : ObjectDefinition = new ObjectDefinition(0) { Name = "building" } - def apply(id : Int, zone : Zone) : Building = { - new Building(id, zone) + def apply(id : Int, zone : Zone, buildingType : StructureType.Value) : Building = { + new Building(id, zone, buildingType) } - def Structure(id : Int, zone : Zone, context : ActorContext) : Building = { + def Structure(buildingType : StructureType.Value, location : Vector3)(id : Int, zone : Zone, context : ActorContext) : Building = { import akka.actor.Props - val obj = new Building(id, zone) - obj.Actor = context.actorOf(Props(classOf[BuildingControl], obj), s"$id-building") + val obj = new Building(id, zone, buildingType) + obj.Position = location + obj.Actor = context.actorOf(Props(classOf[BuildingControl], obj), s"$id-$buildingType-building") + obj + } + + def Structure(buildingType : StructureType.Value)(id : Int, zone : Zone, context : ActorContext) : Building = { + import akka.actor.Props + val obj = new Building(id, zone, buildingType) + obj.Actor = context.actorOf(Props(classOf[BuildingControl], obj), s"$id-$buildingType-building") obj } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/StructureType.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/StructureType.scala new file mode 100644 index 000000000..7acc1840a --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/StructureType.scala @@ -0,0 +1,20 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.structures + +/** + * An `Enumeration` of the kinds of building structures found in the game. + * This is merely a kludge for more a future, more complicated internal object that handles base operations. + */ +object StructureType extends Enumeration { + type Type = Value + + val + Bridge, + Building, //generic + Bunker, + Facility, + Platform, //outdoor amenities like the spawn pads in sanctuary + Tower, + WarpGate + = Value +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala index 784685377..bf23cc0d1 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala @@ -4,7 +4,7 @@ package net.psforever.objects.serverobject.structures import akka.actor.ActorContext import net.psforever.objects.zones.Zone -class WarpGate(id : Int, zone : Zone) extends Building(id, zone) { +class WarpGate(id : Int, zone : Zone) extends Building(id, zone, StructureType.WarpGate) { //TODO stuff later } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala index 94fef0fcd..2c062abb7 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala @@ -339,8 +339,8 @@ object EquipmentTerminalDefinition { import net.psforever.objects.Loadout._ entry match { case obj : ShorthandTool => - val ammo : List[AmmoBoxDefinition] = obj.ammo.map(fmode => { fmode.ammo.adef }) - val tool = Tool(obj.tdef) + val ammo : List[AmmoBoxDefinition] = obj.ammo.map(fmode => { fmode.ammo.definition }) + val tool = Tool(obj.definition) //makes Tools where an ammo slot may have one of its alternate ammo types (0 until tool.MaxAmmoSlot).foreach(index => { val slot = tool.AmmoSlots(index) @@ -350,16 +350,16 @@ object EquipmentTerminalDefinition { tool case obj : ShorthandAmmoBox => - MakeAmmoBox(obj.adef, Some(obj.capacity)) + MakeAmmoBox(obj.definition, Some(obj.capacity)) case obj : ShorthandConstructionItem => - MakeConstructionItem(obj.cdef) + MakeConstructionItem(obj.definition) case obj : ShorthandSimpleItem => - MakeSimpleItem(obj.sdef) + MakeSimpleItem(obj.definition) case obj : ShorthandKit => - MakeKit(obj.kdef) + MakeKit(obj.definition) } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/MatrixTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/MatrixTerminalDefinition.scala index 047e786d5..292e30b24 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/MatrixTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/MatrixTerminalDefinition.scala @@ -19,8 +19,11 @@ class MatrixTerminalDefinition(object_id : Int) extends TerminalDefinition(objec else if(object_id == 519) { "matrix_terminalc" } + else if(object_id == 812) { + "spawn_terminal" + } else { - throw new IllegalArgumentException("terminal must be object id 517-519") + throw new IllegalArgumentException("terminal must be object id 517-519 or 812") } override def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal() diff --git a/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTube.scala b/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTube.scala index 9d9baac9d..91aff35df 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTube.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTube.scala @@ -1,34 +1,58 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.tube -import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.GlobalDefinitions import net.psforever.objects.serverobject.structures.Amenity -class SpawnTube(tubeDef : ObjectDefinition) extends Amenity { - def Definition : ObjectDefinition = tubeDef +/** + * An owned server object that is used as a placeholder for the position and direction + * that infantry will be arranged upon spawning into the game world. + * @param tDef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ +class SpawnTube(tDef : SpawnTubeDefinition) extends Amenity { + def Definition : SpawnTubeDefinition = tDef } object SpawnTube { - def apply(tubeDef : ObjectDefinition) : SpawnTube = { - new SpawnTube(tubeDef) + /** + * Overloaded constructor. + * @param tDef the spawn tube's definition entry + * @return a `SpawnTube` object + */ + def apply(tDef : SpawnTubeDefinition) : SpawnTube = { + new SpawnTube(tDef) } -// import akka.actor.ActorContext -// import net.psforever.types.Vector3 -// /** -// * Instantiate an configure a `SpawnTube` object -// * @param pos the position (used to determine spawn point) -// * @param orient the orientation (used to indicate spawn direction) -// * @param id the unique id that will be assigned to this entity -// * @param context a context to allow the object to properly set up `ActorSystem` functionality -// * @return the `SpawnTube` object -// */ -// def Constructor(pos : Vector3, orient : Vector3)(id : Int, context : ActorContext) : SpawnTube = { -// import net.psforever.objects.GlobalDefinitions -// -// val obj = SpawnTube(GlobalDefinitions.ams_respawn_tube) -// obj.Position = pos -// obj.Orientation = orient -// obj -// } + import akka.actor.ActorContext + import net.psforever.types.Vector3 + /** + * Instantiate an configure a `SpawnTube` object + * @param pos the position (used to determine spawn point) + * @param orient the orientation (used to indicate spawn direction) + * @param id the unique id that will be assigned to this entity + * @param context a context to allow the object to properly set up `ActorSystem` functionality + * @return the `SpawnTube` object + */ + def Constructor(pos : Vector3, orient : Vector3)(id : Int, context : ActorContext) : SpawnTube = { + Constructor(GlobalDefinitions.respawn_tube, pos, orient)(id, context) + } + + /** + * Instantiate an configure a `SpawnTube` object with the given definition + * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + * @param pos the position (used to determine spawn point) + * @param orient the orientation (used to indicate spawn direction) + * @param id the unique id that will be assigned to this entity + * @param context a context to allow the object to properly set up `ActorSystem` functionality + * @return the `SpawnTube` object + */ + def Constructor(tdef : SpawnTubeDefinition, pos : Vector3, orient : Vector3)(id : Int, context : ActorContext) : SpawnTube = { + import akka.actor.Props + + val obj = SpawnTube(tdef) + obj.Position = pos + obj.Orientation = orient + obj.Actor = context.actorOf(Props(classOf[SpawnTubeControl], obj), s"${tdef.Name}_$id") + obj + } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeDefinition.scala index 774449c03..1cb1279dd 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeDefinition.scala @@ -6,7 +6,24 @@ import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.definition.converter.SpawnTubeConverter import net.psforever.objects.serverobject.structures.Amenity +/** + * The definition for any `VehicleSpawnPad`. + * Currently, all tubes identify as object id 49 - `ams_respawn_tube` - configured manually. + * @see `GlobalDefinitions.ams_respawn_tube` + */ class SpawnTubeDefinition(object_id : Int) extends ObjectDefinition(object_id) { + Name = if(object_id == 49) { + "ams_respawn_tube" + } + else if(object_id == 732) { + "respawn_tube" + } + else if(object_id == 733) { + "respawn_tube_tower" + } + else { + throw new IllegalArgumentException("terminal must be object id 49, 732, or 733") + } Packet = new SpawnTubeConverter } diff --git a/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala b/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala index ddd70cca2..a1b79e7ce 100644 --- a/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala +++ b/common/src/main/scala/net/psforever/objects/zones/InterstellarCluster.scala @@ -2,7 +2,6 @@ package net.psforever.objects.zones import akka.actor.{Actor, Props} -import net.psforever.objects.Player import scala.annotation.tailrec @@ -42,16 +41,25 @@ class InterstellarCluster(zones : List[Zone]) extends Actor { def receive : Receive = { case InterstellarCluster.GetWorld(zoneId) => log.info(s"Asked to find $zoneId") - findWorldInCluster(zones.iterator, zoneId) match { + recursiveFindWorldInCluster(zones.iterator, _.Id == zoneId) match { case Some(continent) => sender ! InterstellarCluster.GiveWorld(zoneId, continent) case None => log.error(s"Requested zone $zoneId could not be found") } - case InterstellarCluster.RequestClientInitialization(tplayer) => + case InterstellarCluster.RequestClientInitialization() => zones.foreach(zone => { sender ! Zone.ClientInitialization(zone.ClientInitialization()) }) - sender ! InterstellarCluster.ClientInitializationComplete(tplayer) //will be processed after all Zones + sender ! InterstellarCluster.ClientInitializationComplete() //will be processed after all Zones + + case msg @ Zone.Lattice.RequestSpawnPoint(zone_number, _, _) => + recursiveFindWorldInCluster(zones.iterator, _.Number == zone_number) match { + case Some(zone) => + zone.Actor forward msg + + case None => //zone_number does not exist + sender ! Zone.Lattice.NoValidSpawnPoint(zone_number, None) + } case _ => ; } @@ -59,20 +67,20 @@ class InterstellarCluster(zones : List[Zone]) extends Actor { /** * Search through the `List` of `Zone` entities and find the one with the matching designation. * @param iter an `Iterator` of `Zone` entities - * @param zoneId the name of the `Zone` + * @param predicate a condition to check against to determine when the appropriate `Zone` is discovered * @return the discovered `Zone` */ - @tailrec private def findWorldInCluster(iter : Iterator[Zone], zoneId : String) : Option[Zone] = { + @tailrec private def recursiveFindWorldInCluster(iter : Iterator[Zone], predicate : Zone=>Boolean) : Option[Zone] = { if(!iter.hasNext) { None } else { val cont = iter.next - if(cont.Id == zoneId) { + if(predicate.apply(cont)) { Some(cont) } else { - findWorldInCluster(iter, zoneId) + recursiveFindWorldInCluster(iter, predicate) } } } @@ -95,17 +103,41 @@ object InterstellarCluster { /** * Signal to the cluster that a new client needs to be initialized for all listed `Zone` destinations. - * @param tplayer the `Player` belonging to the client; - * may be superfluous * @see `Zone` */ - final case class RequestClientInitialization(tplayer : Player) + final case class RequestClientInitialization() /** * Return signal intended to inform the original sender that all `Zone`s have finished being initialized. - * @param tplayer the `Player` belonging to the client; - * may be superfluous * @see `WorldSessionActor` */ - final case class ClientInitializationComplete(tplayer : Player) + final case class ClientInitializationComplete() } + +/* +// List[Building] --> List[List[(Amenity, Building)]] --> List[(SpawnTube*, Building)] +zone.LocalLattice.Buildings.values + .filter(_.Faction == player.Faction) + .map(building => { building.Amenities.map { _ -> building } }) + .flatMap( _.filter({ case(amenity, _) => amenity.isInstanceOf[SpawnTube] }) ) + */ + +/* +zone.Buildings.values.filter(building => { + ( + if(spawn_zone == 6) { Set(StructureType.Tower) } + else if(spawn_zone == 7) { Set(StructureType.Facility, StructureType.Building) } + else { Set.empty[StructureType.Value] } + ).contains(building.BuildingType) && + building.Amenities.exists(_.isInstanceOf[SpawnTube]) && + building.Faction == player.Faction && + building.Position != Vector3.Zero +}) + .toSeq + .sortBy(building => { + Vector3.DistanceSquared(player.Position, building.Position) < Vector3.DistanceSquared(player.Position, building.Position) + }) + .map(building => { building.Amenities.map { _ -> building } }) + .flatMap( _.filter({ case(amenity, _) => amenity.isInstanceOf[SpawnTube] }) ) +).headOption + */ 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 f084443df..5104c7dc7 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -3,17 +3,19 @@ package net.psforever.objects.zones import akka.actor.{ActorContext, ActorRef, Props} import akka.routing.RandomPool -import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle} +import net.psforever.objects.{Avatar, PlanetSideGameObject, Player, Vehicle} import net.psforever.objects.equipment.Equipment import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.actor.UniqueNumberSystem import net.psforever.objects.guid.selector.RandomSelector import net.psforever.objects.guid.source.LimitedNumberSource import net.psforever.objects.serverobject.structures.{Amenity, Building} +import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.packet.game.PlanetSideGUID import net.psforever.types.Vector3 import scala.annotation.tailrec +import scala.collection.concurrent.TrieMap import scala.collection.mutable.ListBuffer import scala.collection.immutable.{Map => PairMap} @@ -44,8 +46,8 @@ 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 + guid.AddPool("environment", (0 to 3000).toList) //TODO tailer ro suit requirements of zone + guid.AddPool("dynamic", (3001 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. */ @@ -54,8 +56,16 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { private var vehicles : List[Vehicle] = List[Vehicle]() /** */ private var transport : ActorRef = ActorRef.noSender + /** */ + private val players : TrieMap[Avatar, Option[Player]] = TrieMap[Avatar, Option[Player]]() + /** */ + private val corpses : ListBuffer[Player] = ListBuffer[Player]() + /** */ + private var population : ActorRef = ActorRef.noSender private var buildings : PairMap[Int, Building] = PairMap.empty[Int, Building] + /** key - spawn zone id, value - buildings belonging to spawn zone */ + private var spawnGroups : Map[Building, List[SpawnTube]] = PairMap[Building, List[SpawnTube]]() /** * Establish the basic accessible conditions necessary for a functional `Zone`.
@@ -66,7 +76,12 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { * First, the `Actor`-driven aspect of the globally unique identifier system for this `Zone` is finalized. * Second, all supporting `Actor` agents are created, e.g., `ground`. * Third, the `ZoneMap` server objects are loaded and constructed within that aforementioned system. - * To avoid being called more than once, there is a test whether the `accessor` for the globally unique identifier system has been changed. + * To avoid being called more than once, there is a test whether the `accessor` for the globally unique identifier system has been changed.
+ *
+ * Execution of this operation should be fail-safe. + * The chances of failure should be mitigated or skipped. + * An testing routine should be run after the fact on the results of the process. + * @see `ZoneActor.ZoneSetupCheck` * @param context a reference to an `ActorContext` necessary for `Props` */ def Init(implicit context : ActorContext) : Unit = { @@ -75,12 +90,12 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { accessor = context.actorOf(RandomPool(25).props(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(guid))), s"$Id-uns") ground = context.actorOf(Props(classOf[ZoneGroundActor], equipmentOnGround), s"$Id-ground") transport = context.actorOf(Props(classOf[ZoneVehicleActor], this), s"$Id-vehicles") + population = context.actorOf(Props(classOf[ZonePopulationActor], this, players, corpses), s"$Id-players") - Map.LocalObjects.foreach({ builderObject => - builderObject.Build - }) + Map.LocalObjects.foreach({ builderObject => builderObject.Build }) MakeBuildings(context) AssignAmenities() + CreateSpawnGroups() } } @@ -176,6 +191,12 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { def Vehicles : List[Vehicle] = vehicles + def Players : List[Avatar] = players.keys.toList + + def LivePlayers : List[Player] = players.values.collect( { case Some(tplayer) => tplayer }).toList + + def Corpses : List[Player] = corpses.toList + def AddVehicle(vehicle : Vehicle) : List[Vehicle] = { vehicles = vehicles :+ vehicle Vehicles @@ -217,6 +238,8 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { def Transport : ActorRef = transport + def Population : ActorRef = population + def Buildings : Map[Int, Building] = buildings def Building(id : Int) : Option[Building] = { @@ -235,26 +258,55 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { }) } + private def CreateSpawnGroups() : Unit = { + buildings.values + .filterNot { _.Position == Vector3.Zero } + .map(building => { building -> building.Amenities.collect { case(obj : SpawnTube) => obj } }) + .filter( { case((_, spawns)) => spawns.nonEmpty }) + .foreach { SpawnGroups } + } + + def SpawnGroups() : Map[Building, List[SpawnTube]] = spawnGroups + + def SpawnGroups(building : Building) : List[SpawnTube] = SpawnGroups(building.Id) + + def SpawnGroups(buildingId : Int) : List[SpawnTube] = { + spawnGroups.find({ case((building, _)) => building.Id == buildingId }) match { + case Some((_, list)) => + list + case None => + List.empty[SpawnTube] + } + } + + def SpawnGroups(spawns : (Building, List[SpawnTube])) : Map[Building, List[SpawnTube]] = { + val (building, tubes) = spawns + val entry : Map[Building, List[SpawnTube]] = PairMap(building -> tubes) + spawnGroups = spawnGroups ++ entry + entry + } + /** * 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:
- * - `BroadcastWarpgateUpdateMessage`
* - `BuildingInfoUpdateMessage`
+ * - `DensityLevelUpdateMessage`
+ * - `BroadcastWarpgateUpdateMessage`
* - `CaptureFlagUpdateMessage`
* - `ContinentalLockUpdateMessage`
- * - `DensityLevelUpdateMessage`
* - `ModuleLimitsMessage`
* - `VanuModuleUpdateMessage`
* - `ZoneForcedCavernConnectionMessage`
* - `ZoneInfoMessage`
* - `ZoneLockInfoMessage`
* - `ZonePopulationUpdateMessage` - * @return a `List` of `GamePacket` messages + * @return the `Zone` object */ def ClientInitialization() : Zone = this } object Zone { + /** Default value, non-zone area. */ final val Nowhere : Zone = new Zone("nowhere", new ZoneMap("nowhere"), 99) /** @@ -263,6 +315,97 @@ object Zone { */ final case class Init() + object Population { + /** + * Message that introduces a user, by their `Avatar`, into a `Zone`. + * That user will be counted as part of that zone's population. + * The `avatar` may associate `Player` objects with itself in the future. + * @param avatar the `Avatar` object + */ + final case class Join(avatar : Avatar) + /** + * Message that excuses a user, by their `Avatar`, into a `Zone`. + * That user will not longer be counted as part of that zone's population. + * @see `PlayerHasLeft` + * @param avatar the `Avatar` object + */ + final case class Leave(avatar : Avatar) + /** + * Message that instructs the zone to disassociate a `Player` from this `Actor`. + * @see `PlayerAlreadySpawned`
+ * `PlayerCanNotSpawn` + * @param avatar the `Avatar` object + * @param player the `Player` object + */ + final case class Spawn(avatar : Avatar, player : Player) + /** + * Message that instructs the zone to disassociate a `Player` from this `Actor`. + * @see `PlayerHasLeft` + * @param avatar the `Avatar` object + */ + final case class Release(avatar : Avatar) + /** + * Message that acts in reply to `Leave(avatar)` or `Release(avatar)`. + * In the former case, the avatar will have successfully left the zone, and `player` may be defined. + * In the latter case, the avatar did not initially `Join` the zone, and `player` is `None`. + * This message should not be considered a failure or a success case. + * @see `Release`
+ * `Leave` + * @param zone the `Zone` object + * @param player the `Player` object + */ + final case class PlayerHasLeft(zone : Zone, player : Option[Player]) //Leave(avatar), but still has a player + /** + * Message that acts in reply to `Spawn(avatar, player)`, but the avatar already has a player. + * @param player the `Player` object + */ + final case class PlayerAlreadySpawned(zone : Zone, player : Player) + /** + * Message that acts in reply to `Spawn(avatar, player)`, but the avatar did not initially `Join` this zone. + * @param zone the `Zone` object + * @param player the `Player` object + */ + final case class PlayerCanNotSpawn(zone : Zone, player : Player) + } + + object Corpse { + /** + * Message that reports to the zone of a freshly dead player. + * @param player the dead `Player` + */ + final case class Add(player : Player) + /** + * Message that tells the zone to no longer mind the dead player. + * @param player the dead `Player` + */ + final case class Remove(player : Player) + } + + object Lattice { + /** + * Message requesting that the current zone determine where a `player` can spawn. + * @param zone_number this zone's numeric identifier + * @param player the `Player` object + * @param spawn_group the category of spawn points the request wants searched + */ + final case class RequestSpawnPoint(zone_number : Int, player : Player, spawn_group : Int) + /** + * Message that returns a discovered spawn point to a request source. + * @param zone_id the zone's text identifier + * @param building the `Building` in which the spawnpoint is located + * @param spawn_tube the spawn point holding object + */ + final case class SpawnPoint(zone_id : String, building : Building, spawn_tube : SpawnTube) + /** + * Message that informs a request source that a spawn point could not be discovered with the previous criteria. + * @param zone_number this zone's numeric identifier + * @param spawn_group the spawn point holding object; + * if `None`, then the previous `zone_number` could not be found; + * otherwise, no spawn points could be found in the zone + */ + final case class NoValidSpawnPoint(zone_number : Int, spawn_group : Option[Int]) + } + /** * Message to relinguish an item and place in on the ground. * @param item the piece of `Equipment` 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 af5c06eab..0f93d9183 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala @@ -1,8 +1,12 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.zones +import java.util.concurrent.atomic.AtomicInteger + import akka.actor.Actor import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.serverobject.structures.StructureType +import net.psforever.types.Vector3 import org.log4s.Logger /** @@ -12,29 +16,108 @@ import org.log4s.Logger class ZoneActor(zone : Zone) extends Actor { private[this] val log = org.log4s.getLogger - def receive : Receive = { + def receive : Receive = Init + + def Init : Receive = { case Zone.Init() => zone.Init ZoneSetupCheck() + context.become(Processing) + + case _ => ; + } + + def Processing : Receive = { + //frwd to Population Actor + case msg @ Zone.Population.Join => + zone.Population forward msg + + case msg @ Zone.Population.Leave => + zone.Population forward msg + + case msg @ Zone.Population.Spawn => + zone.Population forward msg + + case msg @ Zone.Population.Release => + zone.Population forward msg + + case msg @ Zone.Corpse.Add => + zone.Population forward msg + + case msg @ Zone.Corpse.Remove => + zone.Population forward msg + + //frwd to Ground Actor + case msg @ Zone.DropItemOnGround => + zone.Ground forward msg + + case msg @ Zone.GetItemOnGround => + zone.Ground forward msg + + //frwd to Vehicle Actor + case msg @ Zone.SpawnVehicle => + zone.Transport forward msg + + case msg @ Zone.DespawnVehicle => + zone.Transport forward msg + + //own + case Zone.Lattice.RequestSpawnPoint(zone_number, player, spawn_group) => + if(zone_number == zone.Number) { + val buildingTypeSet = if(spawn_group == 6) { + Set(StructureType.Tower) + } + else if(spawn_group == 7) { + Set(StructureType.Facility, StructureType.Building) + } + else { + Set.empty[StructureType.Value] + } + val playerPosition = player.Position.xy + zone.SpawnGroups() + .filter({ case((building, _)) => + building.Faction == player.Faction && buildingTypeSet.contains(building.BuildingType) + }) + .toSeq + .sortBy({ case ((building, _)) => + Vector3.DistanceSquared(playerPosition, building.Position.xy) + }) + .headOption match { + case Some((building, List(tube))) => + sender ! Zone.Lattice.SpawnPoint(zone.Id, building, tube) + + case Some((building, tubes)) => + sender ! Zone.Lattice.SpawnPoint(zone.Id, building, scala.util.Random.shuffle(tubes).head) + + case None => + sender ! Zone.Lattice.NoValidSpawnPoint(zone_number, Some(spawn_group)) + } + } + else { //wrong zone_number + sender ! Zone.Lattice.NoValidSpawnPoint(zone_number, None) + } case msg => log.warn(s"Received unexpected message - $msg") } - def ZoneSetupCheck(): Unit = { + def ZoneSetupCheck() : Int = { import ZoneActor._ - def guid(id : Int) = zone.GUID(id) val map = zone.Map + def guid(id : Int) = zone.GUID(id) val slog = org.log4s.getLogger(s"zone/${zone.Id}/sanity") - val validateObject : (Int, (PlanetSideGameObject)=>Boolean, String) => Boolean = ValidateObject(guid, slog) + val errors = new AtomicInteger(0) + val validateObject : (Int, (PlanetSideGameObject)=>Boolean, String) => Boolean = ValidateObject(guid, slog, errors) //check base to object associations - map.ObjectToBuilding.foreach({ case((object_guid, base_id)) => - if(zone.Building(base_id).isEmpty) { - slog.error(s"expected a building at id #$base_id") + map.ObjectToBuilding.foreach({ case((object_guid, building_id)) => + if(zone.Building(building_id).isEmpty) { + slog.error(s"expected a building at id #$building_id") + errors.incrementAndGet() } if(guid(object_guid).isEmpty) { slog.error(s"expected object id $object_guid to exist, but it did not") + errors.incrementAndGet() } }) @@ -55,11 +138,11 @@ class ZoneActor(zone : Zone) extends Actor { validateObject(mech_guid, ImplantMechCheck, "implant terminal mech") validateObject(interface_guid, TerminalCheck, "implant terminal interface") }) + errors.intValue() } } object ZoneActor { - /** * Recover an object from a collection and perform any number of validating tests upon it. * If the object fails any tests, log an error. @@ -73,11 +156,12 @@ object ZoneActor { * @return `true` if the object was discovered and validates correctly; * `false` if the object failed any tests */ - def ValidateObject(guid : (Int)=>Option[PlanetSideGameObject], elog : Logger) + def ValidateObject(guid : (Int)=>Option[PlanetSideGameObject], elog : Logger, errorCounter : AtomicInteger) (object_guid : Int, test : (PlanetSideGameObject)=>Boolean, description : String) : Boolean = { try { if(!test(guid(object_guid).get)) { elog.error(s"expected id $object_guid to be a $description, but it was not") + errorCounter.incrementAndGet() false } else { @@ -85,8 +169,9 @@ object ZoneActor { } } catch { - case _ : Exception => - elog.error(s"expected a $description at id $object_guid but no object is initialized") + case e : Exception => + elog.error(s"expected a $description at id $object_guid but no object is initialized - $e") + errorCounter.incrementAndGet() false } } 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 d80192743..51ebeafa4 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala @@ -2,7 +2,7 @@ package net.psforever.objects.zones import net.psforever.objects.serverobject.structures.FoundationBuilder -import net.psforever.objects.serverobject.ServerObjectBuilder +import net.psforever.objects.serverobject.{PlanetSideServerObject, ServerObjectBuilder} /** * The fixed instantiation and relation of a series of server objects.
@@ -44,10 +44,15 @@ class ZoneMap(private val name : String) { /** * Append the builder for a server object to the list of builders known to this `ZoneMap`. - * @param obj the builder for a server object + * @param id the unique id that will be assigned to this entity + * @param constructor the logic that initializes the emitted entity + * @return the current number of builders */ - def LocalObject(obj : ServerObjectBuilder[_]) : Unit = { - localObjects = localObjects :+ obj + def LocalObject[A <: PlanetSideServerObject](id : Int, constructor : ServerObjectBuilder.ConstructorType[A]) : Int = { + if(id > 0) { + localObjects = localObjects :+ ServerObjectBuilder[A](id, constructor) + } + localObjects.size } def LocalBuildings : Map[Int, FoundationBuilder] = buildings diff --git a/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala new file mode 100644 index 000000000..a22e9b431 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala @@ -0,0 +1,184 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.zones + +import akka.actor.Actor +import net.psforever.objects.{Avatar, Player} + +import scala.annotation.tailrec +import scala.collection.concurrent.TrieMap +import scala.collection.mutable.ListBuffer + +/** + * A support `Actor` that sequences adding and removing `Avatar` and `Player` objects to mappings and lists. + * The former mapping is considered to represent every user connect to the `zone` (`as Avatar` objects) + * and their current representation (as `Player` objects). + * The latter list keeps track of a group of former user representations. + * @param zone the `Zone` object + * @param playerMap the mapping of `Avatar` objects to `Player` objects + * @param corpseList a list of `Player` objects + */ +class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player]], corpseList : ListBuffer[Player]) extends Actor { + import ZonePopulationActor._ + + def receive : Receive = { + case Zone.Population.Join(avatar) => + PopulationJoin(avatar, playerMap) + + case Zone.Population.Leave(avatar) => + PopulationLeave(avatar, playerMap) match { + case None => ; + case player @ Some(_) => + sender ! Zone.Population.PlayerHasLeft(zone, player) + } + + case Zone.Population.Spawn(avatar, player) => + PopulationSpawn(avatar, player, playerMap) match { + case Some(tplayer) => + if(tplayer ne player) { + sender ! Zone.Population.PlayerAlreadySpawned(zone, player) + } + case None => + sender ! Zone.Population.PlayerCanNotSpawn(zone, player) + } + + case Zone.Population.Release(avatar) => + PopulationRelease(avatar, playerMap) match { + case Some(_) => ; + case None => + sender ! Zone.Population.PlayerHasLeft(zone, None) + } + + case Zone.Corpse.Add(player) => + CorpseAdd(player, corpseList) + + case Zone.Corpse.Remove(player) => + CorpseRemove(player, corpseList) + + case _ => ; + } +} + +object ZonePopulationActor { + /** + * Add an `avatar` as the key of an `Avatar` to `Player` object pair in the given collection. + * @param avatar an `Avatar` object + * @param playerMap the mapping of `Avatar` objects to `Player` objects + * @return true, if the mapping is for a new key; + * false, if the key already exists + */ + def PopulationJoin(avatar : Avatar, playerMap : TrieMap[Avatar, Option[Player]]) : Boolean = { + playerMap.get(avatar) match { + case Some(_) => + false + case None => + playerMap += avatar -> None + true + } + } + /** + * Remove an `avatar` from the key of an `Avatar` to `Player` object pair in the given collection. + * If a `Player` object is associated at the time, return it safely. + * @param avatar an `Avatar` object + * @param playerMap the mapping of `Avatar` objects to `Player` objects + * @return any `Player` object that was associated at the time the `avatar` was removed + */ + def PopulationLeave(avatar : Avatar, playerMap : TrieMap[Avatar, Option[Player]]) : Option[Player] = { + playerMap.remove(avatar) match { + case None => + None + case Some(tplayer) => + tplayer + } + } + + /** + * Associate a `Player` object as a value to an existing `Avatar` object that will be its key. + * Do not overwrite players that are already associated. + * @param avatar an `Avatar` object + * @param player a `Player` object + * @param playerMap the mapping of `Avatar` objects to `Player` objects + * @return the `Player` object that is associated with the `Avatar` key + */ + def PopulationSpawn(avatar : Avatar, player : Player, playerMap : TrieMap[Avatar, Option[Player]]) : Option[Player] = { + playerMap.get(avatar) match { + case None => + None + case Some(tplayer) => + tplayer match { + case Some(aplayer) => + Some(aplayer) + case None => + playerMap(avatar) = Some(player) + Some(player) + } + } + } + + /** + * Disassociate a `Player` object from an existing `Avatar` object that was be its key. + * @param avatar an `Avatar` object + * @param playerMap the mapping of `Avatar` objects to `Player` objects + * @return any `Player` object that is associated at the time + */ + def PopulationRelease(avatar : Avatar, playerMap : TrieMap[Avatar, Option[Player]]) : Option[Player] = { + playerMap.get(avatar) match { + case None => + None + case Some(tplayer) => + playerMap(avatar) = None + tplayer + } + } + + /** + * If the given `player` passes a condition check, add it to the list. + * @param player a `Player` object + * @param corpseList a list of `Player` objects + * @return true, if the `player` was added to the list; + * false, otherwise + */ + def CorpseAdd(player : Player, corpseList : ListBuffer[Player]) : Boolean = { + if(player.isBackpack) { + corpseList += player + true + } + else { + false + } + } + + /** + * Remove the given `player` from the list. + * @param player a `Player` object + * @param corpseList a list of `Player` objects + */ + def CorpseRemove(player : Player, corpseList : ListBuffer[Player]) : Unit = { + recursiveFindCorpse(corpseList.iterator, player) match { + case None => ; + case Some(index) => + corpseList.remove(index) + } + } + + /** + * A recursive function that finds and removes a specific player from a list of players. + * @param iter an `Iterator` of `Player` objects + * @param player the target `Player` + * @param index the index of the discovered `Player` object + * @return the index of the `Player` object in the list to be removed; + * `None`, otherwise + */ + @tailrec final def recursiveFindCorpse(iter : Iterator[Player], player : Player, index : Int = 0) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + if(iter.next == player) { + Some(index) + } + else { + recursiveFindCorpse(iter, player, index + 1) + } + } + } +} diff --git a/common/src/main/scala/net/psforever/packet/game/AvatarDeadStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/AvatarDeadStateMessage.scala index 4b7ad326d..5e17f544a 100644 --- a/common/src/main/scala/net/psforever/packet/game/AvatarDeadStateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/AvatarDeadStateMessage.scala @@ -2,15 +2,19 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import net.psforever.types.Vector3 -import scodec.Codec +import net.psforever.types.{PlanetSideEmpire, Vector3} +import scodec.Attempt.{Failure, Successful} +import scodec.{Codec, Err} import scodec.codecs._ +/** + * An `Enumeration` of the various states a `Player` may possess in the cycle of nanite life and death. + */ object DeadState extends Enumeration { type Type = Value val - Nothing, + Alive, Dead, Release, RespawnTime @@ -20,19 +24,41 @@ object DeadState extends Enumeration { } /** - * na - * @param state avatar's relationship with the world + * Dispatched by the server to manipulate the client's management of the `Player` object owned by the user as his "avatar."
+ *
+ * The cycle of a player is generally `Alive` to `Dead` and `Dead` to `Release` and `Release` to `RespawnTimer` to `Alive`. + * When deconstructing oneself, the user makes a jump between `Alive` and `Release`; + * and, he may make a further jump from `Release` to `Alive` depending on spawning choices. + * Being `Alive` is the most common state. + * (Despite what anyone says.) + * Being `Dead` is just a technical requirement to initialize the revive timer. + * The player should be sufficiently "dead" by having his health points decreased to zero. + * If the timer is reduced to zero, the player is sent back to their faction-appropriate sanctuary continent.
+ *
+ * `Release` causes a "dead" player to have its character model converted into a backpack or a form of pastry. + * This cancels the revival timer - the player may no longer be revived - and brings the user to the deployment map. + * From the deployment map, the user may select a place where they may respawn a new character. + * The options available form this spawn are not only related to the faction affinity of the bases compared to the user's player(s) + * but also to the field `faction` as is provided in the packet. + * If the player is converted to a state of `Release` while being alive, the deployment map is still displayed. + * Their character model is not replaced by a backpack or pastry.
+ *
+ * `RespawnTimer` is like `Dead` as it is just a formal distinction to cause the client to display a timer. + * The state indicates that the player is being resurrected at a previously-selected location in the state `Alive`. + * @param state avatar's mortal relationship with the world; + * the following timers are applicable during `Death` and `RespawnTimer`; + * `faction` is applicable mainly during `Release` * @param timer_max total length of respawn countdown, in milliseconds * @param timer initial length of the respawn timer, in milliseconds - * @param pos last position - * @param unk4 na + * @param pos player's last position + * @param faction spawn points available for this faction on redeployment map * @param unk5 na */ final case class AvatarDeadStateMessage(state : DeadState.Value, timer_max : Long, timer : Long, pos : Vector3, - unk4 : Long, + faction : PlanetSideEmpire.Value, unk5 : Boolean) extends PlanetSideGamePacket { type Packet = AvatarDeadStateMessage @@ -41,12 +67,32 @@ final case class AvatarDeadStateMessage(state : DeadState.Value, } object AvatarDeadStateMessage extends Marshallable[AvatarDeadStateMessage] { + /** + * allocate all values from the `PlanetSideEmpire` `Enumeration` + */ + private val factionLongValues = PlanetSideEmpire.values map { _.id.toLong } + + /** + * `Codec` for converting between the limited `PlanetSideEmpire` `Enumeration` and a `Long` value. + */ + private val factionLongCodec = uint32L.exmap[PlanetSideEmpire.Value] ( + fv => + if(factionLongValues.contains(fv)) { + Successful(PlanetSideEmpire(fv.toInt)) + } + else { + Failure(Err(s"$fv is not mapped to a PlanetSideEmpire value")) + }, + f => + Successful(f.id.toLong) + ) + implicit val codec : Codec[AvatarDeadStateMessage] = ( ("state" | DeadState.codec) :: ("timer_max" | uint32L) :: ("timer" | uint32L) :: ("pos" | Vector3.codec_pos) :: - ("unk4" | uint32L) :: + ("unk4" | factionLongCodec) :: ("unk5" | bool) ).as[AvatarDeadStateMessage] } diff --git a/common/src/main/scala/net/psforever/packet/game/DisconnectMessage.scala b/common/src/main/scala/net/psforever/packet/game/DisconnectMessage.scala index ddd65b5a0..4c1ec29b1 100644 --- a/common/src/main/scala/net/psforever/packet/game/DisconnectMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/DisconnectMessage.scala @@ -20,8 +20,8 @@ import scodec.codecs._ * @param unk3 na */ final case class DisconnectMessage(msg : String, - unk2 : String = "", - unk3 : String = "") + unk2 : String, + unk3 : String) extends PlanetSideGamePacket { type Packet = DisconnectMessage def opcode = GamePacketOpcode.DisconnectMessage @@ -29,6 +29,15 @@ final case class DisconnectMessage(msg : String, } object DisconnectMessage extends Marshallable[DisconnectMessage] { + /** + * Overloaded constructor that focuses only on the visible disconnection message + * @param msg the displayed message + * @return a `DisconnectMessage` object + */ + def apply(msg : String) : DisconnectMessage = { + new DisconnectMessage(msg, "", "") + } + implicit val codec : Codec[DisconnectMessage] = ( ("msg" | PacketHelpers.encodedString) :: ("unk2" | PacketHelpers.encodedString) :: diff --git a/common/src/main/scala/net/psforever/packet/game/SpawnRequestMessage.scala b/common/src/main/scala/net/psforever/packet/game/SpawnRequestMessage.scala index cbb35a959..c3049546f 100644 --- a/common/src/main/scala/net/psforever/packet/game/SpawnRequestMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/SpawnRequestMessage.scala @@ -5,7 +5,18 @@ import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacke import scodec.Codec import scodec.codecs._ - +/** + * na + * @param unk1 when defined, na; + * non-zero when selecting the sanctuary option from a non-sanctuary continent deployment map + * @param unk2 when defined, indicates type of spawn point by destination; + * 0 is unknown (may refer to all available spawns regardless of last position); + * 6 is towers; + * 7 is facilities + * @param unk3 na + * @param unk4 na + * @param unk5 when defined, the continent number + */ final case class SpawnRequestMessage(unk1 : Int, unk2 : Long, unk3 : Int, diff --git a/common/src/main/scala/net/psforever/types/Vector3.scala b/common/src/main/scala/net/psforever/types/Vector3.scala index 3b2b05833..e501d635f 100644 --- a/common/src/main/scala/net/psforever/types/Vector3.scala +++ b/common/src/main/scala/net/psforever/types/Vector3.scala @@ -9,8 +9,8 @@ final case class Vector3(x : Float, y : Float, z : Float) { /** - * Operator override for vector addition, treating `Vector3` objects as actual mathematical vectors. - * The application of this overload is "vector1 + vector2." + * Operator for vector addition, treating `Vector3` objects as actual mathematical vectors. + * The application of this definition is "vector1 + vector2." * @param vec the other `Vector3` object * @return a new `Vector3` object with the summed values */ @@ -19,8 +19,8 @@ final case class Vector3(x : Float, } /** - * Operator override for vector subtraction, treating `Vector3` objects as actual mathematical vectors. - * The application of this overload is "vector1 - vector2." + * Operator for vector subtraction, treating `Vector3` objects as actual mathematical vectors. + * The application of this definition is "vector1 - vector2." * @param vec the other `Vector3` object * @return a new `Vector3` object with the difference values */ @@ -29,7 +29,7 @@ final case class Vector3(x : Float, } /** - * Operator override for vector scaling, treating `Vector3` objects as actual mathematical vectors. + * Operator for vector scaling, treating `Vector3` objects as actual mathematical vectors. * The application of this overload is "vector * scalar" exclusively. * "scalar * vector" is invalid. * @param scalar the value to multiply this vector @@ -38,6 +38,14 @@ final case class Vector3(x : Float, def *(scalar : Float) : Vector3 = { Vector3(x*scalar, y*scalar, z*scalar) } + + /** + * Operator for returning the ground-planar coordinates + * and ignoring the perpendicular distance from the world floor. + * The application of this definition is "vector.xy" or "vector xy." + * @return a new `Vector3` object with only two of the components of the original + */ + def xy : Vector3 = Vector3(x, y, 0) } object Vector3 { diff --git a/common/src/test/scala/Vector3Test.scala b/common/src/test/scala/Vector3Test.scala index 75f1c2e53..26fcddd51 100644 --- a/common/src/test/scala/Vector3Test.scala +++ b/common/src/test/scala/Vector3Test.scala @@ -64,6 +64,11 @@ class Vector3Test extends Specification { vec * 3f mustEqual Vector3(3.8999999f, -7.7999997f, 11.700001f) } + "separate into x-component and y-component only" in { + val obj = Vector3(1.1f, 2.2f, 3.3f) + obj.xy mustEqual Vector3(1.1f, 2.2f, 0f) + } + "calculate the unit vector (zero)" in { Vector3.Unit(Vector3.Zero) mustEqual Vector3(0,0,0) } diff --git a/common/src/test/scala/game/AvatarDeadStateMessageTest.scala b/common/src/test/scala/game/AvatarDeadStateMessageTest.scala index ef8747279..f1db1008f 100644 --- a/common/src/test/scala/game/AvatarDeadStateMessageTest.scala +++ b/common/src/test/scala/game/AvatarDeadStateMessageTest.scala @@ -4,11 +4,12 @@ package game import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ -import net.psforever.types.Vector3 +import net.psforever.types.{PlanetSideEmpire, Vector3} import scodec.bits._ class AvatarDeadStateMessageTest extends Specification { val string = hex"ad3c1260801c12608009f99861fb0741e040000010" + val string_invalid = hex"ad3c1260801c12608009f99861fb0741e0400000F0" "decode" in { PacketCoding.DecodePacket(string).require match { @@ -17,15 +18,19 @@ class AvatarDeadStateMessageTest extends Specification { unk2 mustEqual 300000 unk3 mustEqual 300000 pos mustEqual Vector3(6552.617f,4602.375f,60.90625f) - unk4 mustEqual 2 + unk4 mustEqual PlanetSideEmpire.VS unk5 mustEqual true case _ => ko } } + "decode (failure)" in { + PacketCoding.DecodePacket(string_invalid).isFailure mustEqual true + } + "encode" in { - val msg = AvatarDeadStateMessage(DeadState.Dead, 300000, 300000, Vector3(6552.617f,4602.375f,60.90625f), 2, true) + val msg = AvatarDeadStateMessage(DeadState.Dead, 300000, 300000, Vector3(6552.617f,4602.375f,60.90625f), PlanetSideEmpire.VS, true) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string diff --git a/common/src/test/scala/game/DisconnectMessageTest.scala b/common/src/test/scala/game/DisconnectMessageTest.scala index 008ee149b..6e4f4c3a7 100644 --- a/common/src/test/scala/game/DisconnectMessageTest.scala +++ b/common/src/test/scala/game/DisconnectMessageTest.scala @@ -26,4 +26,8 @@ class DisconnectMessageTest extends Specification { pkt mustEqual string } + + "comparison" in { + DisconnectMessage("First") mustEqual DisconnectMessage("First", "", "") + } } diff --git a/common/src/test/scala/objects/AvatarTest.scala b/common/src/test/scala/objects/AvatarTest.scala new file mode 100644 index 000000000..95fb698fe --- /dev/null +++ b/common/src/test/scala/objects/AvatarTest.scala @@ -0,0 +1,397 @@ +// Copyright (c) 2017 PSForever +package objects + +import net.psforever.objects.GlobalDefinitions._ +import net.psforever.objects._ +import net.psforever.objects.definition.ImplantDefinition +import net.psforever.types.{CharacterGender, ImplantType, PlanetSideEmpire} +import org.specs2.mutable._ + +class AvatarTest extends Specification { + def CreatePlayer() : (Player, Avatar) = { + val avatar = Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) + val + player = Player(avatar) + player.Slot(0).Equipment = Tool(beamer) + player.Slot(2).Equipment = Tool(suppressor) + player.Slot(4).Equipment = Tool(forceblade) + player.Slot(6).Equipment = AmmoBox(bullet_9mm) + player.Slot(9).Equipment = AmmoBox(bullet_9mm) + player.Slot(12).Equipment = AmmoBox(bullet_9mm) + player.Slot(33).Equipment = AmmoBox(bullet_9mm_AP) + player.Slot(36).Equipment = AmmoBox(energy_cell) + player.Slot(39).Equipment = SimpleItem(remote_electronics_kit) + (player, avatar) + } + + "construct" in { + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + av.name mustEqual "Chord" + av.faction mustEqual PlanetSideEmpire.TR + av.sex mustEqual CharacterGender.Male + av.head mustEqual 0 + av.voice mustEqual 5 + av.BEP mustEqual 0 + av.CEP mustEqual 0 + av.Certifications mustEqual Set.empty + av.Definition.ObjectId mustEqual 121 + } + + "can maintain cumulative battle experience point values" in { + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + av.BEP mustEqual 0 + av.BEP = 100 + av.BEP mustEqual 100 + av.BEP = 700 + av.BEP mustEqual 700 + } + + "can maintain battle experience point values up to a maximum (Long.MaxValue)" in { + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + av.BEP mustEqual 0 + av.BEP = 4294967295L + av.BEP mustEqual 4294967295L + } + + "can not maintain battle experience point values below zero" in { + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + av.BEP mustEqual 0 + av.BEP = -1 + av.BEP mustEqual 0 + av.BEP = 100 + av.BEP mustEqual 100 + av.BEP = -1 + av.BEP mustEqual 0 + } + + "can maintain cumulative command experience point values" in { + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + av.CEP mustEqual 0 + av.CEP = 100 + av.CEP mustEqual 100 + av.CEP = 700 + av.CEP mustEqual 700 + } + + "can maintain command experience point values up to a maximum (Long.MaxValue)" in { + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + av.CEP mustEqual 0 + av.CEP = 4294967295L + av.CEP mustEqual 4294967295L + } + + "can not maintain command experience point values below zero" in { + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + av.CEP mustEqual 0 + av.CEP = -1 + av.CEP mustEqual 0 + av.CEP = 100 + av.CEP mustEqual 100 + av.CEP = -1 + av.CEP mustEqual 0 + } + + "can tell the difference between avatars" in { + (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) mustEqual true + + (Avatar("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + Avatar("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) mustEqual false + + (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + Avatar("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, 5)) mustEqual false + + (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Female, 0, 5)) mustEqual false + + (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 1, 5)) mustEqual false + + (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 6)) mustEqual false + } + + //refer to ImplantTest.scala for more tests + "maximum of three implant slots" in { + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants.length mustEqual 3 + obj.Implants(0).Unlocked mustEqual false + obj.Implants(0).Initialized mustEqual false + obj.Implants(0).Active mustEqual false + obj.Implants(0).Implant mustEqual ImplantType.None + obj.Implant(0) mustEqual ImplantType.None + obj.Implants(0).Installed mustEqual None + obj.Implants(1).Unlocked mustEqual false + obj.Implants(1).Initialized mustEqual false + obj.Implants(1).Active mustEqual false + obj.Implants(1).Implant mustEqual ImplantType.None + obj.Implant(1) mustEqual ImplantType.None + obj.Implants(1).Installed mustEqual None + obj.Implants(2).Unlocked mustEqual false + obj.Implants(2).Initialized mustEqual false + obj.Implants(2).Active mustEqual false + obj.Implants(2).Implant mustEqual ImplantType.None + obj.Implant(2) mustEqual ImplantType.None + obj.Implants(2).Installed mustEqual None + + obj.Implant(3) mustEqual ImplantType.None //invalid slots beyond the third always reports as ImplantType.None + } + + "can install an implant" in { + val testplant : ImplantDefinition = ImplantDefinition(1) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked = true + obj.InstallImplant(testplant) mustEqual Some(0) + obj.Implants.find({p => p.Implant == ImplantType(1)}) match { //find the installed implant + case Some(slot) => + slot.Installed mustEqual Some(testplant) + case _ => + ko + } + ok + } + + "can install implants in sequential slots" in { + val testplant1 : ImplantDefinition = ImplantDefinition(1) + val testplant2 : ImplantDefinition = ImplantDefinition(2) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked = true + obj.Implants(1).Unlocked = true + + obj.InstallImplant(testplant1) mustEqual Some(0) + obj.InstallImplant(testplant2) mustEqual Some(1) + } + + "can not install the same type of implant twice" in { + val testplant1 : ImplantDefinition = ImplantDefinition(1) + val testplant2 : ImplantDefinition = ImplantDefinition(1) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked = true + obj.Implants(1).Unlocked = true + + obj.InstallImplant(testplant1) mustEqual Some(0) + obj.InstallImplant(testplant2) mustEqual None + } + + "can not install more implants than slots available (two unlocked)" in { + val testplant1 : ImplantDefinition = ImplantDefinition(1) + val testplant2 : ImplantDefinition = ImplantDefinition(2) + val testplant3 : ImplantDefinition = ImplantDefinition(3) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked = true + obj.Implants(1).Unlocked = true + + obj.InstallImplant(testplant1) mustEqual Some(0) + obj.InstallImplant(testplant2) mustEqual Some(1) + obj.InstallImplant(testplant3) mustEqual None + } + + "can not install more implants than slots available (four implants)" in { + val testplant1 : ImplantDefinition = ImplantDefinition(1) + val testplant2 : ImplantDefinition = ImplantDefinition(2) + val testplant3 : ImplantDefinition = ImplantDefinition(3) + val testplant4 : ImplantDefinition = ImplantDefinition(4) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked = true + obj.Implants(1).Unlocked = true + obj.Implants(2).Unlocked = true + + obj.InstallImplant(testplant1) mustEqual Some(0) + obj.InstallImplant(testplant2) mustEqual Some(1) + obj.InstallImplant(testplant3) mustEqual Some(2) + obj.InstallImplant(testplant4) mustEqual None + } + + "can uninstall an implant" in { + val testplant : ImplantDefinition = ImplantDefinition(1) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked = true + obj.InstallImplant(testplant) mustEqual Some(0) + obj.Implants(0).Installed mustEqual Some(testplant) + + obj.UninstallImplant(testplant.Type) mustEqual Some(0) + obj.Implants(0).Installed mustEqual None + } + + "can uninstall just a specific implant" in { + val testplant1 : ImplantDefinition = ImplantDefinition(1) + val testplant2 : ImplantDefinition = ImplantDefinition(2) + val testplant3 : ImplantDefinition = ImplantDefinition(3) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked = true + obj.Implants(1).Unlocked = true + obj.Implants(2).Unlocked = true + obj.InstallImplant(testplant1) mustEqual Some(0) + obj.InstallImplant(testplant2) mustEqual Some(1) + obj.InstallImplant(testplant3) mustEqual Some(2) + + obj.Implant(0) mustEqual testplant1.Type + obj.Implant(1) mustEqual testplant2.Type + obj.Implant(2) mustEqual testplant3.Type + obj.UninstallImplant(testplant2.Type) mustEqual Some(1) + obj.Implant(0) mustEqual testplant1.Type + obj.Implant(1) mustEqual ImplantType.None + obj.Implant(2) mustEqual testplant3.Type + } + + "can install implants to any available slot" in { + val testplant1 : ImplantDefinition = ImplantDefinition(1) + val testplant2 : ImplantDefinition = ImplantDefinition(2) + val testplant3 : ImplantDefinition = ImplantDefinition(3) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked = true + obj.Implants(1).Unlocked = true + obj.Implants(2).Unlocked = true + obj.InstallImplant(testplant1) mustEqual Some(0) + obj.InstallImplant(testplant2) mustEqual Some(1) + obj.InstallImplant(testplant3) mustEqual Some(2) + obj.UninstallImplant(testplant2.Type) mustEqual Some(1) + obj.Implant(0) mustEqual testplant1.Type + obj.Implant(1) mustEqual ImplantType.None + obj.Implant(2) mustEqual testplant3.Type + + val testplant4 : ImplantDefinition = ImplantDefinition(4) + obj.InstallImplant(testplant4) mustEqual Some(1) + obj.Implant(0) mustEqual testplant1.Type + obj.Implant(1) mustEqual testplant4.Type + obj.Implant(2) mustEqual testplant3.Type + } + + "can reset implants to uninitialized state" in { + val testplant1 : ImplantDefinition = ImplantDefinition(1) + val testplant2 : ImplantDefinition = ImplantDefinition(2) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked = true + obj.Implants(1).Unlocked = true + obj.InstallImplant(testplant1) mustEqual Some(0) + obj.InstallImplant(testplant2) mustEqual Some(1) + obj.Implants(0).Initialized = true + obj.Implants(0).Active = true + obj.Implants(1).Initialized = true + + obj.Implants(0).Initialized mustEqual true + obj.Implants(0).Active mustEqual true + obj.Implants(1).Initialized mustEqual true + obj.ResetAllImplants() + obj.Implants(0).Initialized mustEqual false + obj.Implants(0).Active mustEqual false + obj.Implants(1).Initialized mustEqual false + } + + "does not have any loadout specifications by default" in { + val (_, avatar) = CreatePlayer() + (0 to 9).foreach { avatar.LoadLoadout(_) mustEqual None } + ok + } + + "save player's current inventory as a loadout" in { + val (obj, avatar) = CreatePlayer() + obj.Slot(0).Equipment.get.asInstanceOf[Tool].Magazine = 1 //non-standard but legal + obj.Slot(2).Equipment.get.asInstanceOf[Tool].AmmoSlot.Magazine = 100 //non-standard (and out of range, real=25) + avatar.SaveLoadout(obj, "test", 0) + + avatar.LoadLoadout(0) match { + case Some(items) => + items.Label mustEqual "test" + items.ExoSuit mustEqual obj.ExoSuit + items.Subtype mustEqual 0 + + items.VisibleSlots.length mustEqual 3 + val holsters = items.VisibleSlots.sortBy(_.index) + holsters.head.index mustEqual 0 + holsters.head.item.asInstanceOf[Loadout.ShorthandTool].definition mustEqual beamer + holsters.head.item.asInstanceOf[Loadout.ShorthandTool].ammo.head.ammo.capacity mustEqual 1 //we changed this + holsters(1).index mustEqual 2 + holsters(1).item.asInstanceOf[Loadout.ShorthandTool].definition mustEqual suppressor + holsters(1).item.asInstanceOf[Loadout.ShorthandTool].ammo.head.ammo.capacity mustEqual 100 //we changed this + holsters(2).index mustEqual 4 + holsters(2).item.asInstanceOf[Loadout.ShorthandTool].definition mustEqual forceblade + + items.Inventory.length mustEqual 6 + val inventory = items.Inventory.sortBy(_.index) + inventory.head.index mustEqual 6 + inventory.head.item.asInstanceOf[Loadout.ShorthandAmmoBox].definition mustEqual bullet_9mm + inventory(1).index mustEqual 9 + inventory(1).item.asInstanceOf[Loadout.ShorthandAmmoBox].definition mustEqual bullet_9mm + inventory(2).index mustEqual 12 + inventory(2).item.asInstanceOf[Loadout.ShorthandAmmoBox].definition mustEqual bullet_9mm + inventory(3).index mustEqual 33 + inventory(3).item.asInstanceOf[Loadout.ShorthandAmmoBox].definition mustEqual bullet_9mm_AP + inventory(4).index mustEqual 36 + inventory(4).item.asInstanceOf[Loadout.ShorthandAmmoBox].definition mustEqual energy_cell + inventory(5).index mustEqual 39 + inventory(5).item.asInstanceOf[Loadout.ShorthandSimpleItem].definition mustEqual remote_electronics_kit + case None => + ko + } + } + + "save player's current inventory as a loadout, only found in the called-out slot number" in { + val (obj, avatar) = CreatePlayer() + avatar.SaveLoadout(obj, "test", 0) + + avatar.LoadLoadout(1).isDefined mustEqual false + avatar.LoadLoadout(0).isDefined mustEqual true + } + + "try to save player's current inventory as a loadout, but will not save to an invalid slot" in { + val (obj, avatar) = CreatePlayer() + avatar.SaveLoadout(obj, "test", 10) + + avatar.LoadLoadout(10) mustEqual None + } + + "save player's current inventory as a loadout, without inventory contents" in { + val (obj, avatar) = CreatePlayer() + obj.Inventory.Clear() + avatar.SaveLoadout(obj, "test", 0) + + avatar.LoadLoadout(0) match { + case Some(items) => + items.Label mustEqual "test" + items.ExoSuit mustEqual obj.ExoSuit + items.Subtype mustEqual 0 + items.VisibleSlots.length mustEqual 3 + items.Inventory.length mustEqual 0 //empty + case None => + ko + } + } + + "save player's current inventory as a loadout, without visible slot contents" in { + val (obj, avatar) = CreatePlayer() + obj.Slot(0).Equipment = None + obj.Slot(2).Equipment = None + obj.Slot(4).Equipment = None + avatar.SaveLoadout(obj, "test", 0) + + avatar.LoadLoadout(0) match { + case Some(items) => + items.Label mustEqual "test" + items.ExoSuit mustEqual obj.ExoSuit + items.Subtype mustEqual 0 + items.VisibleSlots.length mustEqual 0 //empty + items.Inventory.length mustEqual 6 + case None => + ko + } + } + + "save, load, delete; rapidly" in { + val (obj, avatar) = CreatePlayer() + avatar.SaveLoadout(obj, "test", 0) + + avatar.LoadLoadout(0).isDefined mustEqual true + avatar.DeleteLoadout(0) + avatar.LoadLoadout(0) mustEqual None + } + + "the fifth slot is the locker wrapped in an EquipmentSlot" in { + val (_, avatar) = CreatePlayer() + avatar.FifthSlot.Equipment.contains(avatar.Locker) + } + + "toString" in { + Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5).toString mustEqual "TR Chord" + } +} diff --git a/common/src/test/scala/objects/BuildingTest.scala b/common/src/test/scala/objects/BuildingTest.scala index dc533b1b8..72da3fab0 100644 --- a/common/src/test/scala/objects/BuildingTest.scala +++ b/common/src/test/scala/objects/BuildingTest.scala @@ -6,7 +6,7 @@ import net.psforever.objects.GlobalDefinitions import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.doors.{Door, DoorControl} -import net.psforever.objects.serverobject.structures.{Amenity, Building, BuildingControl, WarpGate} +import net.psforever.objects.serverobject.structures._ import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID import net.psforever.types.PlanetSideEmpire @@ -27,7 +27,7 @@ class AmenityTest extends Specification { "can be owned by a building" in { val ao = new AmenityObject() - val bldg = Building(10, Zone.Nowhere) + val bldg = Building(10, Zone.Nowhere, StructureType.Building) ao.Owner = bldg ao.Owner mustEqual bldg @@ -51,7 +51,7 @@ class AmenityTest extends Specification { "confer faction allegiance through ownership" in { //see FactionAffinityTest val ao = new AmenityObject() - val bldg = Building(10, Zone.Nowhere) + val bldg = Building(10, Zone.Nowhere, StructureType.Building) ao.Owner = bldg bldg.Faction mustEqual PlanetSideEmpire.NEUTRAL ao.Faction mustEqual PlanetSideEmpire.NEUTRAL @@ -66,7 +66,7 @@ class AmenityTest extends Specification { class BuildingTest extends Specification { "Building" should { "construct" in { - val bldg = Building(10, Zone.Nowhere) + val bldg = Building(10, Zone.Nowhere, StructureType.Building) bldg.Id mustEqual 10 bldg.Actor mustEqual ActorRef.noSender bldg.Amenities mustEqual Nil @@ -75,7 +75,7 @@ class BuildingTest extends Specification { } "change faction affinity" in { - val bldg = Building(10, Zone.Nowhere) + val bldg = Building(10, Zone.Nowhere, StructureType.Building) bldg.Faction mustEqual PlanetSideEmpire.NEUTRAL bldg.Faction = PlanetSideEmpire.TR @@ -83,7 +83,7 @@ class BuildingTest extends Specification { } "keep track of amenities" in { - val bldg = Building(10, Zone.Nowhere) + val bldg = Building(10, Zone.Nowhere, StructureType.Building) val door1 = Door(GlobalDefinitions.door) val door2 = Door(GlobalDefinitions.door) @@ -114,7 +114,7 @@ class WarpGateTest extends Specification { class BuildingControl1Test extends ActorTest { "Building Control" should { "construct" in { - val bldg = Building(10, Zone.Nowhere) + val bldg = Building(10, Zone.Nowhere, StructureType.Building) bldg.Actor = system.actorOf(Props(classOf[BuildingControl], bldg), "test") assert(bldg.Actor != ActorRef.noSender) } @@ -124,7 +124,7 @@ class BuildingControl1Test extends ActorTest { class BuildingControl2Test extends ActorTest { "Building Control" should { "convert and assert faction affinity on convert request" in { - val bldg = Building(10, Zone.Nowhere) + val bldg = Building(10, Zone.Nowhere, StructureType.Building) bldg.Faction = PlanetSideEmpire.TR bldg.Actor = system.actorOf(Props(classOf[BuildingControl], bldg), "test") assert(bldg.Faction == PlanetSideEmpire.TR) @@ -142,7 +142,7 @@ class BuildingControl2Test extends ActorTest { class BuildingControl3Test extends ActorTest { "Building Control" should { "convert and assert faction affinity on convert request, and for each of its amenities" in { - val bldg = Building(10, Zone.Nowhere) + val bldg = Building(10, Zone.Nowhere, StructureType.Building) bldg.Faction = PlanetSideEmpire.TR bldg.Actor = system.actorOf(Props(classOf[BuildingControl], bldg), "building-test") val door1 = Door(GlobalDefinitions.door) diff --git a/common/src/test/scala/objects/ConverterTest.scala b/common/src/test/scala/objects/ConverterTest.scala index 57d8fc863..ccc1f7ae4 100644 --- a/common/src/test/scala/objects/ConverterTest.scala +++ b/common/src/test/scala/objects/ConverterTest.scala @@ -151,6 +151,7 @@ class ConverterTest extends Specification { } "Player" should { + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) val obj : Player = { /* Create an AmmoBoxDefinition with which to build two AmmoBoxes @@ -171,7 +172,7 @@ class ConverterTest extends Specification { val tool = Tool(tdef) tool.GUID = PlanetSideGUID(92) tool.AmmoSlot.Box.GUID = PlanetSideGUID(90) - val obj = Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = Player(avatar) obj.GUID = PlanetSideGUID(93) obj.Slot(2).Equipment = tool obj.Slot(5).Equipment.get.GUID = PlanetSideGUID(94) @@ -182,7 +183,7 @@ class ConverterTest extends Specification { val converter = new CharacterSelectConverter "convert to packet (BR < 24)" in { - obj.BEP = 0 + avatar.BEP = 0 obj.Definition.Packet.DetailedConstructorData(obj) match { case Success(_) => ok @@ -198,7 +199,7 @@ class ConverterTest extends Specification { } "convert to packet (BR >= 24)" in { - obj.BEP = 10000000 + avatar.BEP = 10000000 obj.Definition.Packet.DetailedConstructorData(obj) match { case Success(_) => ok @@ -214,7 +215,7 @@ class ConverterTest extends Specification { } "convert to simple packet (BR < 24)" in { - obj.BEP = 0 + avatar.BEP = 0 converter.DetailedConstructorData(obj) match { case Success(_) => ok @@ -226,7 +227,7 @@ class ConverterTest extends Specification { } "convert to simple packet (BR >= 24)" in { - obj.BEP = 10000000 + avatar.BEP = 10000000 converter.DetailedConstructorData(obj) match { case Success(_) => ok diff --git a/common/src/test/scala/objects/DoorTest.scala b/common/src/test/scala/objects/DoorTest.scala index 228d44ab5..73e55c00d 100644 --- a/common/src/test/scala/objects/DoorTest.scala +++ b/common/src/test/scala/objects/DoorTest.scala @@ -2,9 +2,9 @@ package objects import akka.actor.{ActorRef, ActorSystem, Props} -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.doors.{Door, DoorControl} -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.zones.Zone import net.psforever.packet.game.{PlanetSideGUID, UseItemMessage} import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} @@ -13,7 +13,7 @@ import org.specs2.mutable.Specification import scala.concurrent.duration.Duration class DoorTest extends Specification { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) "Door" should { "construct" in { @@ -121,8 +121,8 @@ object DoorControlTest { def SetUpAgents(faction : PlanetSideEmpire.Value)(implicit system : ActorSystem) : (Player, Door) = { val door = Door(GlobalDefinitions.door) door.Actor = system.actorOf(Props(classOf[DoorControl], door), "door") - door.Owner = new Building(0, Zone.Nowhere) + door.Owner = new Building(0, Zone.Nowhere, StructureType.Building) door.Owner.Faction = faction - (Player("test", faction, CharacterGender.Male, 0, 0), door) + (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), door) } } diff --git a/common/src/test/scala/objects/FactionAffinityTest.scala b/common/src/test/scala/objects/FactionAffinityTest.scala index c4598896a..a3163d9f7 100644 --- a/common/src/test/scala/objects/FactionAffinityTest.scala +++ b/common/src/test/scala/objects/FactionAffinityTest.scala @@ -5,7 +5,7 @@ import akka.actor.{Actor, ActorSystem, Props} import net.psforever.objects.{GlobalDefinitions, Vehicle} import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.doors.Door -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.zones.Zone import net.psforever.types.PlanetSideEmpire import org.specs2.mutable.Specification @@ -42,7 +42,7 @@ class FactionAffinityTest extends Specification { "inherits affinity from owner 2" in { val obj = new Door(GlobalDefinitions.door) - val bldg = new Building(1, Zone.Nowhere) + val bldg = new Building(1, Zone.Nowhere, StructureType.Building) obj.Owner = bldg obj.Faction mustEqual PlanetSideEmpire.NEUTRAL diff --git a/common/src/test/scala/objects/IFFLockTest.scala b/common/src/test/scala/objects/IFFLockTest.scala index 05eb1e2fb..5cd66613c 100644 --- a/common/src/test/scala/objects/IFFLockTest.scala +++ b/common/src/test/scala/objects/IFFLockTest.scala @@ -3,10 +3,9 @@ package objects import akka.actor.{ActorRef, ActorSystem, Props} import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.locks.{IFFLock, IFFLockControl} -import net.psforever.objects.serverobject.structures.Building -import net.psforever.objects.serverobject.terminals.{Terminal, TerminalControl, TerminalDefinition} +import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID import net.psforever.types.{CharacterGender, PlanetSideEmpire} @@ -68,8 +67,8 @@ object IFFLockControlTest { def SetUpAgents(faction : PlanetSideEmpire.Value)(implicit system : ActorSystem) : (Player, IFFLock) = { val lock = IFFLock(GlobalDefinitions.lock_external) lock.Actor = system.actorOf(Props(classOf[IFFLockControl], lock), "lock-control") - lock.Owner = new Building(0, Zone.Nowhere) + lock.Owner = new Building(0, Zone.Nowhere, StructureType.Building) lock.Owner.Faction = faction - (Player("test", faction, CharacterGender.Male, 0, 0), lock) + (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), lock) } } diff --git a/common/src/test/scala/objects/LoadoutTest.scala b/common/src/test/scala/objects/LoadoutTest.scala index db0bd0f05..fb01d6963 100644 --- a/common/src/test/scala/objects/LoadoutTest.scala +++ b/common/src/test/scala/objects/LoadoutTest.scala @@ -4,193 +4,87 @@ package objects import net.psforever.objects._ import net.psforever.types.{CharacterGender, ExoSuitType, PlanetSideEmpire} import net.psforever.objects.GlobalDefinitions._ -import net.psforever.objects.equipment.{Equipment, EquipmentSize} import org.specs2.mutable._ class LoadoutTest extends Specification { + val avatar = Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) + def CreatePlayer() : Player = { - val - player = Player("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) - player.Slot(0).Equipment = Tool(beamer) - player.Slot(2).Equipment = Tool(suppressor) - player.Slot(4).Equipment = Tool(forceblade) - player.Slot(6).Equipment = AmmoBox(bullet_9mm) - player.Slot(9).Equipment = AmmoBox(bullet_9mm) - player.Slot(12).Equipment = AmmoBox(bullet_9mm) - player.Slot(33).Equipment = AmmoBox(bullet_9mm_AP) - player.Slot(36).Equipment = AmmoBox(energy_cell) - player.Slot(39).Equipment = SimpleItem(remote_electronics_kit) - player + new Player(avatar) { + Slot(0).Equipment = Tool(beamer) + Slot(2).Equipment = Tool(suppressor) + Slot(4).Equipment = Tool(forceblade) + Slot(6).Equipment = ConstructionItem(ace) + Slot(9).Equipment = AmmoBox(bullet_9mm) + Slot(12).Equipment = AmmoBox(bullet_9mm) + Slot(33).Equipment = Kit(medkit) + Slot(39).Equipment = SimpleItem(remote_electronics_kit) + } } - "Player Loadout" should { - "test sample player" in { - val obj : Player = CreatePlayer() - obj.Holsters()(0).Equipment.get.Definition mustEqual beamer - obj.Holsters()(2).Equipment.get.Definition mustEqual suppressor - obj.Holsters()(4).Equipment.get.Definition mustEqual forceblade - obj.Slot(6).Equipment.get.Definition mustEqual bullet_9mm - obj.Slot(9).Equipment.get.Definition mustEqual bullet_9mm - obj.Slot(12).Equipment.get.Definition mustEqual bullet_9mm - obj.Slot(33).Equipment.get.Definition mustEqual bullet_9mm_AP - obj.Slot(36).Equipment.get.Definition mustEqual energy_cell - obj.Slot(39).Equipment.get.Definition mustEqual remote_electronics_kit - } + "test sample player" in { + val player = CreatePlayer() + player.Holsters()(0).Equipment.get.Definition mustEqual beamer + player.Holsters()(2).Equipment.get.Definition mustEqual suppressor + player.Holsters()(4).Equipment.get.Definition mustEqual forceblade + player.Slot(6).Equipment.get.Definition mustEqual ace + player.Slot(9).Equipment.get.Definition mustEqual bullet_9mm + player.Slot(12).Equipment.get.Definition mustEqual bullet_9mm + player.Slot(33).Equipment.get.Definition mustEqual medkit + player.Slot(39).Equipment.get.Definition mustEqual remote_electronics_kit + } - "do not load, if never saved" in { - CreatePlayer().LoadLoadout(0) mustEqual None - } + "create a loadout that contains a player's inventory" in { + val player = CreatePlayer() + val obj = Loadout.Create(player, "test") - "save but incorrect load" in { - val obj : Player = CreatePlayer() - obj.SaveLoadout("test", 0) + obj.Label mustEqual "test" + obj.ExoSuit mustEqual obj.ExoSuit + obj.Subtype mustEqual 0 - obj.LoadLoadout(1) mustEqual None - } + obj.VisibleSlots.length mustEqual 3 + val holsters = obj.VisibleSlots.sortBy(_.index) + holsters.head.index mustEqual 0 + holsters.head.item.asInstanceOf[Loadout.ShorthandTool].definition mustEqual beamer + holsters(1).index mustEqual 2 + holsters(1).item.asInstanceOf[Loadout.ShorthandTool].definition mustEqual suppressor + holsters(2).index mustEqual 4 + holsters(2).item.asInstanceOf[Loadout.ShorthandTool].definition mustEqual forceblade - "save and load" in { - val obj : Player = CreatePlayer() - obj.Slot(0).Equipment.get.asInstanceOf[Tool].Magazine = 1 //non-standard but legal - obj.Slot(2).Equipment.get.asInstanceOf[Tool].AmmoSlot.Magazine = 100 //non-standard (and out of range, real=25) - obj.SaveLoadout("test", 0) + obj.Inventory.length mustEqual 5 + val inventory = obj.Inventory.sortBy(_.index) + inventory.head.index mustEqual 6 + inventory.head.item.asInstanceOf[Loadout.ShorthandConstructionItem].definition mustEqual ace + inventory(1).index mustEqual 9 + inventory(1).item.asInstanceOf[Loadout.ShorthandAmmoBox].definition mustEqual bullet_9mm + inventory(2).index mustEqual 12 + inventory(2).item.asInstanceOf[Loadout.ShorthandAmmoBox].definition mustEqual bullet_9mm + inventory(3).index mustEqual 33 + inventory(3).item.asInstanceOf[Loadout.ShorthandKit].definition mustEqual medkit + inventory(4).index mustEqual 39 + inventory(4).item.asInstanceOf[Loadout.ShorthandSimpleItem].definition mustEqual remote_electronics_kit + } - obj.LoadLoadout(0) match { - case Some(items) => - items.Label mustEqual "test" - items.ExoSuit mustEqual obj.ExoSuit - items.Subtype mustEqual 0 + "distinguish MAX subtype information" in { + val player = CreatePlayer() + val slot = player.Slot(0) + slot.Equipment = None //only an unequipped slot can have its Equipment Size changed (Rifle -> Max) + Player.SuitSetup(player, ExoSuitType.MAX) - items.VisibleSlots.length mustEqual 3 - val holsters = items.VisibleSlots.sortBy(_.index) - holsters.head.index mustEqual 0 - holsters.head.item.asInstanceOf[Loadout.ShorthandTool].tdef mustEqual beamer - holsters.head.item.asInstanceOf[Loadout.ShorthandTool].ammo.head.ammo.capacity mustEqual 1 - holsters(1).index mustEqual 2 - holsters(1).item.asInstanceOf[Loadout.ShorthandTool].tdef mustEqual suppressor - holsters(1).item.asInstanceOf[Loadout.ShorthandTool].ammo.head.ammo.capacity mustEqual 100 - holsters(2).index mustEqual 4 - holsters(2).item.asInstanceOf[Loadout.ShorthandTool].tdef mustEqual forceblade + val ldout1 = Loadout.Create(player, "weaponless") + slot.Equipment = None + slot.Equipment = Tool(trhev_dualcycler) + val ldout2 = Loadout.Create(player, "cycler") + slot.Equipment = None + slot.Equipment = Tool(trhev_pounder) + val ldout3 = Loadout.Create(player, "pounder") + slot.Equipment = None + slot.Equipment = Tool(trhev_burster) + val ldout4 = Loadout.Create(player, "burster") - items.Inventory.length mustEqual 6 - val inventory = items.Inventory.sortBy(_.index) - inventory.head.index mustEqual 6 - inventory.head.item.asInstanceOf[Loadout.ShorthandAmmoBox].adef mustEqual bullet_9mm - inventory(1).index mustEqual 9 - inventory(1).item.asInstanceOf[Loadout.ShorthandAmmoBox].adef mustEqual bullet_9mm - inventory(2).index mustEqual 12 - inventory(2).item.asInstanceOf[Loadout.ShorthandAmmoBox].adef mustEqual bullet_9mm - inventory(3).index mustEqual 33 - inventory(3).item.asInstanceOf[Loadout.ShorthandAmmoBox].adef mustEqual bullet_9mm_AP - inventory(4).index mustEqual 36 - inventory(4).item.asInstanceOf[Loadout.ShorthandAmmoBox].adef mustEqual energy_cell - inventory(5).index mustEqual 39 - inventory(5).item.asInstanceOf[Loadout.ShorthandSimpleItem].sdef mustEqual remote_electronics_kit - case None => - ko - } - } - - "save without inventory contents" in { - val obj : Player = CreatePlayer() - obj.Inventory.Clear() - obj.SaveLoadout("test", 0) - - obj.LoadLoadout(0) match { - case Some(items) => - items.Label mustEqual "test" - items.ExoSuit mustEqual obj.ExoSuit - items.Subtype mustEqual 0 - items.VisibleSlots.length mustEqual 3 - items.Inventory.length mustEqual 0 //empty - case None => - ko - } - } - - "save without visible slot contents" in { - val obj : Player = CreatePlayer() - obj.Slot(0).Equipment = None - obj.Slot(2).Equipment = None - obj.Slot(4).Equipment = None - obj.SaveLoadout("test", 0) - - obj.LoadLoadout(0) match { - case Some(items) => - items.Label mustEqual "test" - items.ExoSuit mustEqual obj.ExoSuit - items.Subtype mustEqual 0 - items.VisibleSlots.length mustEqual 0 //empty - items.Inventory.length mustEqual 6 - case None => - ko - } - } - - "save (a construction item) and load" in { - val obj : Player = CreatePlayer() - obj.Inventory.Clear() - obj.Slot(6).Equipment = ConstructionItem(advanced_ace) - obj.SaveLoadout("test", 0) - - obj.LoadLoadout(0) match { - case Some(items) => - items.Inventory.length mustEqual 1 - items.Inventory.head.index mustEqual 6 - items.Inventory.head.item.asInstanceOf[Loadout.ShorthandConstructionItem].cdef mustEqual advanced_ace - case None => - ko - } - } - - "save (a kit) and load" in { - val obj : Player = CreatePlayer() - obj.Inventory.Clear() - obj.Slot(6).Equipment = Kit(medkit) - obj.SaveLoadout("test", 0) - - obj.LoadLoadout(0) match { - case Some(items) => - items.Inventory.length mustEqual 1 - items.Inventory.head.index mustEqual 6 - items.Inventory.head.item.asInstanceOf[Loadout.ShorthandKit].kdef mustEqual medkit - case None => - ko - } - } - - "save, load, delete" in { - val obj : Player = CreatePlayer() - obj.SaveLoadout("test", 0) - - obj.LoadLoadout(0) match { - case None => - ko - case Some(_) => ; //good; keep going - } - obj.DeleteLoadout(0) - obj.LoadLoadout(0) mustEqual None - } - - "distinguish MAX subtype information" in { - val obj : Player = CreatePlayer() - val slot = obj.Slot(0) - slot.Equipment = None //only an unequipped slot can have its Equipment Size changed (Rifle -> Max) - Player.SuitSetup(obj, ExoSuitType.MAX) - obj.SaveLoadout("generic", 0) //weaponless - slot.Equipment = None - slot.Equipment = Tool(trhev_dualcycler) - obj.SaveLoadout("cycler", 1) - slot.Equipment = None - slot.Equipment = Tool(trhev_pounder) - obj.SaveLoadout("pounder", 2) - slot.Equipment = None - slot.Equipment = Tool(trhev_burster) - obj.SaveLoadout("burster", 3) - - obj.LoadLoadout(0).get.Subtype mustEqual 0 - obj.LoadLoadout(1).get.Subtype mustEqual 1 - obj.LoadLoadout(2).get.Subtype mustEqual 2 - obj.LoadLoadout(3).get.Subtype mustEqual 3 - } + ldout1.Subtype mustEqual 0 + ldout2.Subtype mustEqual 1 + ldout3.Subtype mustEqual 2 + ldout4.Subtype mustEqual 3 } } diff --git a/common/src/test/scala/objects/MountableTest.scala b/common/src/test/scala/objects/MountableTest.scala index 83ba5babd..56fe1f831 100644 --- a/common/src/test/scala/objects/MountableTest.scala +++ b/common/src/test/scala/objects/MountableTest.scala @@ -2,7 +2,7 @@ package objects import akka.actor.{Actor, ActorRef, Props} -import net.psforever.objects.Player +import net.psforever.objects.{Avatar, Player} import net.psforever.objects.definition.{ObjectDefinition, SeatDefinition} import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.PlanetSideServerObject @@ -25,7 +25,7 @@ class MountableControl1Test extends ActorTest() { class MountableControl2Test extends ActorTest() { "MountableControl" should { "let a player mount" in { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val obj = new MountableTest.MountableTestObject obj.Actor = system.actorOf(Props(classOf[MountableTest.MountableTestControl], obj), "mountable") val msg = Mountable.TryMount(player, 0) @@ -46,8 +46,8 @@ class MountableControl2Test extends ActorTest() { class MountableControl3Test extends ActorTest() { "MountableControl" should { "block a player from mounting" in { - val player1 = Player("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) - val player2 = Player("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player1 = Player(Avatar("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player2 = Player(Avatar("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val obj = new MountableTest.MountableTestObject obj.Actor = system.actorOf(Props(classOf[MountableTest.MountableTestControl], obj), "mountable") obj.Actor ! Mountable.TryMount(player1, 0) diff --git a/common/src/test/scala/objects/PlayerTest.scala b/common/src/test/scala/objects/PlayerTest.scala index d082fe2f2..dd9771cb5 100644 --- a/common/src/test/scala/objects/PlayerTest.scala +++ b/common/src/test/scala/objects/PlayerTest.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever package objects +import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects._ import net.psforever.objects.definition.{ImplantDefinition, SimpleItemDefinition} import net.psforever.objects.equipment.EquipmentSize @@ -8,38 +9,53 @@ import net.psforever.packet.game.PlanetSideGUID import net.psforever.types.{CharacterGender, ExoSuitType, ImplantType, PlanetSideEmpire} import org.specs2.mutable._ +import scala.util.Success + class PlayerTest extends Specification { + def TestPlayer(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : Int) : Player = { + new Player(Avatar(name, faction, sex, head, voice)) + } + "construct" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.isAlive mustEqual false + obj.FacingYawUpper mustEqual 0 + obj.Jumping mustEqual false + obj.Crouching mustEqual false + obj.Cloaked mustEqual false + + obj.FacingYawUpper = 1.3f + obj.Jumping = true + obj.Crouching = true + obj.Cloaked = true + obj.FacingYawUpper mustEqual 1.3f + obj.Jumping mustEqual true + obj.Crouching mustEqual true + obj.Cloaked mustEqual true } "different players" in { - (Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) mustEqual true - (Player("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Player("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) mustEqual false - (Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Player("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, 5)) mustEqual false - (Player("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Player("Chord2", PlanetSideEmpire.TR, CharacterGender.Female, 0, 5)) mustEqual false - (Player("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Player("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 1, 5)) mustEqual false - (Player("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Player("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 6)) mustEqual false - } + (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) mustEqual true - "become a backpack" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) - obj.isAlive mustEqual false - obj.isBackpack mustEqual false - obj.Release - obj.isAlive mustEqual false - obj.isBackpack mustEqual true + (TestPlayer("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + TestPlayer("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) mustEqual false + + (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + TestPlayer("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, 5)) mustEqual false + + (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Female, 0, 5)) mustEqual false + + (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 1, 5)) mustEqual false + + (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == + TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 6)) mustEqual false } "(re)spawn" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.isAlive mustEqual false obj.Health mustEqual 0 obj.Stamina mustEqual 0 @@ -54,32 +70,24 @@ class PlayerTest extends Specification { obj.Armor mustEqual 50 } - "set new maximum values (health, stamina)" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) - obj.MaxHealth mustEqual 100 - obj.MaxStamina mustEqual 100 - obj.MaxHealth = 123 - obj.MaxStamina = 456 + "will not (re)spawn if not dead" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.Spawn - obj.Health mustEqual 123 - obj.Stamina mustEqual 456 + obj.Health mustEqual 100 + obj.Armor mustEqual 50 + obj.isAlive mustEqual true + + obj.Health = 10 + obj.Armor = 10 + obj.Health mustEqual 10 + obj.Armor mustEqual 10 + obj.Spawn + obj.Health mustEqual 10 + obj.Armor mustEqual 10 } - "init (Standard Exo-Suit)" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) - obj.ExoSuit mustEqual ExoSuitType.Standard - obj.Slot(0).Size mustEqual EquipmentSize.Pistol - obj.Slot(1).Size mustEqual EquipmentSize.Blocked - obj.Slot(2).Size mustEqual EquipmentSize.Rifle - obj.Slot(3).Size mustEqual EquipmentSize.Blocked - obj.Slot(4).Size mustEqual EquipmentSize.Melee - obj.Inventory.Width mustEqual 9 - obj.Inventory.Height mustEqual 6 - obj.Inventory.Offset mustEqual 6 - } - - "die" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + "can die" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.Spawn obj.Armor = 35 //50 -> 35 obj.isAlive mustEqual true @@ -93,9 +101,80 @@ class PlayerTest extends Specification { obj.Armor mustEqual 35 } + "can not become a backpack if alive" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Spawn + obj.isAlive mustEqual true + obj.isBackpack mustEqual false + obj.Release + obj.isAlive mustEqual true + obj.isBackpack mustEqual false + } + + "can become a backpack" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.isAlive mustEqual false + obj.isBackpack mustEqual false + obj.Release + obj.isAlive mustEqual false + obj.isBackpack mustEqual true + } + + "set new maximum values (health, stamina)" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.MaxHealth mustEqual 100 + obj.MaxStamina mustEqual 100 + obj.MaxHealth = 123 + obj.MaxStamina = 456 + obj.Spawn + obj.Health mustEqual 123 + obj.Stamina mustEqual 456 + } + + "set new values (health, armor, stamina) but only when alive" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Health = 23 + obj.Armor = 34 + obj.Stamina = 45 + obj.Health mustEqual 0 + obj.Armor mustEqual 0 + obj.Stamina mustEqual 0 + + obj.Spawn + obj.Health mustEqual obj.MaxHealth + obj.Armor mustEqual obj.MaxArmor + obj.Stamina mustEqual obj.MaxStamina + obj.Health = 23 + obj.Armor = 34 + obj.Stamina = 45 + obj.Health mustEqual 23 + obj.Armor mustEqual 34 + obj.Stamina mustEqual 45 + } + + "has visible slots" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.VisibleSlots mustEqual Set(0,1,2,3,4) + obj.ExoSuit = ExoSuitType.MAX + obj.VisibleSlots mustEqual Set(0) + } + + "init (Standard Exo-Suit)" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.ExoSuit mustEqual ExoSuitType.Standard + obj.Slot(0).Size mustEqual EquipmentSize.Pistol + obj.Slot(1).Size mustEqual EquipmentSize.Blocked + obj.Slot(2).Size mustEqual EquipmentSize.Rifle + obj.Slot(3).Size mustEqual EquipmentSize.Blocked + obj.Slot(4).Size mustEqual EquipmentSize.Melee + obj.Inventory.Width mustEqual 9 + obj.Inventory.Height mustEqual 6 + obj.Inventory.Offset mustEqual 6 + } + "draw equipped holsters only" in { val wep = SimpleItem(SimpleItemDefinition(149)) - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.Slot(1).Size = EquipmentSize.Pistol obj.Slot(1).Equipment = wep obj.DrawnSlot mustEqual Player.HandsDownSlot @@ -108,7 +187,7 @@ class PlayerTest extends Specification { "remember the last drawn holster" in { val wep1 = SimpleItem(SimpleItemDefinition(149)) val wep2 = SimpleItem(SimpleItemDefinition(149)) - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.Slot(0).Size = EquipmentSize.Pistol obj.Slot(0).Equipment = wep1 obj.Slot(1).Size = EquipmentSize.Pistol @@ -147,7 +226,7 @@ class PlayerTest extends Specification { "hold something in their free hand" in { val wep = SimpleItem(SimpleItemDefinition(149)) - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.Slot(Player.FreeHandSlot).Equipment = wep obj.Slot(Player.FreeHandSlot).Equipment.get.Definition.ObjectId mustEqual 149 @@ -155,15 +234,15 @@ class PlayerTest extends Specification { "provide an invalid hand that can not hold anything" in { val wep = SimpleItem(SimpleItemDefinition(149)) - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.Slot(-1).Equipment = wep obj.Slot(-1).Equipment mustEqual None } - "search for the smallest available slot in which to satore equipment" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) - obj.Inventory.Resize(3,3) + "search for the smallest available slot in which to store equipment" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Inventory.Resize(3,3) //fits one item obj.Fit(Tool(GlobalDefinitions.beamer)) mustEqual Some(0) @@ -176,46 +255,180 @@ class PlayerTest extends Specification { obj.Slot(6).Equipment = ammo obj.Fit(ammo2) mustEqual Some(Player.FreeHandSlot) obj.Slot(Player.FreeHandSlot).Equipment = ammo2 - obj.Fit(ammo2) mustEqual None + obj.Fit(ammo3) mustEqual None } - "install an implant" in { - val testplant : ImplantDefinition = ImplantDefinition(1) - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) - obj.Implants(0).Unlocked = true - obj.InstallImplant(testplant) mustEqual Some(0) - obj.Implants.find({p => p.Implant == ImplantType(1)}) match { //find the installed implant - case Some(slot) => - slot.Installed mustEqual Some(testplant) + "can use their free hand to hold things" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val ammo = AmmoBox(GlobalDefinitions.bullet_9mm) + obj.FreeHand.Equipment mustEqual None + + obj.FreeHand.Equipment = ammo + obj.FreeHand.Equipment mustEqual Some(ammo) + } + + "can access the player's locker-space" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Slot(5).Equipment.get.isInstanceOf[LockerContainer] mustEqual true + } + + "can find equipment" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Slot(0).Equipment = { + val item = Tool(beamer) + item.GUID = PlanetSideGUID(1) + item + } + obj.Slot(4).Equipment = { + val item = Tool(forceblade) + item.GUID = PlanetSideGUID(2) + item + } + obj.Slot(6).Equipment = { + val item = ConstructionItem(ace) + item.GUID = PlanetSideGUID(3) + item + } + obj.Locker.Slot(6).Equipment = { + val item = Kit(medkit) + item.GUID = PlanetSideGUID(4) + item + } + obj.FreeHand.Equipment = { + val item = SimpleItem(remote_electronics_kit) + item.GUID = PlanetSideGUID(5) + item + } + + obj.Find(PlanetSideGUID(1)) mustEqual Some(0) //holsters + obj.Find(PlanetSideGUID(2)) mustEqual Some(4) //holsters, melee + obj.Find(PlanetSideGUID(3)) mustEqual Some(6) //inventory + obj.Find(PlanetSideGUID(4)) mustEqual Some(Player.LockerSlot) //locker-space + obj.Find(PlanetSideGUID(5)) mustEqual Some(Player.FreeHandSlot) //free hand + obj.Find(PlanetSideGUID(6)) mustEqual None //not here + } + + "does equipment collision checking (are we already holding something there?)" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val item1 = Tool(beamer) + val item2 = Kit(medkit) + val item3 = AmmoBox(GlobalDefinitions.bullet_9mm) + obj.Slot(0).Equipment = item1 + obj.Slot(6).Equipment = item2 + obj.FreeHand.Equipment = item3 + + obj.Collisions(0, 1, 1) match { + case Success(List(item)) => + item.obj mustEqual item1 + item.start mustEqual 0 case _ => ko - } - ok + } //holsters + + obj.Collisions(1, 1, 1) match { + case Success(List()) => ; + case _ => + ko + } //holsters, nothing + + obj.Collisions(6, 1, 1)match { + case Success(List(item)) => + item.obj mustEqual item2 + item.start mustEqual 6 + case _ => + ko + } //inventory + + obj.Collisions(Player.FreeHandSlot, 1, 1)match { + case Success(List(item)) => + item.obj mustEqual item3 + item.start mustEqual Player.FreeHandSlot + case _ => + ko + } //free hand } - "can not install the same type of implant twice" in { - val testplant1 : ImplantDefinition = ImplantDefinition(1) - val testplant2 : ImplantDefinition = ImplantDefinition(1) - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) - obj.Implants(0).Unlocked = true - obj.Implants(1).Unlocked = true - obj.InstallImplant(testplant1) mustEqual Some(0) - obj.InstallImplant(testplant2) mustEqual Some(1) + "battle experience point values of the avatar" in { + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val player = Player(avatar) + + player.BEP mustEqual avatar.BEP + avatar.BEP = 1002 + player.BEP mustEqual avatar.BEP } - "uninstall implants" in { - val testplant : ImplantDefinition = ImplantDefinition(1) - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) - obj.Implants(0).Unlocked = true - obj.InstallImplant(testplant) mustEqual Some(0) - obj.Implants(0).Installed mustEqual Some(testplant) + "command experience point values of the avatar" in { + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val player = Player(avatar) - obj.UninstallImplant(testplant.Type) - obj.Implants(0).Installed mustEqual None + player.CEP mustEqual avatar.CEP + avatar.CEP = 1002 + player.CEP mustEqual avatar.CEP + } + + "can get a quick summary of implant slots (default)" in { + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val player = Player(avatar) + + player.Implants mustEqual Array.empty + } + + "can get a quick summary of implant slots (two unlocked, one installed)" in { + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val player = Player(avatar) + val temp = new ImplantDefinition(1) + avatar.Implants(0).Unlocked = true + avatar.InstallImplant(new ImplantDefinition(1)) + avatar.Implants(1).Unlocked = true + avatar.InstallImplant(new ImplantDefinition(2)) + avatar.UninstallImplant(temp.Type) + + val list = player.Implants + //slot 0 + val (implant1, init1, active1) = list(0) + implant1 mustEqual ImplantType.None + init1 mustEqual -1 + active1 mustEqual false + //slot 1 + val (implant2, init2, active2) = list(1) + implant2 mustEqual ImplantType(2) + init2 mustEqual 0 + active2 mustEqual false + } + + "can get a quick summary of implant slots (all unlocked, first two installed)" in { + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val player = Player(avatar) + avatar.Implants(0).Unlocked = true + avatar.InstallImplant(new ImplantDefinition(1)) + avatar.Implants(0).Initialized = true + avatar.Implants(0).Active = true + avatar.Implants(1).Unlocked = true + avatar.InstallImplant(new ImplantDefinition(2)) + avatar.Implants(1).Initialized = true + avatar.Implants(1).Active = false + avatar.Implants(2).Unlocked = true + + val list = player.Implants + //slot 0 + val (implant1, init1, active1) = list(0) + implant1 mustEqual ImplantType(1) + init1 mustEqual 0 + active1 mustEqual true + //slot 1 + val (implant2, init2, active2) = list(1) + implant2 mustEqual ImplantType(2) + init2 mustEqual 0 + active2 mustEqual false + //slot 2 + val (implant3, init3, active3) = list(2) + implant3 mustEqual ImplantType.None + init3 mustEqual -1 + active3 mustEqual false } "seat in a vehicle" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.VehicleSeated mustEqual None obj.VehicleSeated = PlanetSideGUID(65) obj.VehicleSeated mustEqual Some(PlanetSideGUID(65)) @@ -224,7 +437,7 @@ class PlayerTest extends Specification { } "own in a vehicle" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.VehicleOwned mustEqual None obj.VehicleOwned = PlanetSideGUID(65) obj.VehicleOwned mustEqual Some(PlanetSideGUID(65)) @@ -233,27 +446,18 @@ class PlayerTest extends Specification { } "remember what zone he is in" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.Continent mustEqual "home2" obj.Continent = "ugd01" obj.Continent mustEqual "ugd01" } - "administrate" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) - obj.Admin mustEqual false - Player.Administrate(obj, true) - obj.Admin mustEqual true - Player.Administrate(obj, false) - obj.Admin mustEqual false - } + "toString" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.toString mustEqual "TR Chord 0/100 0/50" - "spectate" in { - val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) - obj.Spectator mustEqual false - Player.Spectate(obj, true) - obj.Spectator mustEqual true - Player.Spectate(obj, false) - obj.Spectator mustEqual false + obj.GUID = PlanetSideGUID(455) + obj.Continent = "z3" + obj.toString mustEqual "TR Chord z3-455 0/100 0/50" } } diff --git a/common/src/test/scala/objects/ServerObjectBuilderTest.scala b/common/src/test/scala/objects/ServerObjectBuilderTest.scala index 903e2e50d..813dbe36c 100644 --- a/common/src/test/scala/objects/ServerObjectBuilderTest.scala +++ b/common/src/test/scala/objects/ServerObjectBuilderTest.scala @@ -5,7 +5,7 @@ import akka.actor.{Actor, ActorContext, Props} import net.psforever.objects.guid.NumberPoolHub import net.psforever.packet.game.PlanetSideGUID import net.psforever.objects.serverobject.ServerObjectBuilder -import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, WarpGate} +import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, StructureType, WarpGate} import net.psforever.objects.zones.Zone import net.psforever.types.Vector3 @@ -14,7 +14,7 @@ import scala.concurrent.duration.Duration class BuildingBuilderTest extends ActorTest { "Building object" should { "build" in { - val structure : (Int,Zone,ActorContext)=>Building = Building.Structure + val structure : (Int,Zone,ActorContext)=>Building = Building.Structure(StructureType.Building) val actor = system.actorOf(Props(classOf[ServerObjectBuilderTest.BuildingTestActor], structure, 10, Zone.Nowhere), "building") actor ! "!" @@ -169,6 +169,26 @@ class LockerObjectBuilderTest extends ActorTest { } } +class SpawnTubeObjectBuilderTest extends ActorTest { + import net.psforever.objects.serverobject.tube.SpawnTube + "LockerObjectBuilder" should { + "build" in { + val hub = ServerObjectBuilderTest.NumberPoolHub + val actor = system.actorOf(Props(classOf[ServerObjectBuilderTest.BuilderTestActor], ServerObjectBuilder(1, + SpawnTube.Constructor(Vector3(3980.4062f, 4267.3047f, 257.5625f), Vector3(0, 0, 90))), hub), "spawn-tube") + actor ! "!" + + val reply = receiveOne(Duration.create(1000, "ms")) + assert(reply.isInstanceOf[SpawnTube]) + assert(reply.asInstanceOf[SpawnTube].HasGUID) + assert(reply.asInstanceOf[SpawnTube].GUID == PlanetSideGUID(1)) + assert(reply.asInstanceOf[SpawnTube].Position == Vector3(3980.4062f, 4267.3047f, 257.5625f)) + assert(reply.asInstanceOf[SpawnTube].Orientation == Vector3(0, 0, 90)) + assert(reply == hub(1).get) + } + } +} + object ServerObjectBuilderTest { import net.psforever.objects.guid.source.LimitedNumberSource def NumberPoolHub : NumberPoolHub = { diff --git a/common/src/test/scala/objects/SpawnTubeTest.scala b/common/src/test/scala/objects/SpawnTubeTest.scala new file mode 100644 index 000000000..7ef8e86b7 --- /dev/null +++ b/common/src/test/scala/objects/SpawnTubeTest.scala @@ -0,0 +1,59 @@ +// Copyright (c) 2017 PSForever +package objects + +import akka.actor.{ActorRef, Props} +import net.psforever.objects.GlobalDefinitions +import net.psforever.objects.serverobject.tube.{SpawnTube, SpawnTubeControl, SpawnTubeDefinition} +import org.specs2.mutable.Specification + +class SpawnTubeTest extends Specification { + "SpawnTubeDefinition" should { + "define (ams_respawn_tube)" in { + val obj = new SpawnTubeDefinition(49) + obj.ObjectId mustEqual 49 + obj.Name mustEqual "ams_respawn_tube" + } + + "define (respawn_tube)" in { + val obj = new SpawnTubeDefinition(732) + obj.ObjectId mustEqual 732 + obj.Name mustEqual "respawn_tube" + } + + "define (respawn_tube_tower)" in { + val obj = new SpawnTubeDefinition(733) + obj.ObjectId mustEqual 733 + obj.Name mustEqual "respawn_tube_tower" + } + + "define (invalid)" in { + var id : Int = (math.random * Int.MaxValue).toInt + if(id == 49 || id == 733) { + id += 1 + } + else if(id == 732) { + id += 2 + } + + new SpawnTubeDefinition(id) must throwA[IllegalArgumentException] + } + } + + "SpawnTube" should { + "construct" in { + val obj = SpawnTube(GlobalDefinitions.ams_respawn_tube) + obj.Actor mustEqual ActorRef.noSender + obj.Definition mustEqual GlobalDefinitions.ams_respawn_tube + } + } +} + +class SpawnTubeControlTest extends ActorTest() { + "SpawnTubeControl" should { + "construct" in { + val obj = SpawnTube(GlobalDefinitions.ams_respawn_tube) + obj.Actor = system.actorOf(Props(classOf[SpawnTubeControl], obj), "spawn-tube") + assert(obj.Actor != ActorRef.noSender) + } + } +} diff --git a/common/src/test/scala/objects/VehicleSpawnPadTest.scala b/common/src/test/scala/objects/VehicleSpawnPadTest.scala index f595da65f..d4393b5d2 100644 --- a/common/src/test/scala/objects/VehicleSpawnPadTest.scala +++ b/common/src/test/scala/objects/VehicleSpawnPadTest.scala @@ -3,10 +3,10 @@ package objects import akka.actor.{ActorRef, ActorSystem, Props} import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.vehicles.VehicleControl import net.psforever.objects.zones.Zone -import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Vehicle} import net.psforever.packet.game.PlanetSideGUID import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} import org.specs2.mutable.Specification @@ -109,8 +109,8 @@ object VehicleSpawnPadControl { def SetUpAgents(faction : PlanetSideEmpire.Value)(implicit system : ActorSystem) : (Player, VehicleSpawnPad) = { val pad = VehicleSpawnPad(GlobalDefinitions.spawn_pad) pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), "test-pad") - pad.Owner = new Building(0, Zone.Nowhere) + pad.Owner = new Building(0, Zone.Nowhere, StructureType.Building) pad.Owner.Faction = faction - (Player("test", faction, CharacterGender.Male, 0, 0), pad) + (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), pad) } } diff --git a/common/src/test/scala/objects/VehicleTest.scala b/common/src/test/scala/objects/VehicleTest.scala index e7e6f2e39..0124917dc 100644 --- a/common/src/test/scala/objects/VehicleTest.scala +++ b/common/src/test/scala/objects/VehicleTest.scala @@ -2,17 +2,18 @@ package objects import akka.actor.Props -import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects._ import net.psforever.objects.definition.{SeatDefinition, VehicleDefinition} import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vehicles._ import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.{CharacterGender, ExoSuitType, PlanetSideEmpire} +import net.psforever.types.ExoSuitType import org.specs2.mutable._ import scala.concurrent.duration.Duration class VehicleTest extends Specification { + import VehicleTest._ "SeatDefinition" should { val seat = new SeatDefinition @@ -73,7 +74,7 @@ class VehicleTest extends Specification { val seat = new Seat(seat_def) seat.Occupant.isDefined mustEqual false - val player1 = Player("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player1 = Player(avatar1) player1.ExoSuit = ExoSuitType.MAX seat.Occupant = player1 seat.Occupant.isDefined mustEqual true @@ -84,13 +85,13 @@ class VehicleTest extends Specification { val seat = new Seat(seat_def) seat.Occupant.isDefined mustEqual false - val player1 = Player("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player1 = Player(avatar1) player1.ExoSuit = ExoSuitType.MAX seat.Occupant = player1 seat.Occupant.isDefined mustEqual true seat.Occupant.contains(player1) mustEqual true - val player2 = Player("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(avatar1) player2.ExoSuit = ExoSuitType.MAX seat.Occupant = player2 seat.Occupant.isDefined mustEqual true @@ -101,13 +102,13 @@ class VehicleTest extends Specification { val seat = new Seat(seat_def) seat.Occupant.isDefined mustEqual false - val player1 = Player("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player1 = Player(avatar1) player1.ExoSuit = ExoSuitType.MAX seat.Occupant = player1 seat.Occupant.isDefined mustEqual true seat.Occupant.contains(player1) mustEqual true - val player2 = Player("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(avatar2) player2.ExoSuit = ExoSuitType.MAX seat.Occupant = player2 seat.Occupant.isDefined mustEqual true @@ -160,7 +161,7 @@ class VehicleTest extends Specification { val fury_vehicle = Vehicle(GlobalDefinitions.fury) fury_vehicle.Owner.isDefined mustEqual false - val player1 = Player("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player1 = Player(avatar1) player1.GUID = PlanetSideGUID(1) fury_vehicle.Owner = player1 fury_vehicle.Owner.isDefined mustEqual true @@ -171,13 +172,13 @@ class VehicleTest extends Specification { val fury_vehicle = Vehicle(GlobalDefinitions.fury) fury_vehicle.Owner.isDefined mustEqual false - val player1 = Player("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player1 = Player(avatar1) player1.GUID = PlanetSideGUID(1) fury_vehicle.Owner = player1 fury_vehicle.Owner.isDefined mustEqual true fury_vehicle.Owner.contains(PlanetSideGUID(1)) mustEqual true - val player2 = Player("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(avatar2) player2.GUID = PlanetSideGUID(2) fury_vehicle.Owner = player2 fury_vehicle.Owner.isDefined mustEqual true @@ -234,9 +235,9 @@ class VehicleTest extends Specification { "can find a passenger in a seat" in { val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) - val player1 = Player("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player1 = Player(avatar1) player1.GUID = PlanetSideGUID(1) - val player2 = Player("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(avatar2) player2.GUID = PlanetSideGUID(2) harasser_vehicle.Seat(0).get.Occupant = player1 //don't worry about ownership for now harasser_vehicle.Seat(1).get.Occupant = player2 @@ -282,9 +283,9 @@ class VehicleTest extends Specification { class VehicleControl1Test extends ActorTest { "Vehicle Control" should { "deactivate and stop handling mount messages" in { - val player1 = Player("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player1 = Player(VehicleTest.avatar1) player1.GUID = PlanetSideGUID(1) - val player2 = Player("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(VehicleTest.avatar2) val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) vehicle.GUID = PlanetSideGUID(3) vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") @@ -303,9 +304,9 @@ class VehicleControl1Test extends ActorTest { class VehicleControl2Test extends ActorTest { "Vehicle Control" should { "reactivate and resume handling mount messages" in { - val player1 = Player("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player1 = Player(VehicleTest.avatar1) player1.GUID = PlanetSideGUID(1) - val player2 = Player("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(VehicleTest.avatar2) player2.GUID = PlanetSideGUID(2) val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) vehicle.GUID = PlanetSideGUID(3) @@ -324,3 +325,10 @@ class VehicleControl2Test extends ActorTest { } } } + +object VehicleTest { + import net.psforever.objects.Avatar + import net.psforever.types.{CharacterGender, PlanetSideEmpire} + val avatar1 = Avatar("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val avatar2 = Avatar("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) +} diff --git a/common/src/test/scala/objects/ZoneTest.scala b/common/src/test/scala/objects/ZoneTest.scala index 024c8aef6..2c5cfcb69 100644 --- a/common/src/test/scala/objects/ZoneTest.scala +++ b/common/src/test/scala/objects/ZoneTest.scala @@ -1,16 +1,24 @@ // Copyright (c) 2017 PSForever package objects -import akka.actor.{ActorContext, ActorRef} +import java.util.concurrent.atomic.AtomicInteger + +import akka.actor.{Actor, ActorContext, ActorRef, Props} import net.psforever.objects.entity.IdentifiableEntity import net.psforever.objects.equipment.Equipment import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.LimitedNumberSource -import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder} -import net.psforever.objects.zones.{Zone, ZoneMap} -import net.psforever.objects.{GlobalDefinitions, Vehicle} +import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, StructureType} +import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.objects.serverobject.tube.SpawnTube +import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap} +import net.psforever.objects._ +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} import org.specs2.mutable.Specification +import scala.concurrent.duration.Duration + class ZoneTest extends Specification { def test(a: Int, b : Zone, c : ActorContext) : Building = { Building.NoBuilding } @@ -82,6 +90,8 @@ class ZoneTest extends Specification { //zone also has a unique identifier system but it can't be accessed without its the Actor GUID being initialized zone.EquipmentOnGround mustEqual List.empty[Equipment] zone.Vehicles mustEqual List.empty[Vehicle] + zone.Players mustEqual List.empty[Player] + zone.Corpses mustEqual List.empty[Player] } "can have its unique identifier system changed if no objects were added to it" in { @@ -124,3 +134,422 @@ class ZoneTest extends Specification { } } } + +class ZoneActorTest extends ActorTest { + "Zone" should { + "have an Actor" in { + val zone = new Zone("test", new ZoneMap("map6"), 1) + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "test-actor") + expectNoMsg(Duration.create(100, "ms")) + assert(zone.Actor != ActorRef.noSender) + } + + "set up spawn groups based on buildings" in { + val map6 = new ZoneMap("map6") { + LocalBuilding(1, FoundationBuilder(Building.Structure(StructureType.Building, Vector3(1,1,1)))) + LocalObject(1, SpawnTube.Constructor(Vector3.Zero, Vector3.Zero)) + LocalObject(2, Terminal.Constructor(GlobalDefinitions.dropship_vehicle_terminal)) + LocalObject(3, SpawnTube.Constructor(Vector3.Zero, Vector3.Zero)) + ObjectToBuilding(1, 1) + ObjectToBuilding(2, 1) + ObjectToBuilding(3, 1) + + LocalBuilding(2, FoundationBuilder(Building.Structure(StructureType.Building))) + LocalObject(7, SpawnTube.Constructor(Vector3.Zero, Vector3.Zero)) + ObjectToBuilding(7, 2) + + LocalBuilding(3, FoundationBuilder(Building.Structure(StructureType.Building, Vector3(1,1,1)))) + LocalObject(4, Terminal.Constructor(GlobalDefinitions.dropship_vehicle_terminal)) + LocalObject(5, SpawnTube.Constructor(Vector3.Zero, Vector3.Zero)) + LocalObject(6, Terminal.Constructor(GlobalDefinitions.dropship_vehicle_terminal)) + ObjectToBuilding(4, 3) + ObjectToBuilding(5, 3) + ObjectToBuilding(6, 3) + } + val zone = new Zone("test", map6, 1) + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "test-init") + zone.Actor ! Zone.Init() + expectNoMsg(Duration.create(300, "ms")) + + val groups = zone.SpawnGroups() + assert(groups.size == 2) + zone.SpawnGroups().foreach({ case(building, tubes) => + if(building.Id == 1) { + val building1 = zone.SpawnGroups(building) + assert(tubes.length == 2) + assert(tubes.head == building1.head) + assert(tubes.head.GUID == PlanetSideGUID(1)) + assert(tubes(1) == building1(1)) + assert(tubes(1).GUID == PlanetSideGUID(3)) + } + else if(building.Id == 3) { + val building2 = zone.SpawnGroups(building) + assert(tubes.length == 1) + assert(tubes.head == building2.head) + assert(tubes.head.GUID == PlanetSideGUID(5)) + } + else { + assert(false) + } + }) + } + + "select spawn points based on the position of the player in reference to buildings" in { + val map6 = new ZoneMap("map6") { + LocalBuilding(1, FoundationBuilder(Building.Structure(StructureType.Building, Vector3(1,1,1)))) + LocalObject(1, SpawnTube.Constructor(Vector3.Zero, Vector3.Zero)) + ObjectToBuilding(1, 1) + + LocalBuilding(3, FoundationBuilder(Building.Structure(StructureType.Building, Vector3(4,4,4)))) + LocalObject(5, SpawnTube.Constructor(Vector3.Zero, Vector3.Zero)) + ObjectToBuilding(5, 3) + } + val zone = new Zone("test", map6, 1) + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "test-spawn") + zone.Actor ! Zone.Init() + expectNoMsg(Duration.create(300, "ms")) + val player = Player(Avatar("Chord", PlanetSideEmpire.NEUTRAL, CharacterGender.Male, 0, 5)) + + val bldg1 = zone.Building(1).get + val bldg3 = zone.Building(3).get + player.Position = Vector3(1,1,1) //closer to bldg1 + zone.Actor ! Zone.Lattice.RequestSpawnPoint(1, player, 7) + val reply1 = receiveOne(Duration.create(200, "ms")) + assert(reply1.isInstanceOf[Zone.Lattice.SpawnPoint]) + assert(reply1.asInstanceOf[Zone.Lattice.SpawnPoint].zone_id == "test") + assert(reply1.asInstanceOf[Zone.Lattice.SpawnPoint].building == bldg1) + assert(reply1.asInstanceOf[Zone.Lattice.SpawnPoint].spawn_tube.Owner == bldg1) + + player.Position = Vector3(3,3,3) //closer to bldg3 + zone.Actor ! Zone.Lattice.RequestSpawnPoint(1, player, 7) + val reply3 = receiveOne(Duration.create(200, "ms")) + assert(reply3.isInstanceOf[Zone.Lattice.SpawnPoint]) + assert(reply3.asInstanceOf[Zone.Lattice.SpawnPoint].zone_id == "test") + assert(reply3.asInstanceOf[Zone.Lattice.SpawnPoint].building == bldg3) + assert(reply3.asInstanceOf[Zone.Lattice.SpawnPoint].spawn_tube.Owner == bldg3) + } + + "will report if no spawn points have been found in a zone" in { + val map6 = new ZoneMap("map6") { + LocalBuilding(1, FoundationBuilder(Building.Structure(StructureType.Building, Vector3(1,1,1)))) + + LocalBuilding(3, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(4,4,4)))) + LocalObject(5, SpawnTube.Constructor(Vector3.Zero, Vector3.Zero)) + ObjectToBuilding(5, 3) + } + val zone = new Zone("test", map6, 1) + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "test-no-spawn") + zone.Actor ! Zone.Init() + expectNoMsg(Duration.create(300, "ms")) + val player = Player(Avatar("Chord", PlanetSideEmpire.NEUTRAL, CharacterGender.Male, 0, 5)) + + zone.Actor ! Zone.Lattice.RequestSpawnPoint(1, player, 7) + val reply = receiveOne(Duration.create(200, "ms")) + assert(reply.isInstanceOf[Zone.Lattice.NoValidSpawnPoint]) + assert(reply.asInstanceOf[Zone.Lattice.NoValidSpawnPoint].zone_number == 1) + assert(reply.asInstanceOf[Zone.Lattice.NoValidSpawnPoint].spawn_group.contains(7)) + } + } +} + +class ZonePopulationTest extends ActorTest { + val testNum = new AtomicInteger(1) + def TestName : String = s"test${testNum.getAndIncrement()}" + + "ZonePopulationActor" should { + "add new user to zones" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" + receiveOne(Duration.create(200, "ms")) //consume + + assert(zone.Players.isEmpty) + assert(zone.LivePlayers.isEmpty) + zone.Population ! Zone.Population.Join(avatar) + expectNoMsg(Duration.create(100, "ms")) + assert(zone.Players.size == 1) + assert(zone.Players.head == avatar) + assert(zone.LivePlayers.isEmpty) + } + + "remove user from zones" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" + receiveOne(Duration.create(200, "ms")) //consume + zone.Population ! Zone.Population.Join(avatar) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.Players.size == 1) + assert(zone.Players.head == avatar) + zone.Population ! Zone.Population.Leave(avatar) + expectNoMsg(Duration.create(100, "ms")) + assert(zone.Players.isEmpty) + } + + "associate user with a character" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val player = Player(avatar) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" + receiveOne(Duration.create(200, "ms")) //consume + zone.Population ! Zone.Population.Join(avatar) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.Players.size == 1) + assert(zone.Players.head == avatar) + assert(zone.LivePlayers.isEmpty) + zone.Population ! Zone.Population.Spawn(avatar, player) + expectNoMsg(Duration.create(100, "ms")) + assert(zone.Players.size == 1) + assert(zone.Players.head == avatar) + assert(zone.LivePlayers.size == 1) + assert(zone.LivePlayers.head == player) + } + + "disassociate character from a user" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val player = Player(avatar) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" + receiveOne(Duration.create(200, "ms")) //consume + zone.Population ! Zone.Population.Join(avatar) + expectNoMsg(Duration.create(100, "ms")) + zone.Population ! Zone.Population.Spawn(avatar, player) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.Players.size == 1) + assert(zone.Players.head == avatar) + assert(zone.LivePlayers.size == 1) + assert(zone.LivePlayers.head == player) + zone.Population ! Zone.Population.Release(avatar) + expectNoMsg(Duration.create(100, "ms")) + assert(zone.Players.size == 1) + assert(zone.Players.head == avatar) + assert(zone.LivePlayers.isEmpty) + } + + "user tries to Leave, but still has an associated character" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val player = Player(avatar) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" + receiveOne(Duration.create(500, "ms")) //consume + zone.Population ! Zone.Population.Join(avatar) + expectNoMsg(Duration.create(100, "ms")) + zone.Population ! Zone.Population.Spawn(avatar, player) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.Players.size == 1) + assert(zone.Players.head == avatar) + assert(zone.LivePlayers.size == 1) + assert(zone.LivePlayers.head == player) + zone.Population ! Zone.Population.Leave(avatar) + val reply = receiveOne(Duration.create(100, "ms")) + assert(zone.Players.isEmpty) + assert(zone.LivePlayers.isEmpty) + assert(reply.isInstanceOf[Zone.Population.PlayerHasLeft]) + assert(reply.asInstanceOf[Zone.Population.PlayerHasLeft].zone == zone) + assert(reply.asInstanceOf[Zone.Population.PlayerHasLeft].player.contains(player)) + } + + "user tries to Spawn a character, but an associated character already exists" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val player1 = Player(avatar) + val player2 = Player(avatar) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" + receiveOne(Duration.create(200, "ms")) //consume + zone.Population ! Zone.Population.Join(avatar) + expectNoMsg(Duration.create(100, "ms")) + zone.Population ! Zone.Population.Spawn(avatar, player1) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.Players.size == 1) + assert(zone.Players.head == avatar) + assert(zone.LivePlayers.size == 1) + assert(zone.LivePlayers.head == player1) + zone.Population ! Zone.Population.Spawn(avatar, player2) + val reply = receiveOne(Duration.create(100, "ms")) + assert(zone.Players.size == 1) + assert(zone.Players.head == avatar) + assert(zone.LivePlayers.size == 1) + assert(zone.LivePlayers.head == player1) + assert(reply.isInstanceOf[Zone.Population.PlayerAlreadySpawned]) + assert(reply.asInstanceOf[Zone.Population.PlayerAlreadySpawned].player == player1) + } + + "user tries to Spawn a character, but did not Join first" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val player = Player(avatar) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" + receiveOne(Duration.create(200, "ms")) //consume + + assert(zone.Players.isEmpty) + assert(zone.LivePlayers.isEmpty) + zone.Population ! Zone.Population.Spawn(avatar, player) + val reply = receiveOne(Duration.create(100, "ms")) + assert(zone.Players.isEmpty) + assert(zone.LivePlayers.isEmpty) + assert(reply.isInstanceOf[Zone.Population.PlayerCanNotSpawn]) + assert(reply.asInstanceOf[Zone.Population.PlayerCanNotSpawn].zone == zone) + assert(reply.asInstanceOf[Zone.Population.PlayerCanNotSpawn].player == player) + } + + "user tries to Release a character, but did not Spawn a character first" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" + receiveOne(Duration.create(200, "ms")) //consume + zone.Population ! Zone.Population.Join(avatar) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.Players.size == 1) + assert(zone.Players.head == avatar) + assert(zone.LivePlayers.isEmpty) + zone.Population ! Zone.Population.Release(avatar) + val reply = receiveOne(Duration.create(100, "ms")) + assert(zone.Players.size == 1) + assert(zone.Players.head == avatar) + assert(zone.LivePlayers.isEmpty) + assert(reply.isInstanceOf[Zone.Population.PlayerHasLeft]) + assert(reply.asInstanceOf[Zone.Population.PlayerHasLeft].zone == zone) + assert(reply.asInstanceOf[Zone.Population.PlayerHasLeft].player.isEmpty) + } + + "user adds character to list of retired characters" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + player.Release + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" + receiveOne(Duration.create(200, "ms")) //consume + + assert(zone.Corpses.isEmpty) + zone.Population ! Zone.Corpse.Add(player) + expectNoMsg(Duration.create(100, "ms")) + assert(zone.Corpses.size == 1) + assert(zone.Corpses.head == player) + } + + "user removes character from the list of retired characters" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + player.Release + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" + receiveOne(Duration.create(200, "ms")) //consume + zone.Population ! Zone.Corpse.Add(player) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.Corpses.size == 1) + assert(zone.Corpses.head == player) + zone.Population ! Zone.Corpse.Remove(player) + expectNoMsg(Duration.create(100, "ms")) + assert(zone.Corpses.isEmpty) + } + + "user removes THE CORRECT character from the list of retired characters" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val player1 = Player(Avatar("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + player1.Release + val player2 = Player(Avatar("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + player2.Release + val player3 = Player(Avatar("Chord3", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + player3.Release + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" + receiveOne(Duration.create(200, "ms")) //consume + zone.Population ! Zone.Corpse.Add(player1) + zone.Population ! Zone.Corpse.Add(player2) + zone.Population ! Zone.Corpse.Add(player3) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.Corpses.size == 3) + assert(zone.Corpses.head == player1) + assert(zone.Corpses(1) == player2) + assert(zone.Corpses(2) == player3) + zone.Population ! Zone.Corpse.Remove(player2) + expectNoMsg(Duration.create(100, "ms")) + assert(zone.Corpses.size == 2) + assert(zone.Corpses.head == player1) + assert(zone.Corpses(1) == player3) + } + + "user tries to add character to list of retired characters, but is not in correct state" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + //player.Release !!important + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "testC") ! "!" + receiveOne(Duration.create(500, "ms")) //consume + + assert(zone.Corpses.isEmpty) + zone.Population ! Zone.Corpse.Add(player) + expectNoMsg(Duration.create(100, "ms")) + assert(zone.Corpses.isEmpty) + } + } +} + +class ZoneGroundTest extends ActorTest { + val item = AmmoBox(GlobalDefinitions.bullet_9mm) + item.GUID = PlanetSideGUID(10) + + "ZoneGroundActor" should { + "drop item on ground" in { + val zone = new Zone("test", new ZoneMap(""), 0) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-item-test") ! "!" + receiveOne(Duration.create(200, "ms")) //consume + + assert(zone.EquipmentOnGround.isEmpty) + assert(item.Position == Vector3.Zero) + assert(item.Orientation == Vector3.Zero) + zone.Ground ! Zone.DropItemOnGround(item, Vector3(1.1f, 2.2f, 3.3f), Vector3(4.4f, 5.5f, 6.6f)) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.EquipmentOnGround == List(item)) + assert(item.Position == Vector3(1.1f, 2.2f, 3.3f)) + assert(item.Orientation == Vector3(4.4f, 5.5f, 6.6f)) + } + + "get item from ground (success)" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "get-item-test-good") ! "!" + receiveOne(Duration.create(200, "ms")) //consume + zone.Ground ! Zone.DropItemOnGround(item, Vector3.Zero, Vector3.Zero) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.EquipmentOnGround == List(item)) + zone.Ground ! Zone.GetItemOnGround(player, PlanetSideGUID(10)) + val reply = receiveOne(Duration.create(100, "ms")) + + assert(zone.EquipmentOnGround.isEmpty) + assert(reply.isInstanceOf[Zone.ItemFromGround]) + assert(reply.asInstanceOf[Zone.ItemFromGround].player == player) + assert(reply.asInstanceOf[Zone.ItemFromGround].item == item) + } + + "get item from ground (failure)" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "get-item-test-fail") ! "!" + receiveOne(Duration.create(200, "ms")) //consume + zone.Ground ! Zone.DropItemOnGround(item, Vector3.Zero, Vector3.Zero) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.EquipmentOnGround == List(item)) + zone.Ground ! Zone.GetItemOnGround(player, PlanetSideGUID(11)) //wrong guid + expectNoMsg(Duration.create(500, "ms")) + } + } +} + +object ZoneTest { + class ZoneInitActor(zone : Zone) extends Actor { + def receive : Receive = { + case "!" => + zone.Init(context) + sender ! "!" + case _ => ; + } + } +} diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegister5Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegister5Test.scala index a96b55dd2..0a19c67df 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskRegister5Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegister5Test.scala @@ -1,7 +1,6 @@ // Copyright (c) 2017 PSForever package objects.guidtask - import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} import net.psforever.types.{CharacterGender, PlanetSideEmpire} @@ -10,7 +9,7 @@ import objects.ActorTest class GUIDTaskRegister5Test extends ActorTest() { "RegisterAvatar" in { val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup - val obj = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val obj_wep = Tool(GlobalDefinitions.beamer) obj.Slot(0).Equipment = obj_wep val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegister6Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegister6Test.scala new file mode 100644 index 000000000..091ea62fc --- /dev/null +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegister6Test.scala @@ -0,0 +1,38 @@ +// Copyright (c) 2017 PSForever +package objects.guidtask + +import net.psforever.objects._ +import net.psforever.objects.guid.{GUIDTask, TaskResolver} +import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import objects.ActorTest + +class GUIDTaskRegister6Test extends ActorTest() { + "RegisterPlayer" in { + val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup + val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val obj_wep = Tool(GlobalDefinitions.beamer) + obj.Slot(0).Equipment = obj_wep + val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) + obj_wep.AmmoSlots.head.Box = obj_wep_ammo + val obj_inv_ammo = AmmoBox(GlobalDefinitions.energy_cell) + obj.Slot(6).Equipment = obj_inv_ammo + val obj_locker = obj.Slot(5).Equipment.get + val obj_locker_ammo = AmmoBox(GlobalDefinitions.energy_cell) + obj_locker.asInstanceOf[LockerContainer].Inventory += 0 -> obj_locker_ammo + + assert(!obj.HasGUID) + assert(!obj_wep.HasGUID) + assert(!obj_wep_ammo.HasGUID) + assert(!obj_inv_ammo.HasGUID) + assert(!obj_locker.HasGUID) + assert(!obj_locker_ammo.HasGUID) + taskResolver ! TaskResolver.GiveTask(new GUIDTaskTest.RegisterTestTask(probe.ref), List(GUIDTask.RegisterPlayer(obj)(uns))) + probe.expectMsg(scala.util.Success) + assert(obj.HasGUID) + assert(obj_wep.HasGUID) + assert(obj_wep_ammo.HasGUID) + assert(obj_inv_ammo.HasGUID) + assert(!obj_locker.HasGUID) + assert(!obj_locker_ammo.HasGUID) + } +} diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister5Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregister5Test.scala index b4d28ab47..15cef74e2 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister5Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregister5Test.scala @@ -9,7 +9,7 @@ import objects.ActorTest class GUIDTaskUnregister5Test extends ActorTest() { "UnregisterAvatar" in { val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup - val obj = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val obj_wep = Tool(GlobalDefinitions.beamer) obj.Slot(0).Equipment = obj_wep val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister6Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregister6Test.scala new file mode 100644 index 000000000..718f460f0 --- /dev/null +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregister6Test.scala @@ -0,0 +1,44 @@ +// Copyright (c) 2017 PSForever +package objects.guidtask + +import net.psforever.objects._ +import net.psforever.objects.guid.{GUIDTask, TaskResolver} +import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import objects.ActorTest + +class GUIDTaskUnregister6Test extends ActorTest() { + "UnregisterPlayer" in { + val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup + val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val obj_wep = Tool(GlobalDefinitions.beamer) + obj.Slot(0).Equipment = obj_wep + val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) + obj_wep.AmmoSlots.head.Box = obj_wep_ammo + val obj_inv_ammo = AmmoBox(GlobalDefinitions.energy_cell) + obj.Slot(6).Equipment = obj_inv_ammo + val obj_locker = obj.Slot(5).Equipment.get + val obj_locker_ammo = AmmoBox(GlobalDefinitions.energy_cell) + obj_locker.asInstanceOf[LockerContainer].Inventory += 0 -> obj_locker_ammo + guid.register(obj, "dynamic") + guid.register(obj_wep, "dynamic") + guid.register(obj_wep_ammo, "dynamic") + guid.register(obj_inv_ammo, "dynamic") + guid.register(obj_locker, "dynamic") + guid.register(obj_locker_ammo, "dynamic") + + assert(obj.HasGUID) + assert(obj_wep.HasGUID) + assert(obj_wep_ammo.HasGUID) + assert(obj_inv_ammo.HasGUID) + assert(obj_locker.HasGUID) + assert(obj_locker_ammo.HasGUID) + taskResolver ! TaskResolver.GiveTask(new GUIDTaskTest.RegisterTestTask(probe.ref), List(GUIDTask.UnregisterPlayer(obj)(uns))) + probe.expectMsg(scala.util.Success) + assert(!obj.HasGUID) + assert(!obj_wep.HasGUID) + assert(!obj_wep_ammo.HasGUID) + assert(!obj_inv_ammo.HasGUID) + assert(obj_locker.HasGUID) + assert(obj_locker_ammo.HasGUID) + } +} diff --git a/common/src/test/scala/objects/number/UniqueNumberSystemTest.scala b/common/src/test/scala/objects/number/UniqueNumberSystemTest.scala index be8d3e7fa..954a5837e 100644 --- a/common/src/test/scala/objects/number/UniqueNumberSystemTest.scala +++ b/common/src/test/scala/objects/number/UniqueNumberSystemTest.scala @@ -275,6 +275,40 @@ class UniqueNumberSystemTest8 extends ActorTest() { } } +class UniqueNumberSystemTest9 extends ActorTest() { + class EntityTestClass extends IdentifiableEntity + + "UniqueNumberSystem" should { + "Failures (manually walking the failure cases)" 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 excp = new Exception("EXCEPTION MESSAGE") + expectNoMsg(Duration.create(200, "ms")) + + //GiveNumber + uns ! NumberPoolActor.GiveNumber(1001, Some("test")) //no task associated with id="test" + uns ! NumberPoolActor.GiveNumber(1000, Some("test")) //no task associated with id="test" and number is not pooled + uns ! NumberPoolActor.GiveNumber(1000, Some(1)) //the task could theoretically exist, but does not + //NoNumber + uns ! NumberPoolActor.NoNumber(excp, Some(1)) + uns ! NumberPoolActor.NoNumber(excp, None) + uns ! NumberPoolActor.NoNumber(excp, Some("test")) + //ReturnNumberResult A + uns ! NumberPoolActor.ReturnNumberResult(1001, None, Some("test")) + uns ! NumberPoolActor.ReturnNumberResult(1000, None, Some("test")) + uns ! NumberPoolActor.ReturnNumberResult(1001, None, Some(1)) + uns ! NumberPoolActor.ReturnNumberResult(1000, None, Some(1)) + //ReturnNumberResult B + uns ! NumberPoolActor.ReturnNumberResult(1001, Some(excp), Some("test")) + uns ! NumberPoolActor.ReturnNumberResult(1001, Some(excp), Some(1)) + } + } +} + object UniqueNumberSystemTest { /** * @see `UniqueNumberSystem.AllocateNumberPoolActors(NumberPoolHub)(implicit ActorContext)` diff --git a/common/src/test/scala/objects/terminal/AirVehicleTerminalTest.scala b/common/src/test/scala/objects/terminal/AirVehicleTerminalTest.scala index 65436e495..b635ad5de 100644 --- a/common/src/test/scala/objects/terminal/AirVehicleTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/AirVehicleTerminalTest.scala @@ -2,8 +2,8 @@ package objects.terminal import akka.actor.ActorRef -import net.psforever.objects.serverobject.structures.Building -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} @@ -12,9 +12,9 @@ import org.specs2.mutable.Specification class AirVehicleTerminalTest extends Specification { "Air_Vehicle_Terminal" should { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val terminal = Terminal(GlobalDefinitions.air_vehicle_terminal) - terminal.Owner = new Building(0, Zone.Nowhere) + terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR "construct" in { diff --git a/common/src/test/scala/objects/terminal/CertTerminalTest.scala b/common/src/test/scala/objects/terminal/CertTerminalTest.scala index c3d000d0e..c9791c9ec 100644 --- a/common/src/test/scala/objects/terminal/CertTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/CertTerminalTest.scala @@ -2,19 +2,19 @@ package objects.terminal import akka.actor.ActorRef -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} import net.psforever.types._ import org.specs2.mutable.Specification class CertTerminalTest extends Specification { "Cert_Terminal" should { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val terminal = Terminal(GlobalDefinitions.cert_terminal) - terminal.Owner = new Building(0, Zone.Nowhere) + terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR "construct" in { diff --git a/common/src/test/scala/objects/terminal/DropshipVehicleTerminalTest.scala b/common/src/test/scala/objects/terminal/DropshipVehicleTerminalTest.scala index d498e3e0b..ef6c621b8 100644 --- a/common/src/test/scala/objects/terminal/DropshipVehicleTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/DropshipVehicleTerminalTest.scala @@ -2,8 +2,8 @@ package objects.terminal import akka.actor.ActorRef -import net.psforever.objects.serverobject.structures.Building -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} @@ -12,9 +12,9 @@ import org.specs2.mutable.Specification class DropshipVehicleTerminalTest extends Specification { "Dropship_Vehicle_Terminal" should { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val terminal = Terminal(GlobalDefinitions.dropship_vehicle_terminal) - terminal.Owner = new Building(0, Zone.Nowhere) + terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR "construct" in { diff --git a/common/src/test/scala/objects/terminal/GroundVehicleTerminalTest.scala b/common/src/test/scala/objects/terminal/GroundVehicleTerminalTest.scala index 831bb0b17..ee88b6027 100644 --- a/common/src/test/scala/objects/terminal/GroundVehicleTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/GroundVehicleTerminalTest.scala @@ -2,8 +2,8 @@ package objects.terminal import akka.actor.ActorRef -import net.psforever.objects.serverobject.structures.Building -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} @@ -12,9 +12,9 @@ import org.specs2.mutable.Specification class GroundVehicleTerminalTest extends Specification { "Ground_Vehicle_Terminal" should { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val terminal = Terminal(GlobalDefinitions.ground_vehicle_terminal) - terminal.Owner = new Building(0, Zone.Nowhere) + terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR "construct" in { diff --git a/common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala b/common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala index d846ab944..272815f78 100644 --- a/common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala +++ b/common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala @@ -2,8 +2,8 @@ package objects.terminal import akka.actor.ActorRef -import net.psforever.objects.serverobject.structures.Building -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} @@ -12,9 +12,9 @@ import org.specs2.mutable.Specification class ImplantTerminalInterfaceTest extends Specification { "Implant_Terminal_Interface" should { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val terminal = Terminal(GlobalDefinitions.implant_terminal_interface) - terminal.Owner = new Building(0, Zone.Nowhere) + terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR "construct" in { diff --git a/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala b/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala index a58f0cf0c..91993769f 100644 --- a/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala +++ b/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala @@ -5,8 +5,9 @@ import akka.actor.{ActorRef, ActorSystem, Props} import net.psforever.objects.definition.SeatDefinition import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.implantmech.{ImplantTerminalMech, ImplantTerminalMechControl} +import net.psforever.objects.serverobject.structures.StructureType import net.psforever.objects.vehicles.Seat -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} import objects.ActorTest import org.specs2.mutable.Specification @@ -44,7 +45,7 @@ class ImplantTerminalMechTest extends Specification { } "get passenger in a seat" in { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) obj.PassengerInSeat(player) mustEqual None obj.Seats(0).Occupant = player @@ -89,7 +90,7 @@ class ImplantTerminalMechControl3Test extends ActorTest() { "ImplantTerminalMechControl" should { "block a player from mounting" in { val (player1, mech) = ImplantTerminalMechTest.SetUpAgents(PlanetSideEmpire.TR) - val player2 = Player("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(Avatar("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) mech.Actor ! Mountable.TryMount(player1, 0) receiveOne(Duration.create(100, "ms")) //consume reply @@ -160,9 +161,9 @@ object ImplantTerminalMechTest { val terminal = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) terminal.Actor = system.actorOf(Props(classOf[ImplantTerminalMechControl], terminal), "mech") - terminal.Owner = new Building(0, Zone.Nowhere) + terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = faction terminal.GUID = PlanetSideGUID(1) - (Player("test", faction, CharacterGender.Male, 0, 0), terminal) + (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), terminal) } } diff --git a/common/src/test/scala/objects/terminal/MatrixTerminalTest.scala b/common/src/test/scala/objects/terminal/MatrixTerminalTest.scala index a574c79e1..fbbb4d6e6 100644 --- a/common/src/test/scala/objects/terminal/MatrixTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/MatrixTerminalTest.scala @@ -3,7 +3,7 @@ package objects.terminal import akka.actor.ActorRef import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, Terminal} -import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Vehicle} import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} import net.psforever.types._ import org.specs2.mutable.Specification @@ -22,12 +22,18 @@ class MatrixTerminalTest extends Specification { b.Name mustEqual "matrix_terminalb" } - "define (b)" in { + "define (c)" in { val b = new MatrixTerminalDefinition(519) b.ObjectId mustEqual 519 b.Name mustEqual "matrix_terminalc" } + "define (d)" in { + val b = new MatrixTerminalDefinition(812) + b.ObjectId mustEqual 812 + b.Name mustEqual "spawn_terminal" + } + "define (invalid)" in { var id : Int = (math.random * Int.MaxValue).toInt if(id == 517) { @@ -36,7 +42,7 @@ class MatrixTerminalTest extends Specification { else if(id == 518) { id += 2 } - else if(id == 519) { + else if(id == 519 | id == 812) { id += 1 } @@ -54,7 +60,7 @@ class MatrixTerminalTest extends Specification { } "player can not buy (anything)" in { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 1, "lite_armor", 0, PlanetSideGUID(0)) terminal.Request(player, msg) mustEqual Terminal.NoDeal() diff --git a/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala b/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala index e517624ed..c4d44e14e 100644 --- a/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala +++ b/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala @@ -2,10 +2,10 @@ package objects.terminal import akka.actor.ActorRef -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.terminals.{OrderTerminalABDefinition, Terminal} import net.psforever.objects.zones.Zone -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} import net.psforever.types._ import org.specs2.mutable.Specification @@ -39,7 +39,7 @@ class OrderTerminalABTest extends Specification { "Order_Terminal" should { val terminal = Terminal(GlobalDefinitions.order_terminala) - terminal.Owner = new Building(0, Zone.Nowhere) + terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR "construct" in { @@ -47,14 +47,14 @@ class OrderTerminalABTest extends Specification { } "player can buy different armor ('lite_armor')" in { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 1, "lite_armor", 0, PlanetSideGUID(0)) terminal.Request(player, msg) mustEqual Terminal.BuyExosuit(ExoSuitType.Agile) } "player can buy max armor ('trhev_antiaircraft')" in { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 1, "trhev_antiaircraft", 0, PlanetSideGUID(0)) terminal.Request(player, msg) mustEqual Terminal.NoDeal() @@ -62,10 +62,11 @@ class OrderTerminalABTest extends Specification { //TODO loudout tests "player can not load max loadout" in { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) - player.SaveLoadout("test1", 0) + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(avatar) + avatar.SaveLoadout(player, "test1", 0) player.ExoSuit = ExoSuitType.MAX - player.SaveLoadout("test2", 1) + avatar.SaveLoadout(player, "test2", 1) val msg1 = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.InfantryLoadout, 4, "", 0, PlanetSideGUID(0)) terminal.Request(player, msg1) mustEqual Terminal.InfantryLoadout(ExoSuitType.Standard, 0, Nil, Nil) diff --git a/common/src/test/scala/objects/terminal/OrderTerminalTest.scala b/common/src/test/scala/objects/terminal/OrderTerminalTest.scala index 1998d7d75..024c8794e 100644 --- a/common/src/test/scala/objects/terminal/OrderTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/OrderTerminalTest.scala @@ -2,19 +2,19 @@ package objects.terminal import akka.actor.ActorRef -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone -import net.psforever.objects.{AmmoBox, GlobalDefinitions, Player, Tool} +import net.psforever.objects.{AmmoBox, Avatar, GlobalDefinitions, Player, Tool} import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} import net.psforever.types._ import org.specs2.mutable.Specification class OrderTerminalTest extends Specification { "Order_Terminal" should { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val terminal = Terminal(GlobalDefinitions.order_terminal) - terminal.Owner = new Building(0, Zone.Nowhere) + terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR "construct" in { diff --git a/common/src/test/scala/objects/terminal/TerminalControlTest.scala b/common/src/test/scala/objects/terminal/TerminalControlTest.scala index f3a461f90..428d8d87e 100644 --- a/common/src/test/scala/objects/terminal/TerminalControlTest.scala +++ b/common/src/test/scala/objects/terminal/TerminalControlTest.scala @@ -2,10 +2,10 @@ package objects.terminal import akka.actor.{ActorSystem, Props} -import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.terminals.{Terminal, TerminalControl, TerminalDefinition} import net.psforever.objects.zones.Zone -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} import net.psforever.types._ import objects.ActorTest @@ -122,8 +122,8 @@ object TerminalControlTest { def SetUpAgents(tdef : TerminalDefinition, faction : PlanetSideEmpire.Value)(implicit system : ActorSystem) : (Player, Terminal) = { val terminal = Terminal(tdef) terminal.Actor = system.actorOf(Props(classOf[TerminalControl], terminal), "test-term") - terminal.Owner = new Building(0, Zone.Nowhere) + terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = faction - (Player("test", faction, CharacterGender.Male, 0, 0), terminal) + (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), terminal) } } diff --git a/common/src/test/scala/objects/terminal/VehicleTerminalCombinedTest.scala b/common/src/test/scala/objects/terminal/VehicleTerminalCombinedTest.scala index 6a390432b..0233dc9d6 100644 --- a/common/src/test/scala/objects/terminal/VehicleTerminalCombinedTest.scala +++ b/common/src/test/scala/objects/terminal/VehicleTerminalCombinedTest.scala @@ -2,8 +2,8 @@ package objects.terminal import akka.actor.ActorRef -import net.psforever.objects.serverobject.structures.Building -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} @@ -12,9 +12,9 @@ import org.specs2.mutable.Specification class VehicleTerminalCombinedTest extends Specification { "Ground_Vehicle_Terminal" should { - val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) val terminal = Terminal(GlobalDefinitions.vehicle_terminal_combined) - terminal.Owner = new Building(0, Zone.Nowhere) + terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR "construct" in { diff --git a/pslogin/src/main/scala/Maps.scala b/pslogin/src/main/scala/Maps.scala index ab19d2dbb..a6f8cd0ae 100644 --- a/pslogin/src/main/scala/Maps.scala +++ b/pslogin/src/main/scala/Maps.scala @@ -1,14 +1,14 @@ // Copyright (c) 2017 PSForever import net.psforever.objects.zones.ZoneMap import net.psforever.objects.GlobalDefinitions._ -import net.psforever.objects.serverobject.ServerObjectBuilder import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.pad.VehicleSpawnPad -import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, WarpGate} +import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, StructureType, WarpGate} import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.types.Vector3 object Maps { @@ -23,16 +23,362 @@ object Maps { val map5 = new ZoneMap("map05") val map6 = new ZoneMap("map06") { - //TODO TEST ceryshen - LocalObject(ServerObjectBuilder(3353, Terminal.Constructor(ground_vehicle_terminal))) - LocalObject(ServerObjectBuilder(500, - VehicleSpawnPad.Constructor(Vector3(3962.0f, 4334.0f, 268.0f), Vector3(0f, 0f, 180.0f)) - )) //TODO guid not correct + Building2() + Building38() + Building42() + Building48() + Building49() - LocalBuilding(2, FoundationBuilder(Building.Structure)) - ObjectToBuilding(3353, 2) - ObjectToBuilding(500, 2) - TerminalToSpawnPad(3353, 500) + def Building2() : Unit = { + //Anguta + LocalBuilding(2, FoundationBuilder(Building.Structure(StructureType.Facility, Vector3(3974.2344f, 4287.914f, 0)))) + LocalObject(222, Door.Constructor) //air term building, bay door + LocalObject(370, Door.Constructor) //courtyard + LocalObject(371, Door.Constructor) //courtyard + LocalObject(372, Door.Constructor) //courtyard + LocalObject(373, Door.Constructor) //courtyard + LocalObject(375, Door.Constructor(Vector3(3924.0f, 4231.2656f, 271.82812f), Vector3(0, 0, 180))) //2nd level door, south + LocalObject(376, Door.Constructor(Vector3(3924.0f, 4240.2656f, 271.82812f), Vector3(0, 0, 0))) //2nd level door, north + LocalObject(383, Door.Constructor) //courtyard + LocalObject(384, Door.Constructor(Vector3(3939.6328f, 4232.547f, 279.26562f), Vector3(0, 0, 270))) //3rd floor door + LocalObject(385, Door.Constructor) //courtyard + LocalObject(387, Door.Constructor(Vector3(3951.9531f, 4260.008f, 271.82812f), Vector3(0, 0, 270))) //2nd level door, stairwell + LocalObject(391, Door.Constructor) //courtyard + LocalObject(393, Door.Constructor(Vector3(3997.8984f, 4344.3203f, 271.8125f), Vector3(0, 0, 0))) //air term building, upstairs door + LocalObject(394, Door.Constructor(Vector3(3999.9766f, 4314.3203f, 266.82812f), Vector3(0, 0, 270))) //air term building, f.door + LocalObject(396, Door.Constructor) //courtyard + LocalObject(398, Door.Constructor) //courtyard + LocalObject(399, Door.Constructor) //courtyard + LocalObject(402, Door.Constructor) //courtyard + LocalObject(403, Door.Constructor) //courtyard + LocalObject(404, Door.Constructor(Vector3(4060.0078f, 4319.9766f, 266.8125f), Vector3(0, 0, 0))) //b.door + LocalObject(603, Door.Constructor) + LocalObject(604, Door.Constructor) + LocalObject(605, Door.Constructor) + LocalObject(606, Door.Constructor) + LocalObject(607, Door.Constructor) + LocalObject(610, Door.Constructor) + LocalObject(611, Door.Constructor) + LocalObject(614, Door.Constructor) + LocalObject(619, Door.Constructor) + LocalObject(620, Door.Constructor(Vector3(3983.9531f, 4299.992f, 249.29688f), Vector3(0, 0, 90))) //generator room door + LocalObject(621, Door.Constructor) + LocalObject(622, Door.Constructor(Vector3(3988.0078f, 4248.0156f, 256.82812f), Vector3(0, 0, 180))) //spawn room door + LocalObject(623, Door.Constructor(Vector3(3988.0078f, 4271.9766f, 256.79688f), Vector3(0, 0, 0))) //spawn room door + LocalObject(630, Door.Constructor(Vector3(4000.0078f, 4252.0f, 249.29688f), Vector3(0, 0, 270))) //spawn room door + LocalObject(631, Door.Constructor) //spawn room door, kitchen + LocalObject(634, Door.Constructor) //air term building, interior + LocalObject(638, Door.Constructor(Vector3(4016.0078f, 4212.008f, 249.29688f), Vector3(0, 0, 270))) //cc door + LocalObject(642, Door.Constructor(Vector3(4023.9844f, 4212.008f, 249.32812f), Vector3(0, 0, 90))) //cc door, interior + LocalObject(643, Door.Constructor) //cc door, exterior + LocalObject(645, Door.Constructor) //b.door, interior + LocalObject(646, Door.Constructor) //b.door, interior + LocalObject(715, Door.Constructor(Vector3(3961.5938f ,4235.8125f, 266.84375f), Vector3(0, 0, 90))) //f.door + LocalObject(751, IFFLock.Constructor) + LocalObject(860, IFFLock.Constructor) + LocalObject(863, IFFLock.Constructor) + LocalObject(866, IFFLock.Constructor) + LocalObject(868, IFFLock.Constructor) + LocalObject(873, IFFLock.Constructor) + LocalObject(874, IFFLock.Constructor) + LocalObject(875, IFFLock.Constructor) + LocalObject(876, IFFLock.Constructor) + LocalObject(878, IFFLock.Constructor) + LocalObject(879, IFFLock.Constructor) + LocalObject(882, IFFLock.Constructor) + LocalObject(884, IFFLock.Constructor) + LocalObject(885, IFFLock.Constructor) + LocalObject(1177, Locker.Constructor) + LocalObject(1178, Locker.Constructor) + LocalObject(1179, Locker.Constructor) + LocalObject(1180, Locker.Constructor) + LocalObject(1181, Locker.Constructor) + LocalObject(1182, Locker.Constructor) + LocalObject(1183, Locker.Constructor) + LocalObject(1184, Locker.Constructor) + LocalObject(1185, Locker.Constructor) + LocalObject(1186, Locker.Constructor) + LocalObject(1187, Locker.Constructor) + LocalObject(1188, Locker.Constructor) + LocalObject(1564, Terminal.Constructor(order_terminal)) + LocalObject(1568, Terminal.Constructor(order_terminal)) + LocalObject(1569, Terminal.Constructor(order_terminal)) + LocalObject(1570, Terminal.Constructor(order_terminal)) + LocalObject(1571, Terminal.Constructor(order_terminal)) + LocalObject(1576, Terminal.Constructor(order_terminal)) + LocalObject(1577, Terminal.Constructor(order_terminal)) + LocalObject(1578, Terminal.Constructor(order_terminal)) + LocalObject(2145, SpawnTube.Constructor(Vector3(3980.4062f, 4252.7656f, 257.5625f), Vector3(0, 0, 90))) + LocalObject(2146, SpawnTube.Constructor(Vector3(3980.4062f, 4259.992f, 257.5625f), Vector3(0, 0, 90))) + LocalObject(2147, SpawnTube.Constructor(Vector3(3980.4062f, 4267.3047f, 257.5625f), Vector3(0, 0, 90))) + LocalObject(2239, Terminal.Constructor(spawn_terminal)) + LocalObject(2244, Terminal.Constructor(spawn_terminal)) + LocalObject(2245, Terminal.Constructor(spawn_terminal)) + LocalObject(2246, Terminal.Constructor(spawn_terminal)) + LocalObject(2248, Terminal.Constructor(spawn_terminal)) + LocalObject(2250, Terminal.Constructor(spawn_terminal)) + LocalObject(2251, Terminal.Constructor(spawn_terminal)) + LocalObject(2253, Terminal.Constructor(spawn_terminal)) + LocalObject(2254, Terminal.Constructor(spawn_terminal)) + LocalObject(2322, Door.Constructor) //spawn tube door + LocalObject(2323, Door.Constructor) //spawn tube door + LocalObject(2324, Door.Constructor) //spawn tube door + LocalObject(2419, Terminal.Constructor(ground_vehicle_terminal)) + LocalObject(500, + VehicleSpawnPad.Constructor(Vector3(3962.0f, 4334.0f, 267.75f), Vector3(0f, 0f, 180.0f)) + ) //TODO guid not correct + LocalObject(224, Terminal.Constructor(dropship_vehicle_terminal)) + LocalObject(501, + VehicleSpawnPad.Constructor(Vector3(4012.3594f, 4364.8047f, 271.90625f), Vector3(0f, 0f, 180.0f)) + ) //TODO guid not correct + ObjectToBuilding(222, 2) + ObjectToBuilding(224, 2) + ObjectToBuilding(370, 2) + ObjectToBuilding(371, 2) + ObjectToBuilding(372, 2) + ObjectToBuilding(373, 2) + ObjectToBuilding(375, 2) + ObjectToBuilding(376, 2) + ObjectToBuilding(383, 2) + ObjectToBuilding(384, 2) + ObjectToBuilding(385, 2) + ObjectToBuilding(387, 2) + ObjectToBuilding(391, 2) + ObjectToBuilding(393, 2) + ObjectToBuilding(394, 2) + ObjectToBuilding(396, 2) + ObjectToBuilding(398, 2) + ObjectToBuilding(399, 2) + ObjectToBuilding(402, 2) + ObjectToBuilding(403, 2) + ObjectToBuilding(404, 2) + ObjectToBuilding(603, 2) + ObjectToBuilding(604, 2) + ObjectToBuilding(605, 2) + ObjectToBuilding(606, 2) + ObjectToBuilding(607, 2) + ObjectToBuilding(610, 2) + ObjectToBuilding(611, 2) + ObjectToBuilding(614, 2) + ObjectToBuilding(619, 2) + ObjectToBuilding(620, 2) + ObjectToBuilding(621, 2) + ObjectToBuilding(622, 2) + ObjectToBuilding(623, 2) + ObjectToBuilding(630, 2) + ObjectToBuilding(631, 2) + ObjectToBuilding(634, 2) + ObjectToBuilding(638, 2) + ObjectToBuilding(642, 2) + ObjectToBuilding(643, 2) + ObjectToBuilding(645, 2) + ObjectToBuilding(646, 2) + ObjectToBuilding(715, 2) + ObjectToBuilding(751, 2) + ObjectToBuilding(860, 2) + ObjectToBuilding(863, 2) + ObjectToBuilding(866, 2) + ObjectToBuilding(868, 2) + ObjectToBuilding(873, 2) + ObjectToBuilding(874, 2) + ObjectToBuilding(875, 2) + ObjectToBuilding(876, 2) + ObjectToBuilding(878, 2) + ObjectToBuilding(879, 2) + ObjectToBuilding(882, 2) + ObjectToBuilding(884, 2) + ObjectToBuilding(885, 2) + ObjectToBuilding(1177, 2) + ObjectToBuilding(1178, 2) + ObjectToBuilding(1179, 2) + ObjectToBuilding(1180, 2) + ObjectToBuilding(1181, 2) + ObjectToBuilding(1182, 2) + ObjectToBuilding(1183, 2) + ObjectToBuilding(1184, 2) + ObjectToBuilding(1185, 2) + ObjectToBuilding(1186, 2) + ObjectToBuilding(1187, 2) + ObjectToBuilding(1188, 2) + ObjectToBuilding(1564, 2) + ObjectToBuilding(1568, 2) + ObjectToBuilding(1569, 2) + ObjectToBuilding(1570, 2) + ObjectToBuilding(1571, 2) + ObjectToBuilding(1576, 2) + ObjectToBuilding(1577, 2) + ObjectToBuilding(1578, 2) + ObjectToBuilding(2145, 2) + ObjectToBuilding(2146, 2) + ObjectToBuilding(2147, 2) + ObjectToBuilding(2239, 2) + ObjectToBuilding(2244, 2) + ObjectToBuilding(2245, 2) + ObjectToBuilding(2246, 2) + ObjectToBuilding(2248, 2) + ObjectToBuilding(2250, 2) + ObjectToBuilding(2251, 2) + ObjectToBuilding(2253, 2) + ObjectToBuilding(2254, 2) + ObjectToBuilding(2322, 2) + ObjectToBuilding(2323, 2) + ObjectToBuilding(2324, 2) + ObjectToBuilding(2419, 2) + ObjectToBuilding(500, 2) + ObjectToBuilding(501, 2) + DoorToLock(375, 863) + DoorToLock(376, 860) + DoorToLock(384, 866) + DoorToLock(387, 868) + DoorToLock(393, 876) + DoorToLock(394, 879) + DoorToLock(404, 885) + DoorToLock(620, 873) + DoorToLock(622, 876) + DoorToLock(623, 874) + DoorToLock(630, 878) + DoorToLock(638, 882) + DoorToLock(642, 884) + DoorToLock(715, 751) + TerminalToSpawnPad(224, 501) + TerminalToSpawnPad(2419, 500) + } + + def Building38() : Unit = { + //Anguta, West Bunker + LocalBuilding(38, FoundationBuilder(Building.Structure(StructureType.Bunker))) + LocalObject(362, Door.Constructor) + ObjectToBuilding(362, 38) + } + + def Building42() : Unit = { + //Anguta, East Bunker(s) + LocalBuilding(42, FoundationBuilder(Building.Structure(StructureType.Bunker))) + LocalObject(407, Door.Constructor) + LocalObject(408, Door.Constructor) + ObjectToBuilding(407, 42) + ObjectToBuilding(408, 42) + } + + def Building48() : Unit = { + //North Anguta Watchtower + LocalBuilding(48, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(3864.2266f, 4518.0234f, 0)))) + LocalObject(364, Door.Constructor(Vector3(3871.9688f, 4509.992f, 269.65625f), Vector3(0f, 0f, 180f))) //s1 + LocalObject(365, Door.Constructor(Vector3(3871.9688f, 4509.992f, 279.57812f), Vector3(0f, 0f, 180f))) //s2 + LocalObject(366, Door.Constructor(Vector3(3871.9688f, 4509.992f, 299.57812f), Vector3(0f, 0f, 180f))) //s3 + LocalObject(367, Door.Constructor(Vector3(3871.9688f, 4525.9844f, 269.65625f), Vector3(0f, 0f, 0f))) //n1 + LocalObject(368, Door.Constructor(Vector3(3871.9688f, 4525.9844f, 279.57812f), Vector3(0f, 0f, 0f))) //n2 + LocalObject(369, Door.Constructor(Vector3(3871.9688f, 4525.9844f, 299.57812f), Vector3(0f, 0f, 0f))) //n3 + LocalObject(854, IFFLock.Constructor) + LocalObject(855, IFFLock.Constructor) + LocalObject(856, IFFLock.Constructor) + LocalObject(857, IFFLock.Constructor) + LocalObject(858, IFFLock.Constructor) + LocalObject(859, IFFLock.Constructor) + LocalObject(1140, Locker.Constructor) + LocalObject(1141, Locker.Constructor) + LocalObject(1142, Locker.Constructor) + LocalObject(1143, Locker.Constructor) + LocalObject(1144, Locker.Constructor) + LocalObject(1145, Locker.Constructor) + LocalObject(1146, Locker.Constructor) + LocalObject(1147, Locker.Constructor) + LocalObject(1561, Terminal.Constructor(order_terminal)) + LocalObject(1562, Terminal.Constructor(order_terminal)) + LocalObject(1563, Terminal.Constructor(order_terminal)) + LocalObject(2138, SpawnTube.Constructor(respawn_tube_tower, Vector3(3870.9688f, 4505.7266f, 259.875f), Vector3(0, 0, 90))) + LocalObject(2139, SpawnTube.Constructor(respawn_tube_tower, Vector3(3870.9688f, 4522.1562f, 259.875f), Vector3(0, 0, 90))) + LocalObject(2315, Door.Constructor) //spawn tube door + LocalObject(2316, Door.Constructor) //spawn tube door + ObjectToBuilding(364, 48) + ObjectToBuilding(365, 48) + ObjectToBuilding(366, 48) + ObjectToBuilding(367, 48) + ObjectToBuilding(368, 48) + ObjectToBuilding(369, 48) + ObjectToBuilding(854, 48) + ObjectToBuilding(855, 48) + ObjectToBuilding(856, 48) + ObjectToBuilding(857, 48) + ObjectToBuilding(858, 48) + ObjectToBuilding(859, 48) + ObjectToBuilding(1140, 48) + ObjectToBuilding(1141, 48) + ObjectToBuilding(1142, 48) + ObjectToBuilding(1143, 48) + ObjectToBuilding(1144, 48) + ObjectToBuilding(1145, 48) + ObjectToBuilding(1146, 48) + ObjectToBuilding(1147, 48) + ObjectToBuilding(1561, 48) + ObjectToBuilding(1562, 48) + ObjectToBuilding(1563, 48) + ObjectToBuilding(2138, 48) + ObjectToBuilding(2139, 48) + ObjectToBuilding(2315, 48) + ObjectToBuilding(2316, 48) + DoorToLock(364, 857) + DoorToLock(365, 858) + DoorToLock(366, 859) + DoorToLock(367, 854) + DoorToLock(368, 855) + DoorToLock(369, 856) + } + + def Building49() : Unit = { + //North Akna Air Tower + LocalBuilding(49, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(4358.3203f, 3989.5625f, 0)))) + LocalObject(430, Door.Constructor(Vector3(4366.0156f, 3981.9922f, 237.96875f), Vector3(0f, 0f, 180f))) //s1 + LocalObject(431, Door.Constructor(Vector3(4366.0156f, 3981.9922f, 257.89062f), Vector3(0f, 0f, 180f))) //s2 + LocalObject(432, Door.Constructor(Vector3(4366.0156f, 3997.9297f, 237.96875f), Vector3(0f, 0f, 0f))) //n1 + LocalObject(433, Door.Constructor(Vector3(4366.0156f, 3997.9297f, 257.89062f), Vector3(0f, 0f, 0f))) //n2 + LocalObject(902, IFFLock.Constructor) + LocalObject(903, IFFLock.Constructor) + LocalObject(906, IFFLock.Constructor) + LocalObject(907, IFFLock.Constructor) + LocalObject(1217, Locker.Constructor) + LocalObject(1218, Locker.Constructor) + LocalObject(1219, Locker.Constructor) + LocalObject(1220, Locker.Constructor) + LocalObject(1225, Locker.Constructor) + LocalObject(1226, Locker.Constructor) + LocalObject(1227, Locker.Constructor) + LocalObject(1228, Locker.Constructor) + LocalObject(1591, Terminal.Constructor(order_terminal)) + LocalObject(1592, Terminal.Constructor(order_terminal)) + LocalObject(1593, Terminal.Constructor(order_terminal)) + LocalObject(2156, SpawnTube.Constructor(respawn_tube_tower, Vector3(4364.633f, 3994.125f, 228.1875f), Vector3(0, 0, 90))) + LocalObject(2157, SpawnTube.Constructor(respawn_tube_tower, Vector3(4364.633f, 3977.7266f, 228.1875f), Vector3(0, 0, 90))) + LocalObject(2333, Door.Constructor) //spawn tube door + LocalObject(2334, Door.Constructor) //spawn tube door + ObjectToBuilding(430, 49) + ObjectToBuilding(431, 49) + ObjectToBuilding(432, 49) + ObjectToBuilding(433, 49) + ObjectToBuilding(902, 49) + ObjectToBuilding(903, 49) + ObjectToBuilding(906, 49) + ObjectToBuilding(907, 49) + ObjectToBuilding(1217, 49) + ObjectToBuilding(1218, 49) + ObjectToBuilding(1219, 49) + ObjectToBuilding(1220, 49) + ObjectToBuilding(1225, 49) + ObjectToBuilding(1226, 49) + ObjectToBuilding(1227, 49) + ObjectToBuilding(1228, 49) + ObjectToBuilding(1591, 49) + ObjectToBuilding(1592, 49) + ObjectToBuilding(1593, 49) + ObjectToBuilding(2156, 49) + ObjectToBuilding(2157, 49) + ObjectToBuilding(2333, 49) + ObjectToBuilding(2334, 49) + DoorToLock(430, 906) + DoorToLock(431, 907) + DoorToLock(432, 902) + DoorToLock(433, 903) + } } val map7 = new ZoneMap("map07") @@ -48,154 +394,241 @@ object Maps { val map12 = new ZoneMap("map12") val map13 = new ZoneMap("map13") { - LocalBuilding(1, FoundationBuilder(WarpGate.Structure)) - LocalBuilding(2, FoundationBuilder(WarpGate.Structure)) - LocalBuilding(3, FoundationBuilder(WarpGate.Structure)) + Building1() + Building2() + Building3() + Building29() + Building42() + Building51() + Building77() - LocalObject(ServerObjectBuilder(372, Door.Constructor)) - LocalObject(ServerObjectBuilder(373, Door.Constructor)) + def Building1() : Unit = { + //warpgate? + LocalBuilding(1, FoundationBuilder(WarpGate.Structure)) + } - LocalObject(ServerObjectBuilder(520, ImplantTerminalMech.Constructor)) //Hart B - LocalObject(ServerObjectBuilder(853, Terminal.Constructor(order_terminal))) - LocalObject(ServerObjectBuilder(855, Terminal.Constructor(order_terminal))) - LocalObject(ServerObjectBuilder(860, Terminal.Constructor(order_terminal))) - LocalObject(ServerObjectBuilder(1081, Terminal.Constructor(implant_terminal_interface))) //tube 520 - TerminalToInterface(520, 1081) + def Building3() : Unit = { + //warpgate? + LocalBuilding(3, FoundationBuilder(WarpGate.Structure)) + } - LocalBuilding(2, FoundationBuilder(Building.Structure)) //HART building C - LocalObject(ServerObjectBuilder(186, Terminal.Constructor(cert_terminal))) - LocalObject(ServerObjectBuilder(187, Terminal.Constructor(cert_terminal))) - LocalObject(ServerObjectBuilder(188, Terminal.Constructor(cert_terminal))) - LocalObject(ServerObjectBuilder(362, Door.Constructor)) - LocalObject(ServerObjectBuilder(370, Door.Constructor)) - LocalObject(ServerObjectBuilder(371, Door.Constructor)) - LocalObject(ServerObjectBuilder(374, Door.Constructor)) - LocalObject(ServerObjectBuilder(375, Door.Constructor)) - LocalObject(ServerObjectBuilder(394, Door.Constructor)) - LocalObject(ServerObjectBuilder(395, Door.Constructor)) - LocalObject(ServerObjectBuilder(396, Door.Constructor)) - LocalObject(ServerObjectBuilder(397, Door.Constructor)) - LocalObject(ServerObjectBuilder(398, Door.Constructor)) - LocalObject(ServerObjectBuilder(462, Door.Constructor)) - LocalObject(ServerObjectBuilder(463, Door.Constructor)) - LocalObject(ServerObjectBuilder(522, ImplantTerminalMech.Constructor)) //Hart C - LocalObject(ServerObjectBuilder(523, ImplantTerminalMech.Constructor)) //Hart C - LocalObject(ServerObjectBuilder(524, ImplantTerminalMech.Constructor)) //Hart C - LocalObject(ServerObjectBuilder(525, ImplantTerminalMech.Constructor)) //Hart C - LocalObject(ServerObjectBuilder(526, ImplantTerminalMech.Constructor)) //Hart C - LocalObject(ServerObjectBuilder(527, ImplantTerminalMech.Constructor)) //Hart C - LocalObject(ServerObjectBuilder(528, ImplantTerminalMech.Constructor)) //Hart C - LocalObject(ServerObjectBuilder(529, ImplantTerminalMech.Constructor)) //Hart C - LocalObject(ServerObjectBuilder(686, Locker.Constructor)) - LocalObject(ServerObjectBuilder(687, Locker.Constructor)) - LocalObject(ServerObjectBuilder(688, Locker.Constructor)) - LocalObject(ServerObjectBuilder(689, Locker.Constructor)) - LocalObject(ServerObjectBuilder(690, Locker.Constructor)) - LocalObject(ServerObjectBuilder(691, Locker.Constructor)) - LocalObject(ServerObjectBuilder(692, Locker.Constructor)) - LocalObject(ServerObjectBuilder(693, Locker.Constructor)) - LocalObject(ServerObjectBuilder(842, Terminal.Constructor(order_terminal))) - LocalObject(ServerObjectBuilder(843, Terminal.Constructor(order_terminal))) - LocalObject(ServerObjectBuilder(844, Terminal.Constructor(order_terminal))) - LocalObject(ServerObjectBuilder(845, Terminal.Constructor(order_terminal))) - LocalObject(ServerObjectBuilder(1082, Terminal.Constructor(implant_terminal_interface))) //TODO guid not correct - LocalObject(ServerObjectBuilder(1083, Terminal.Constructor(implant_terminal_interface))) //TODO guid not correct - LocalObject(ServerObjectBuilder(1084, Terminal.Constructor(implant_terminal_interface))) //TODO guid not correct - LocalObject(ServerObjectBuilder(1085, Terminal.Constructor(implant_terminal_interface))) //TODO guid not correct - LocalObject(ServerObjectBuilder(1086, Terminal.Constructor(implant_terminal_interface))) //TODO guid not correct - LocalObject(ServerObjectBuilder(1087, Terminal.Constructor(implant_terminal_interface))) //TODO guid not correct - LocalObject(ServerObjectBuilder(1088, Terminal.Constructor(implant_terminal_interface))) //TODO guid not correct - LocalObject(ServerObjectBuilder(1089, Terminal.Constructor(implant_terminal_interface))) //TODO guid not correct - ObjectToBuilding(186, 2) - ObjectToBuilding(187, 2) - ObjectToBuilding(188, 2) - ObjectToBuilding(522, 2) - ObjectToBuilding(523, 2) - ObjectToBuilding(524, 2) - ObjectToBuilding(525, 2) - ObjectToBuilding(526, 2) - ObjectToBuilding(527, 2) - ObjectToBuilding(528, 2) - ObjectToBuilding(529, 2) - ObjectToBuilding(686, 2) - ObjectToBuilding(687, 2) - ObjectToBuilding(688, 2) - ObjectToBuilding(689, 2) - ObjectToBuilding(690, 2) - ObjectToBuilding(691, 2) - ObjectToBuilding(692, 2) - ObjectToBuilding(693, 2) - ObjectToBuilding(842, 2) - ObjectToBuilding(843, 2) - ObjectToBuilding(844, 2) - ObjectToBuilding(845, 2) - ObjectToBuilding(1082, 2) - ObjectToBuilding(1083, 2) - ObjectToBuilding(1084, 2) - ObjectToBuilding(1085, 2) - ObjectToBuilding(1086, 2) - ObjectToBuilding(1087, 2) - ObjectToBuilding(1088, 2) - ObjectToBuilding(1089, 2) - TerminalToInterface(522, 1082) - TerminalToInterface(523, 1083) - TerminalToInterface(524, 1084) - TerminalToInterface(525, 1085) - TerminalToInterface(526, 1086) - TerminalToInterface(527, 1087) - TerminalToInterface(528, 1088) - TerminalToInterface(529, 1089) +// LocalBuilding(2, FoundationBuilder(WarpGate.Structure)) //TODO might be wrong? - LocalBuilding(29, FoundationBuilder(Building.Structure)) //South Villa Gun Tower - LocalObject(ServerObjectBuilder(330, Door.Constructor(Vector3(3979.9219f, 2592.0547f, 91.140625f), Vector3(0, 0, 180)))) - LocalObject(ServerObjectBuilder(331, Door.Constructor(Vector3(3979.9219f, 2592.0547f, 111.140625f), Vector3(0, 0, 180)))) - LocalObject(ServerObjectBuilder(332, Door.Constructor(Vector3(3979.9688f, 2608.0625f, 91.140625f), Vector3(0, 0, 0)))) - LocalObject(ServerObjectBuilder(333, Door.Constructor(Vector3(3979.9688f, 2608.0625f, 111.140625f), Vector3(0, 0, 0)))) - LocalObject(ServerObjectBuilder(556, IFFLock.Constructor)) - LocalObject(ServerObjectBuilder(557, IFFLock.Constructor)) - LocalObject(ServerObjectBuilder(558, IFFLock.Constructor)) - LocalObject(ServerObjectBuilder(559, IFFLock.Constructor)) - ObjectToBuilding(330, 29) - ObjectToBuilding(331, 29) - ObjectToBuilding(332, 29) - ObjectToBuilding(333, 29) - ObjectToBuilding(556, 29) - ObjectToBuilding(557, 29) - ObjectToBuilding(558, 29) - ObjectToBuilding(559, 29) - DoorToLock(330, 558) - DoorToLock(331, 559) - DoorToLock(332, 556) - DoorToLock(333, 557) +// LocalObject(520, ImplantTerminalMech.Constructor) //Hart B +// LocalObject(1081, Terminal.Constructor(implant_terminal_interface)) //tube 520 +// TerminalToInterface(520, 1081) - LocalBuilding(51, FoundationBuilder(Building.Structure)) - LocalObject(ServerObjectBuilder(304, Terminal.Constructor(dropship_vehicle_terminal))) - LocalObject(ServerObjectBuilder(292, - VehicleSpawnPad.Constructor(Vector3(3508.9844f, 2895.961f, 92.296875f), Vector3(0f, 0f, 270.0f)) - )) - ObjectToBuilding(304, 51) - ObjectToBuilding(292, 51) - TerminalToSpawnPad(304, 292) + def Building2() : Unit = { + //HART building C + LocalBuilding(2, FoundationBuilder(Building.Structure(StructureType.Building))) + LocalObject(186, Terminal.Constructor(cert_terminal)) + LocalObject(187, Terminal.Constructor(cert_terminal)) + LocalObject(188, Terminal.Constructor(cert_terminal)) + LocalObject(362, Door.Constructor) + LocalObject(370, Door.Constructor) + LocalObject(371, Door.Constructor) + LocalObject(374, Door.Constructor) + LocalObject(375, Door.Constructor) + LocalObject(394, Door.Constructor) + LocalObject(395, Door.Constructor) + LocalObject(396, Door.Constructor) + LocalObject(397, Door.Constructor) + LocalObject(398, Door.Constructor) + LocalObject(462, Door.Constructor) + LocalObject(463, Door.Constructor) + LocalObject(522, ImplantTerminalMech.Constructor) + LocalObject(523, ImplantTerminalMech.Constructor) + LocalObject(524, ImplantTerminalMech.Constructor) + LocalObject(525, ImplantTerminalMech.Constructor) + LocalObject(526, ImplantTerminalMech.Constructor) + LocalObject(527, ImplantTerminalMech.Constructor) + LocalObject(528, ImplantTerminalMech.Constructor) + LocalObject(529, ImplantTerminalMech.Constructor) + LocalObject(686, Locker.Constructor) + LocalObject(687, Locker.Constructor) + LocalObject(688, Locker.Constructor) + LocalObject(689, Locker.Constructor) + LocalObject(690, Locker.Constructor) + LocalObject(691, Locker.Constructor) + LocalObject(692, Locker.Constructor) + LocalObject(693, Locker.Constructor) + LocalObject(842, Terminal.Constructor(order_terminal)) + LocalObject(843, Terminal.Constructor(order_terminal)) + LocalObject(844, Terminal.Constructor(order_terminal)) + LocalObject(845, Terminal.Constructor(order_terminal)) + LocalObject(1082, Terminal.Constructor(implant_terminal_interface)) //TODO guid not correct + LocalObject(1083, Terminal.Constructor(implant_terminal_interface)) //TODO guid not correct + LocalObject(1084, Terminal.Constructor(implant_terminal_interface)) //TODO guid not correct + LocalObject(1085, Terminal.Constructor(implant_terminal_interface)) //TODO guid not correct + LocalObject(1086, Terminal.Constructor(implant_terminal_interface)) //TODO guid not correct + LocalObject(1087, Terminal.Constructor(implant_terminal_interface)) //TODO guid not correct + LocalObject(1088, Terminal.Constructor(implant_terminal_interface)) //TODO guid not correct + LocalObject(1089, Terminal.Constructor(implant_terminal_interface)) //TODO guid not correct + ObjectToBuilding(186, 2) + ObjectToBuilding(187, 2) + ObjectToBuilding(188, 2) + ObjectToBuilding(362, 2) + ObjectToBuilding(370, 2) + ObjectToBuilding(371, 2) + ObjectToBuilding(374, 2) + ObjectToBuilding(375, 2) + ObjectToBuilding(394, 2) + ObjectToBuilding(395, 2) + ObjectToBuilding(396, 2) + ObjectToBuilding(397, 2) + ObjectToBuilding(398, 2) + ObjectToBuilding(462, 2) + ObjectToBuilding(463, 2) + ObjectToBuilding(522, 2) + ObjectToBuilding(523, 2) + ObjectToBuilding(524, 2) + ObjectToBuilding(525, 2) + ObjectToBuilding(526, 2) + ObjectToBuilding(527, 2) + ObjectToBuilding(528, 2) + ObjectToBuilding(529, 2) + ObjectToBuilding(686, 2) + ObjectToBuilding(687, 2) + ObjectToBuilding(688, 2) + ObjectToBuilding(689, 2) + ObjectToBuilding(690, 2) + ObjectToBuilding(691, 2) + ObjectToBuilding(692, 2) + ObjectToBuilding(693, 2) + ObjectToBuilding(842, 2) + ObjectToBuilding(843, 2) + ObjectToBuilding(844, 2) + ObjectToBuilding(845, 2) + ObjectToBuilding(1082, 2) + ObjectToBuilding(1083, 2) + ObjectToBuilding(1084, 2) + ObjectToBuilding(1085, 2) + ObjectToBuilding(1086, 2) + ObjectToBuilding(1087, 2) + ObjectToBuilding(1088, 2) + ObjectToBuilding(1089, 2) + TerminalToInterface(522, 1082) + TerminalToInterface(523, 1083) + TerminalToInterface(524, 1084) + TerminalToInterface(525, 1085) + TerminalToInterface(526, 1086) + TerminalToInterface(527, 1087) + TerminalToInterface(528, 1088) + TerminalToInterface(529, 1089) + } - LocalBuilding(77, FoundationBuilder(Building.Structure)) - LocalObject(ServerObjectBuilder(1063, Terminal.Constructor(ground_vehicle_terminal))) - LocalObject(ServerObjectBuilder(706, - VehicleSpawnPad.Constructor(Vector3(3506.0f, 2820.0f, 92.0f), Vector3(0f, 0f, 270.0f)) - )) - ObjectToBuilding(1063, 77) - ObjectToBuilding(706, 77) - TerminalToSpawnPad(1063, 706) + def Building29() : Unit = { + //South Villa Gun Tower + LocalBuilding(29, FoundationBuilder(Building.Structure(StructureType.Tower))) + LocalObject(330, Door.Constructor(Vector3(3979.9219f, 2592.0547f, 91.140625f), Vector3(0, 0, 180))) + LocalObject(331, Door.Constructor(Vector3(3979.9219f, 2592.0547f, 111.140625f), Vector3(0, 0, 180))) + LocalObject(332, Door.Constructor(Vector3(3979.9688f, 2608.0625f, 91.140625f), Vector3(0, 0, 0))) + LocalObject(333, Door.Constructor(Vector3(3979.9688f, 2608.0625f, 111.140625f), Vector3(0, 0, 0))) + LocalObject(556, IFFLock.Constructor) + LocalObject(557, IFFLock.Constructor) + LocalObject(558, IFFLock.Constructor) + LocalObject(559, IFFLock.Constructor) + ObjectToBuilding(330, 29) + ObjectToBuilding(331, 29) + ObjectToBuilding(332, 29) + ObjectToBuilding(333, 29) + ObjectToBuilding(556, 29) + ObjectToBuilding(557, 29) + ObjectToBuilding(558, 29) + ObjectToBuilding(559, 29) + DoorToLock(330, 558) + DoorToLock(331, 559) + DoorToLock(332, 556) + DoorToLock(333, 557) + } - ObjectToBuilding(853, 2) //TODO check building_id - ObjectToBuilding(855, 2) //TODO check building_id - ObjectToBuilding(860, 2) //TODO check building_id + def Building42() : Unit = { + //spawn building south of HART C + LocalBuilding(42, FoundationBuilder(Building.Structure(StructureType.Building, Vector3(1, 0, 0)))) + LocalObject(258, Door.Constructor) //spawn tube door + LocalObject(259, Door.Constructor) //spawn tube door + LocalObject(260, Door.Constructor) //spawn tube door + LocalObject(261, Door.Constructor) //spawn tube door + LocalObject(262, Door.Constructor) //spawn tube door + LocalObject(263, Door.Constructor) //spawn tube door + LocalObject(372, Door.Constructor) //entrance + LocalObject(373, Door.Constructor) //entrance + LocalObject(430, Door.Constructor) //vr door + LocalObject(431, Door.Constructor) //vr door + LocalObject(432, Door.Constructor) //vr door + LocalObject(433, Door.Constructor) //vr door + LocalObject(434, Door.Constructor) //vr door + LocalObject(435, Door.Constructor) //vr door + LocalObject(744, SpawnTube.Constructor(Vector3(3684.336f, 2709.0469f, 91.9f), Vector3(0, 0, 180))) + LocalObject(745, SpawnTube.Constructor(Vector3(3684.336f, 2713.75f, 91.9f), Vector3(0, 0, 0))) + LocalObject(746, SpawnTube.Constructor(Vector3(3690.9062f, 2708.4219f, 91.9f), Vector3(0, 0, 180))) + LocalObject(747, SpawnTube.Constructor(Vector3(3691.0703f, 2713.8672f, 91.9f), Vector3(0, 0, 0))) + LocalObject(748, SpawnTube.Constructor(Vector3(3697.664f, 2708.3984f, 91.9f), Vector3(0, 0, 180))) + LocalObject(749, SpawnTube.Constructor(Vector3(3697.711f, 2713.2344f, 91.9f), Vector3(0, 0, 0))) + LocalObject(852, Terminal.Constructor(order_terminal)) //s. wall + LocalObject(853, Terminal.Constructor(order_terminal)) //n. wall + LocalObject(854, Terminal.Constructor(order_terminal)) //s. wall + LocalObject(855, Terminal.Constructor(order_terminal)) //n. wall + LocalObject(859, Terminal.Constructor(order_terminal)) //s. wall + LocalObject(860, Terminal.Constructor(order_terminal)) //n. wall + ObjectToBuilding(258, 42) + ObjectToBuilding(259, 42) + ObjectToBuilding(260, 42) + ObjectToBuilding(261, 42) + ObjectToBuilding(262, 42) + ObjectToBuilding(263, 42) + ObjectToBuilding(372, 42) + ObjectToBuilding(373, 42) + ObjectToBuilding(430, 42) + ObjectToBuilding(431, 42) + ObjectToBuilding(432, 42) + ObjectToBuilding(433, 42) + ObjectToBuilding(434, 42) + ObjectToBuilding(435, 42) + ObjectToBuilding(744, 42) + ObjectToBuilding(745, 42) + ObjectToBuilding(746, 42) + ObjectToBuilding(747, 42) + ObjectToBuilding(748, 42) + ObjectToBuilding(749, 42) + ObjectToBuilding(852, 42) + ObjectToBuilding(853, 42) + ObjectToBuilding(854, 42) + ObjectToBuilding(855, 42) + ObjectToBuilding(859, 42) + ObjectToBuilding(860, 42) + } + + def Building51() : Unit = { + //air terminal west of HART C + LocalBuilding(51, FoundationBuilder(Building.Structure(StructureType.Platform))) + LocalObject(304, Terminal.Constructor(dropship_vehicle_terminal)) + LocalObject(292, + VehicleSpawnPad.Constructor(Vector3(3508.9844f, 2895.961f, 92.296875f), Vector3(0f, 0f, 270.0f)) + ) + ObjectToBuilding(304, 51) + ObjectToBuilding(292, 51) + TerminalToSpawnPad(304, 292) + } + + def Building77() : Unit = { + //ground terminal west of HART C + LocalBuilding(77, FoundationBuilder(Building.Structure(StructureType.Platform))) + LocalObject(1063, Terminal.Constructor(ground_vehicle_terminal)) + LocalObject(706, + VehicleSpawnPad.Constructor(Vector3(3506.0f, 2820.0f, 92.0f), Vector3(0f, 0f, 270.0f)) + ) + ObjectToBuilding(1063, 77) + ObjectToBuilding(706, 77) + TerminalToSpawnPad(1063, 706) + } } - val map14 = new ZoneMap("map13") + val map14 = new ZoneMap("map14") - val map15 = new ZoneMap("map13") + val map15 = new ZoneMap("map15") - val map16 = new ZoneMap("map13") + val map16 = new ZoneMap("map16") val ugd01 = new ZoneMap("ugd01") @@ -209,7 +642,7 @@ object Maps { val ugd06 = new ZoneMap("ugd06") - val map96 = new ZoneMap("ugd06") + val map96 = new ZoneMap("map96") val map97 = new ZoneMap("map97") diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 509a202e8..d6c597d96 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -9,8 +9,11 @@ import scodec.Attempt.{Failure, Successful} import scodec.bits._ import org.log4s.MDC import MDCContextAware.Implicits._ +import net.psforever.objects.GlobalDefinitions._ import services.ServiceManager.Lookup import net.psforever.objects._ +import net.psforever.objects.definition.ToolDefinition +import net.psforever.objects.definition.converter.CorpseConverter import net.psforever.objects.equipment._ import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} @@ -26,8 +29,9 @@ import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, Terminal} import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage import net.psforever.objects.vehicles.{AccessPermissionGroup, Utility, VehicleLockState} -import net.psforever.objects.serverobject.structures.{Building, WarpGate} +import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate} import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.vehicles.{AccessPermissionGroup, VehicleLockState} import net.psforever.objects.zones.{InterstellarCluster, Zone} import net.psforever.packet.game.objectcreate._ @@ -52,52 +56,106 @@ class WorldSessionActor extends Actor with MDCContextAware { var vehicleService : ActorRef = ActorRef.noSender var taskResolver : ActorRef = Actor.noSender var galaxy : ActorRef = Actor.noSender - var continent : Zone = null + var continent : Zone = Zone.Nowhere + var player : Player = null + var avatar : Avatar = null var progressBarValue : Option[Float] = None var shooting : Option[PlanetSideGUID] = None var accessedContainer : Option[PlanetSideGameObject with Container] = None + var flying : Boolean = false + var speed : Float = 1.0f + var spectator : Boolean = false + var admin : Boolean = false var clientKeepAlive : Cancellable = DefaultCancellable.obj var progressBarUpdate : Cancellable = DefaultCancellable.obj + var reviveTimer : Cancellable = DefaultCancellable.obj override def postStop() = { - if(clientKeepAlive != null) - clientKeepAlive.cancel() - localService ! Service.Leave() - vehicleService ! Service.Leave() - avatarService ! Service.Leave() - LivePlayerList.Remove(sessionId) match { - case Some(tplayer) => - tplayer.VehicleSeated match { - case Some(vehicle_guid) => - //TODO do this at some other time - vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(tplayer.GUID, 0, true, vehicle_guid)) - case None => ; - } - tplayer.VehicleOwned match { - case Some(vehicle_guid) => - continent.GUID(vehicle_guid) match { - case Some(vehicle : Vehicle) => - vehicle.Owner = None - //TODO temporary solution; to un-own, permit driver seat to Empire access level - vehicle.PermissionGroup(10, VehicleLockState.Empire.id) - vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SeatPermissions(tplayer.GUID, vehicle_guid, 10, VehicleLockState.Empire.id)) - case _ => ; - } - case None => ; - } + clientKeepAlive.cancel + reviveTimer.cancel + PlayerActionsToCancel() + localService ! Service.Leave() + vehicleService ! Service.Leave() + avatarService ! Service.Leave() - if(tplayer.HasGUID) { - val guid = tplayer.GUID - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(guid, guid)) - taskResolver ! GUIDTask.UnregisterAvatar(tplayer)(continent.GUID) - //TODO normally, the actual player avatar persists a minute or so after the user disconnects - } + LivePlayerList.Remove(sessionId) + if(player != null && player.HasGUID) { + val player_guid = player.GUID + if(player.isAlive) { + //actually being alive or manually deconstructing + player.VehicleSeated match { + case Some(vehicle_guid) => + DismountVehicleOnLogOut(vehicle_guid, player_guid) + case None => ; + } + continent.Population ! Zone.Population.Release(avatar) + player.Position = Vector3.Zero //save character before doing this + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectDelete(player_guid, player_guid)) + taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) + //TODO normally, the actual player avatar persists a minute or so after the user disconnects + } + else if(continent.LivePlayers.contains(player) && !continent.Corpses.contains(player)) { + //player disconnected while waiting for a revive + //similar to handling ReleaseAvatarRequestMessage + player.Release + continent.Population ! Zone.Population.Release(avatar) + player.VehicleSeated match { + case None => + continent.Population ! Zone.Corpse.Add(player) + FriskCorpse(player) //TODO eliminate dead letters + if(!WellLootedCorpse(player)) { + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent)) + taskResolver ! GUIDTask.UnregisterLocker(player.Locker)(continent.GUID) //rest of player will be cleaned up with corpses + } + else { //no items in inventory; leave no corpse + val player_guid = player.GUID + player.Position = Vector3.Zero //save character before doing this + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) + taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) + } + + case Some(vehicle_guid) => + val player_guid = player.GUID + player.Position = Vector3.Zero //save character before doing this + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) + taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) + DismountVehicleOnLogOut(vehicle_guid, player_guid) + } + } + + player.VehicleOwned match { + case Some(vehicle_guid) => + continent.GUID(vehicle_guid) match { + case Some(vehicle : Vehicle) => + vehicle.Owner = None + //TODO temporary solution; to un-own, permit driver seat to Empire access level + vehicle.PermissionGroup(10, VehicleLockState.Empire.id) + vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SeatPermissions(player_guid, vehicle_guid, 10, VehicleLockState.Empire.id)) + case _ => ; + } case None => ; + } + continent.Population ! Zone.Population.Leave(avatar) } } + /** + * Vehicle cleanup that is specific to log out behavior. + * @param vehicle_guid the vehicle being occupied + * @param player_guid the player + */ + def DismountVehicleOnLogOut(vehicle_guid : PlanetSideGUID, player_guid : PlanetSideGUID) : Unit = { + val vehicle = continent.GUID(vehicle_guid).get.asInstanceOf[Vehicle] + vehicle.Seat(vehicle.PassengerInSeat(player).get).get.Occupant = None + if(vehicle.Seats.values.count(_.isOccupied) == 0) { + vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(vehicle, continent, 600L) //start vehicle decay (10m) + } + vehicleService ! Service.Leave(Some(s"${vehicle.Actor}")) + vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, 0, true, vehicle_guid)) + } + def receive = Initializing def Initializing : Receive = { @@ -145,18 +203,20 @@ class WorldSessionActor extends Actor with MDCContextAware { case GamePacket(_, _, pkt) => handleGamePkt(pkt) // temporary hack to keep the client from disconnecting + //it's been a "temporary hack" since 2016 :P case PokeClient() => sendResponse(KeepAliveMessage()) case AvatarServiceResponse(_, guid, reply) => + val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(0) } reply match { case AvatarResponse.ArmorChanged(suit, subtype) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ArmorChangedMessage(guid, suit, subtype)) } case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3(0,0,0), 0f, 0f, 0f)) sendResponse( ObjectCreateMessage( @@ -170,27 +230,27 @@ class WorldSessionActor extends Actor with MDCContextAware { } case AvatarResponse.ChangeFireMode(item_guid, mode) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ChangeFireModeMessage(item_guid, mode)) } case AvatarResponse.ChangeFireState_Start(weapon_guid) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ChangeFireStateMessage_Start(weapon_guid)) } case AvatarResponse.ChangeFireState_Stop(weapon_guid) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ChangeFireStateMessage_Stop(weapon_guid)) } case AvatarResponse.ConcealPlayer() => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(GenericObjectActionMessage(guid, 36)) } case AvatarResponse.EquipmentInHand(slot, item) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { val definition = item.Definition sendResponse( ObjectCreateMessage( @@ -203,7 +263,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } case AvatarResponse.EquipmentOnGround(pos, orient, item_id, item_guid, item_data) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse( ObjectCreateMessage( item_id, @@ -214,27 +274,27 @@ class WorldSessionActor extends Actor with MDCContextAware { } case AvatarResponse.LoadPlayer(pdata) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ObjectCreateMessage(ObjectClass.avatar, guid, pdata)) } case AvatarResponse.ObjectDelete(item_guid, unk) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ObjectDeleteMessage(item_guid, unk)) } case AvatarResponse.ObjectHeld(slot) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ObjectHeldMessage(guid, slot, false)) } case AvatarResponse.PlanetsideAttribute(attribute_type, attribute_value) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(PlanetsideAttributeMessage(guid, attribute_type, attribute_value)) } case AvatarResponse.PlayerState(msg, spectating, weaponInHand) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { val now = System.currentTimeMillis() val (location, time, distanceSq) : (Vector3, Long, Float) = if(spectating) { @@ -274,13 +334,18 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + case AvatarResponse.Release(tplayer) => + if(tplayer_guid != guid) { + TurnPlayerIntoCorpse(tplayer) + } + case AvatarResponse.Reload(item_guid) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ReloadMessage(item_guid, 1, 0)) } case AvatarResponse.WeaponDryFire(weapon_guid) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(WeaponDryFireMessage(weapon_guid)) } @@ -288,9 +353,10 @@ class WorldSessionActor extends Actor with MDCContextAware { } case LocalServiceResponse(_, guid, reply) => + val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(0) } reply match { case LocalResponse.DoorOpens(door_guid) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(GenericObjectStateMsg(door_guid, 16)) } @@ -312,28 +378,29 @@ class WorldSessionActor extends Actor with MDCContextAware { } case VehicleServiceResponse(_, guid, reply) => + val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(0) } reply match { case VehicleResponse.Awareness(vehicle_guid) => //resets exclamation point fte marker (once) sendResponse(PlanetsideAttributeMessage(guid, 21, vehicle_guid.guid.toLong)) case VehicleResponse.ChildObjectState(object_guid, pitch, yaw) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ChildObjectStateMessage(object_guid, pitch, yaw)) } case VehicleResponse.DismountVehicle(unk1, unk2) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(DismountVehicleMsg(guid, unk1, unk2)) } case VehicleResponse.DeployRequest(object_guid, state, unk1, unk2, pos) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(DeployRequestMessage(guid, object_guid, state, unk1, unk2, pos)) } case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? val obj_guid = obj.GUID sendResponse(ObjectDeleteMessage(obj_guid, 0)) @@ -349,7 +416,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case VehicleResponse.KickPassenger(unk1, unk2, vehicle_guid) => sendResponse(DismountVehicleMsg(guid, unk1, unk2)) - if(guid == player.GUID) { + if(tplayer_guid == guid) { continent.GUID(vehicle_guid) match { case Some(obj : Vehicle) => UnAccessContents(obj) @@ -359,23 +426,23 @@ class WorldSessionActor extends Actor with MDCContextAware { case VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata) => //this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible) - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ObjectCreateMessage(vtype, vguid, vdata)) ReloadVehicleAccessPermissions(vehicle) } case VehicleResponse.MountVehicle(vehicle_guid, seat) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ObjectAttachMessage(vehicle_guid, guid, seat)) } case VehicleResponse.SeatPermissions(vehicle_guid, seat_group, permission) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(PlanetsideAttributeMessage(vehicle_guid, seat_group, permission)) } case VehicleResponse.StowEquipment(vehicle_guid, slot, item_type, item_guid, item_data) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { //TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly? sendResponse( ObjectCreateDetailedMessage(item_type, item_guid, ObjectCreateMessageParent(vehicle_guid, slot), item_data) @@ -386,13 +453,13 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ObjectDeleteMessage(vehicle_guid, 0)) case VehicleResponse.UnstowEquipment(item_guid) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? sendResponse(ObjectDeleteMessage(item_guid, 0)) } case VehicleResponse.VehicleState(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6)) if(player.VehicleSeated.contains(vehicle_guid)) { player.Position = pos @@ -534,7 +601,7 @@ class WorldSessionActor extends Actor with MDCContextAware { else { vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, seat_num, true, obj.GUID)) } - if(obj.Seats.values.count(seat => seat.isOccupied) == 0) { + if(obj.Seats.values.count(_.isOccupied) == 0) { vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(obj, continent, 600L) //start vehicle decay (10m) } @@ -756,9 +823,9 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.InfantryLoadout, true)) case Terminal.LearnCertification(cert, cost) => - if(!player.Certifications.contains(cert)) { + if(!tplayer.Certifications.contains(cert)) { log.info(s"$tplayer is learning the $cert certification for $cost points") - tplayer.Certifications += cert + avatar.Certifications += cert sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 24, cert.id.toLong)) sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, true)) } @@ -768,9 +835,9 @@ class WorldSessionActor extends Actor with MDCContextAware { } case Terminal.SellCertification(cert, cost) => - if(player.Certifications.contains(cert)) { + if(tplayer.Certifications.contains(cert)) { log.info(s"$tplayer is forgetting the $cert certification for $cost points") - tplayer.Certifications -= cert + avatar.Certifications -= cert sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 25, cert.id.toLong)) sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true)) } @@ -787,8 +854,8 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(mech_guid) => ( continent.Map.TerminalToInterface.get(mech_guid.guid), - if(!tplayer.Implants.exists({slot => slot.Implant == implant_type})) { //no duplicates - tplayer.InstallImplant(implant) + if(!avatar.Implants.exists({slot => slot.Implant == implant_type})) { //no duplicates + avatar.InstallImplant(implant) } else { None @@ -827,7 +894,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(mech_guid) => ( continent.Map.TerminalToInterface.get(mech_guid.guid), - tplayer.UninstallImplant(implant_type) + avatar.UninstallImplant(implant_type) ) case None => (None, None) @@ -907,7 +974,6 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.Transport ! Zone.SpawnVehicle(vehicle) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.LoadVehicle(player_guid, vehicle, objedtId, vehicle_guid, vdata)) sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 1L)) //mount points off? - sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 1L)) //mount points off? sendResponse(PlanetsideAttributeMessage(vehicle_guid, 21, player_guid.guid)) //fte and ownership? //sendResponse(ObjectAttachMessage(vehicle_guid, player_guid, 0)) vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(vehicle_guid) //cancel queue timeout delay @@ -959,33 +1025,16 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0)) - case InterstellarCluster.GiveWorld(zoneId, zone) => - log.info(s"Zone $zoneId has been loaded") - player.Continent = zoneId - continent = zone - taskResolver ! RegisterAvatar(player) - - case PlayerLoaded(tplayer) => - log.info(s"Player $tplayer has been loaded") - //init for whole server - galaxy ! InterstellarCluster.RequestClientInitialization(tplayer) - - case PlayerFailedToLoad(tplayer) => - player.Continent match { - case _ => - failWithError(s"$tplayer failed to load anywhere") - } - case VehicleLoaded(_/*vehicle*/) => ; //currently being handled by VehicleSpawnPad.LoadVehicle during testing phase case Zone.ClientInitialization(zone) => val continentNumber = zone.Number - val poplist = LivePlayerList.ZonePopulation(continentNumber, _ => true) + val poplist = zone.Players val popBO = 0 //TODO black ops test (partition) - val popTR = poplist.count(_.Faction == PlanetSideEmpire.TR) - val popNC = poplist.count(_.Faction == PlanetSideEmpire.NC) - val popVS = poplist.count(_.Faction == PlanetSideEmpire.VS) + val popTR = poplist.count(_.faction == PlanetSideEmpire.TR) + val popNC = poplist.count(_.faction == PlanetSideEmpire.NC) + val popVS = poplist.count(_.faction == PlanetSideEmpire.VS) zone.Buildings.foreach({ case(id, building) => initBuilding(continentNumber, id, building) }) sendResponse(ZonePopulationUpdateMessage(continentNumber, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO)) @@ -998,42 +1047,135 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0)) sendResponse(HotSpotUpdateMessage(continentNumber, 1, Nil)) //normally set in bulk; should be fine doing per continent - case InterstellarCluster.ClientInitializationComplete(tplayer)=> + case Zone.Population.PlayerHasLeft(zone, None) => + log.info(s"$avatar does not have a body on ${zone.Id}") + + case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) => + if(tplayer.isAlive) { + log.info(s"$tplayer has left zone ${zone.Id}") + } + + case Zone.Population.PlayerCanNotSpawn(zone, tplayer) => + log.warn(s"$tplayer can not spawn in zone ${zone.Id}; why?") + + case Zone.Population.PlayerAlreadySpawned(zone, tplayer) => + log.warn(s"$tplayer is already spawned on zone ${zone.Id}; a clerical error?") + + case Zone.Lattice.SpawnPoint(zone_id, building, spawn_tube) => + log.info(s"Zone.Lattice.SpawnPoint: spawn point on $zone_id in ${building.Id} @ ${spawn_tube.GUID.guid} selected") + reviveTimer.cancel + val sameZone = zone_id == continent.Id + val backpack = player.isBackpack + val respawnTime : Long = if(sameZone) { 10 } else { 0 } //s + val respawnTimeMillis = respawnTime * 1000 //ms + sendResponse(AvatarDeadStateMessage(DeadState.RespawnTime, respawnTimeMillis, respawnTimeMillis, Vector3.Zero, player.Faction, true)) + val tplayer = if(backpack) { + RespawnClone(player) //new player + } + else { + val player_guid = player.GUID + sendResponse(ObjectDeleteMessage(player_guid, 4)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 4)) + player //player is deconstructing self + } + + tplayer.Position = spawn_tube.Position + tplayer.Orientation = spawn_tube.Orientation + val (target, msg) : (ActorRef, Any) = if(sameZone) { + if(backpack) { + //respawning from unregistered player + (taskResolver, RegisterAvatar(tplayer)) + } + else { + //move existing player + (self, PlayerLoaded(tplayer)) + } + } + else { + continent.Population ! Zone.Population.Leave(avatar) + val original = player + //TODO check player orientation upon spawn not polluted + if(backpack) { + //unregister avatar locker + GiveWorld + player = tplayer + (taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterLocker(original.Locker)(continent.GUID), zone_id)) + } + else { + //unregister avatar whole + GiveWorld + (taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(original)(continent.GUID), zone_id)) + } + } + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + context.system.scheduler.scheduleOnce(respawnTime seconds, target, msg) + + case Zone.Lattice.NoValidSpawnPoint(zone_number, None) => + log.warn(s"Zone.Lattice.SpawnPoint: zone $zone_number could not be accessed as requested") + reviveTimer.cancel + RequestSanctuaryZoneSpawn(player, zone_number) + + case Zone.Lattice.NoValidSpawnPoint(zone_number, Some(spawn_group)) => + log.warn(s"Zone.Lattice.SpawnPoint: zone $zone_number has no available ${player.Faction} targets in spawn group $spawn_group") + reviveTimer.cancel + RequestSanctuaryZoneSpawn(player, zone_number) + + case InterstellarCluster.ClientInitializationComplete() => + LivePlayerList.Add(sessionId, avatar) //PropertyOverrideMessage sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1)) sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil)) sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil)) + galaxy ! InterstellarCluster.GetWorld("z6") + case InterstellarCluster.GiveWorld(zoneId, zone) => + log.info(s"Zone $zoneId will now load") + player.Continent = zoneId + continent = zone + continent.Population ! Zone.Population.Join(avatar) + taskResolver ! RegisterNewAvatar(player) + + case NewPlayerLoaded(tplayer) => + log.info(s"Player ${tplayer.Name} has been loaded") + player = tplayer //LoadMapMessage will cause the client to send back a BeginZoningMessage packet (see below) sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100,25,true,3770441820L)) - log.info("Load the now-registered player") - //load the now-registered player - tplayer.Spawn - tplayer.Health = 50 - val dcdata = tplayer.Definition.Packet.DetailedConstructorData(tplayer).get - sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, tplayer.GUID, dcdata)) - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.LoadPlayer(tplayer.GUID, tplayer.Definition.Packet.ConstructorData(tplayer).get)) - log.debug(s"ObjectCreateDetailedMessage: $dcdata") + AvatarCreate() //important! the LoadMapMessage must be processed by the client before the avatar is created + + case PlayerLoaded(tplayer) => + log.info(s"Player ${tplayer.Name} will respawn") + player = tplayer + AvatarCreate() + self ! SetCurrentAvatar(tplayer) + + case PlayerFailedToLoad(tplayer) => + player.Continent match { + case _ => + failWithError(s"${tplayer.Name} failed to load anywhere") + } case SetCurrentAvatar(tplayer) => + player = tplayer val guid = tplayer.GUID - LivePlayerList.Assign(continent.Number, sessionId, guid) sendResponse(SetCurrentAvatarMessage(guid,0,0)) + sendResponse(PlayerStateShiftMessage(ShiftState(1, tplayer.Position, tplayer.Orientation.z))) + if(spectator) { + sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None)) + } (0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => { sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1)) //init implant slot sendResponse(AvatarImplantMessage(guid, ImplantAction.Activation, slot, 0)) //deactivate implant - //TODO: if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63 + //TODO if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63 }) sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0)) - //TODO: if Medkit does not have shortcut, add to a free slot or write over slot 64 + //TODO if Medkit does not have shortcut, add to a free slot or write over slot 64 sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)) sendResponse(ChangeShortcutBankMessage(guid, 0)) //FavoritesMessage sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on" like this - sendResponse(AvatarDeadStateMessage(DeadState.Nothing, 0,0, tplayer.Position, 0, true)) + sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0,0, tplayer.Position, player.Faction, true)) sendResponse(PlanetsideAttributeMessage(guid, 53, 1)) sendResponse(AvatarSearchCriteriaMessage(guid, List(0,0,0,0,0,0))) (1 to 73).foreach(i => { @@ -1125,48 +1267,48 @@ class WorldSessionActor extends Actor with MDCContextAware { } } - var player : Player = null - def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match { case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) => val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate" log.info(s"New world login to $server with Token:$token. $clientVersion") //TODO begin temp player character auto-loading; remove later import net.psforever.objects.GlobalDefinitions._ - player = Player("TestCharacter"+sessionId.toString, PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) - //player.Position = Vector3(3674.8438f, 2726.789f, 91.15625f) - //player.Position = Vector3(3523.039f, 2855.5078f, 90.859375f) - player.Position = Vector3(3561.0f, 2854.0f, 90.859375f) - player.Orientation = Vector3(0f, 0f, 90f) - player.Certifications += CertificationType.StandardAssault - player.Certifications += CertificationType.MediumAssault - player.Certifications += CertificationType.StandardExoSuit - player.Certifications += CertificationType.AgileExoSuit - player.Certifications += CertificationType.ReinforcedExoSuit - player.Certifications += CertificationType.ATV - player.Certifications += CertificationType.Harasser + import net.psforever.types.CertificationType._ + avatar = Avatar("TestCharacter"+sessionId.toString, PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) + avatar.Certifications += StandardAssault + avatar.Certifications += MediumAssault + avatar.Certifications += StandardExoSuit + avatar.Certifications += AgileExoSuit + avatar.Certifications += ReinforcedExoSuit + avatar.Certifications += ATV + avatar.Certifications += Harasser // - player.Certifications += CertificationType.InfiltrationSuit - player.Certifications += CertificationType.Sniping - player.Certifications += CertificationType.AntiVehicular - player.Certifications += CertificationType.HeavyAssault - player.Certifications += CertificationType.SpecialAssault - player.Certifications += CertificationType.EliteAssault - player.Certifications += CertificationType.GroundSupport - player.Certifications += CertificationType.GroundTransport - player.Certifications += CertificationType.Flail - player.Certifications += CertificationType.Switchblade - player.Certifications += CertificationType.AssaultBuggy - player.Certifications += CertificationType.ArmoredAssault1 - player.Certifications += CertificationType.ArmoredAssault2 - player.Certifications += CertificationType.AirCavalryScout - player.Certifications += CertificationType.AirCavalryAssault - player.Certifications += CertificationType.AirCavalryInterceptor - player.Certifications += CertificationType.AirSupport - player.Certifications += CertificationType.GalaxyGunship - player.Certifications += CertificationType.Phantasm - player.Certifications += CertificationType.UniMAX - AwardBattleExperiencePoints(player, 1000000L) + avatar.Certifications += InfiltrationSuit + avatar.Certifications += Sniping + avatar.Certifications += AntiVehicular + avatar.Certifications += HeavyAssault + avatar.Certifications += SpecialAssault + avatar.Certifications += EliteAssault + avatar.Certifications += GroundSupport + avatar.Certifications += GroundTransport + avatar.Certifications += Flail + avatar.Certifications += Switchblade + avatar.Certifications += AssaultBuggy + avatar.Certifications += ArmoredAssault1 + avatar.Certifications += ArmoredAssault2 + avatar.Certifications += AirCavalryScout + avatar.Certifications += AirCavalryAssault + avatar.Certifications += AirCavalryInterceptor + avatar.Certifications += AirSupport + avatar.Certifications += GalaxyGunship + avatar.Certifications += Phantasm + avatar.Certifications += UniMAX + AwardBattleExperiencePoints(avatar, 1000000L) + player = new Player(avatar) + //player.Position = Vector3(3561.0f, 2854.0f, 90.859375f) //home3, HART C + //player.Orientation = Vector3(0f, 0f, 90f) + player.Position = Vector3(4262.211f ,4067.0625f ,262.35938f) //z6, Akna.tower + player.Orientation = Vector3(0f, 0f, 132.1875f) // player.ExoSuit = ExoSuitType.MAX //TODO strange issue; divide number above by 10 when uncommenting player.Slot(0).Equipment = SimpleItem(remote_electronics_kit) //Tool(GlobalDefinitions.StandardPistol(player.Faction)) player.Slot(2).Equipment = Tool(punisher) //suppressor @@ -1177,7 +1319,7 @@ class WorldSessionActor extends Actor with MDCContextAware { player.Slot(33).Equipment = AmmoBox(bullet_9mm_AP) player.Slot(36).Equipment = AmmoBox(GlobalDefinitions.StandardPistolAmmo(player.Faction)) player.Slot(39).Equipment = AmmoBox(plasma_cartridge) //SimpleItem(remote_electronics_kit) - player.Slot(5).Equipment.get.asInstanceOf[LockerContainer].Inventory += 0 -> SimpleItem(remote_electronics_kit) + player.Locker.Inventory += 0 -> SimpleItem(remote_electronics_kit) //TODO end temp player character auto-loading self ! ListAccountCharacters import scala.concurrent.duration._ @@ -1196,11 +1338,10 @@ class WorldSessionActor extends Actor with MDCContextAware { case CharacterRequestAction.Delete => sendResponse(ActionResultMessage(false, Some(1))) case CharacterRequestAction.Select => - LivePlayerList.Add(sessionId, player) //TODO check if can spawn on last continent/location from player? //TODO if yes, get continent guid accessors //TODO if no, get sanctuary guid accessors and reset the player's expectations - galaxy ! InterstellarCluster.GetWorld("home3") + galaxy ! InterstellarCluster.RequestClientInitialization() case default => log.error("Unsupported " + default + " in " + msg) } @@ -1210,12 +1351,14 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ BeginZoningMessage() => log.info("Reticulating splines ...") - //map-specific initializations - configZone(continent) //todo density + configZone(continent) sendResponse(TimeOfDayMessage(1191182336)) + //custom sendResponse(ContinentalLockUpdateMessage(13, PlanetSideEmpire.VS)) // "The VS have captured the VS Sanctuary." - (1 to 255).foreach(i => { sendResponse(SetEmpireMessage(PlanetSideGUID(i), PlanetSideEmpire.VS)) }) + sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list + sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1)) //common + //(0 to 255).foreach(i => { sendResponse(SetEmpireMessage(PlanetSideGUID(i), PlanetSideEmpire.VS)) }) //render Equipment that was dropped into zone before the player arrived continent.EquipmentOnGround.foreach(item => { @@ -1229,23 +1372,17 @@ class WorldSessionActor extends Actor with MDCContextAware { ) }) //load active players in zone - LivePlayerList.ZonePopulation(continent.Number, _ => true).foreach(char => { - sendResponse( - ObjectCreateMessage(ObjectClass.avatar, char.GUID, char.Definition.Packet.ConstructorData(char).get) - ) + continent.LivePlayers.filterNot(_.GUID == player.GUID).foreach(char => { + sendResponse(ObjectCreateMessage(ObjectClass.avatar, char.GUID, char.Definition.Packet.ConstructorData(char).get)) }) + //load corpses in zone + continent.Corpses.foreach { TurnPlayerIntoCorpse } //load active vehicles in zone continent.Vehicles.foreach(vehicle => { val definition = vehicle.Definition - sendResponse( - ObjectCreateMessage( - definition.ObjectId, - vehicle.GUID, - definition.Packet.ConstructorData(vehicle).get - ) - ) + sendResponse(ObjectCreateMessage(definition.ObjectId, vehicle.GUID, definition.Packet.ConstructorData(vehicle).get)) //seat vehicle occupants - vehicle.Definition.MountPoints.values.foreach(seat_num => { + definition.MountPoints.values.foreach(seat_num => { vehicle.Seat(seat_num).get.Occupant match { case Some(tplayer) => if(tplayer.HasGUID) { @@ -1262,14 +1399,12 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.GUID(interface_guid) match { case Some(obj : Terminal) => val objDef = obj.Definition - val obj_uid = objDef.ObjectId - val obj_data = objDef.Packet.ConstructorData(obj).get sendResponse( ObjectCreateMessage( - obj_uid, + ObjectClass.implant_terminal_interface, PlanetSideGUID(interface_guid), ObjectCreateMessageParent(parent_guid, 1), - obj_data + objDef.Packet.ConstructorData(obj).get ) ) case _ => ; @@ -1295,18 +1430,19 @@ class WorldSessionActor extends Actor with MDCContextAware { 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) => - player.Position = pos - player.Velocity = vel - player.Orientation = Vector3(player.Orientation.x, pitch, yaw) - player.FacingYawUpper = yaw_upper - player.Crouching = is_crouching - player.Jumping = is_jumping - - val wepInHand : Boolean = player.Slot(player.DrawnSlot).Equipment match { - case Some(item) => item.Definition == GlobalDefinitions.bolt_driver - case None => false + if(player.isAlive) { + player.Position = pos + player.Velocity = vel + player.Orientation = Vector3(player.Orientation.x, pitch, yaw) + player.FacingYawUpper = yaw_upper + player.Crouching = is_crouching + player.Jumping = is_jumping + val wepInHand : Boolean = player.Slot(player.DrawnSlot).Equipment match { + case Some(item) => item.Definition == GlobalDefinitions.bolt_driver + case None => false + } + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, msg, spectator, wepInHand)) } - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, msg, player.Spectator, wepInHand)) case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) => //the majority of the following check retrieves information to determine if we are in control of the child @@ -1366,28 +1502,88 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ ReleaseAvatarRequestMessage() => log.info(s"ReleaseAvatarRequest: ${player.GUID} on ${continent.Id} has released") - sendResponse(PlanetsideAttributeMessage(player.GUID, 6, 1)) - sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, 2, true)) + reviveTimer.cancel + player.Release + sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true)) + continent.Population ! Zone.Population.Release(avatar) + player.VehicleSeated match { + case None => + continent.Population ! Zone.Corpse.Add(player) //TODO move back out of this match case when changing below issue + FriskCorpse(player) + if(!WellLootedCorpse(player)) { + TurnPlayerIntoCorpse(player) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent)) + } + else { //no items in inventory; leave no corpse + val player_guid = player.GUID + sendResponse(ObjectDeleteMessage(player_guid, 0)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) + taskResolver ! GUIDTask.UnregisterPlayer(player)(continent.GUID) + } + + case Some(_) => + //TODO we do not want to delete the player if he is seated in a vehicle when releasing + //TODO it is necessary for now until we know how to juggle ownership properly + val player_guid = player.GUID + sendResponse(ObjectDeleteMessage(player_guid, 0)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) + taskResolver ! GUIDTask.UnregisterPlayer(player)(continent.GUID) + self ! PacketCoding.CreateGamePacket(0, DismountVehicleMsg(player_guid, 0, true)) //let vehicle try to clean up its fields + //sendResponse(ObjectDetachMessage(vehicle_guid, player.GUID, Vector3.Zero, 0, 0, 0)) + //sendResponse(PlayerStateShiftMessage(ShiftState(1, Vector3.Zero, 0))) + } case msg @ SpawnRequestMessage(u1, u2, u3, u4, u5) => log.info(s"SpawnRequestMessage: $msg") + //TODO just focus on u5 and u2 for now + galaxy ! Zone.Lattice.RequestSpawnPoint(u5.toInt, player, u2.toInt) case msg @ SetChatFilterMessage(send_channel, origin, whitelist) => log.info("SetChatFilters: " + msg) case msg @ ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) => + var echoContents : String = contents + //TODO messy on/off strings may work + if(messagetype == ChatMessageType.CMT_FLY) { + if(contents.trim.equals("on")) { + flying = true + } + else if(contents.trim.equals("off")) { + flying = false + } + } + else if(messagetype == ChatMessageType.CMT_SPEED) { + speed = { + try { + contents.trim.toFloat + } + catch { + case _ : Exception => + echoContents = "1.000" + 1f + } + } + } + else if(messagetype == ChatMessageType.CMT_TOGGLESPECTATORMODE) { + if(contents.trim.equals("on")) { + spectator = true + } + else if(contents.trim.equals("off")) { + spectator = false + } + } + // TODO: Prevents log spam, but should be handled correctly if (messagetype != ChatMessageType.CMT_TOGGLE_GM) { log.info("Chat: " + msg) } if(messagetype == ChatMessageType.CMT_SUICIDE) { - val player_guid = player.GUID - val pos = player.Position - sendResponse(PlanetsideAttributeMessage(player_guid, 0, 0)) - sendResponse(PlanetsideAttributeMessage(player_guid, 2, 0)) - sendResponse(DestroyMessage(player_guid, player_guid, PlanetSideGUID(0), pos)) - sendResponse(AvatarDeadStateMessage(DeadState.Dead, 300000, 300000, pos, 2, true)) + KillPlayer(player) + } + + if(messagetype == ChatMessageType.CMT_DESTROY) { + self ! PacketCoding.CreateGamePacket(0, RequestDestroyMessage(PlanetSideGUID(contents.toInt))) } if (messagetype == ChatMessageType.CMT_VOICE) { @@ -1400,9 +1596,14 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(DropSession(sessionId, "user quit")) } + if(contents.trim.equals("!loc")) { //dev hack; consider bang-commands to complement slash-commands in future + echoContents = s"zone=${continent.Id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}" + log.info(echoContents) + } + // TODO: Depending on messagetype, may need to prepend sender's name to contents with proper spacing // TODO: Just replays the packet straight back to sender; actually needs to be routed to recipients! - sendResponse(ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents)) + sendResponse(ChatMsg(messagetype, has_wide_contents, recipient, echoContents, note_contents)) case msg @ VoiceHostRequest(unk, PlanetSideGUID(player_guid), data) => log.info("Player "+player_guid+" requested in-game voice chat.") @@ -1711,7 +1912,9 @@ class WorldSessionActor extends Actor with MDCContextAware { // TODO: Make sure this is the correct response for all cases continent.GUID(object_guid) match { case Some(vehicle : Vehicle) => - if(player.VehicleOwned.contains(object_guid) && vehicle.Owner.contains(player.GUID)) { + if((player.VehicleOwned.contains(object_guid) && vehicle.Owner.contains(player.GUID)) + || (player.Faction == vehicle.Faction + && ((vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Health == 0))) { vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(object_guid) vehicleService ! VehicleServiceMessage.RequestDeleteVehicle(vehicle, continent) log.info(s"RequestDestroy: vehicle $object_guid") @@ -1790,9 +1993,9 @@ class WorldSessionActor extends Actor with MDCContextAware { vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item_guid)) vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player_guid, source_guid, index, item2)) //TODO visible slot verification, in the case of BFR arms - case (_ : Player) => + case (obj : Player) => if(source.VisibleSlots.contains(index)) { - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(source_guid, index, item2)) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(source_guid, index, item2)) } case _ => ; //TODO something? @@ -1950,6 +2153,13 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) } + case Some(obj : SpawnTube) => + //deconstruction + PlayerActionsToCancel() + player.Release + sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true)) + continent.Population ! Zone.Population.Release(avatar) + case Some(obj : PlanetSideGameObject) => if(itemType != 121) { sendResponse(UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) @@ -1971,6 +2181,8 @@ class WorldSessionActor extends Actor with MDCContextAware { obj.AccessingTrunk = None UnAccessContents(obj) } + case Some(obj : Player) => + TryDisposeOfLootedCorpse(obj) case _ =>; } @@ -2000,10 +2212,10 @@ class WorldSessionActor extends Actor with MDCContextAware { action match { case FavoritesAction.Unknown => ; case FavoritesAction.Save => - player.SaveLoadout(name, line) + avatar.SaveLoadout(player, name, line) sendResponse(FavoritesMessage(0, player_guid, line, name)) case FavoritesAction.Delete => - player.DeleteLoadout(line) + avatar.DeleteLoadout(line) sendResponse(FavoritesMessage(0, player_guid, line, "")) } } @@ -2075,7 +2287,7 @@ class WorldSessionActor extends Actor with MDCContextAware { def dismountWarning(msg : String) : Unit = { log.warn(s"$msg; some vehicle might not know that a player is no longer sitting in it") } - if(player.GUID == player_guid) { + if(player.HasGUID && player.GUID == player_guid) { //normally disembarking from a seat player.VehicleSeated match { case Some(obj_guid) => @@ -2356,6 +2568,40 @@ class WorldSessionActor extends Actor with MDCContextAware { }) } + /** + * Construct tasking that registers all aspects of a `Player` avatar. + * `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled. + * @param tplayer the avatar `Player` + * @return a `TaskResolver.GiveTask` message + */ + private def RegisterNewAvatar(tplayer : Player) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localPlayer = tplayer + private val localAnnounce = self + + override def isComplete : Task.Resolution.Value = { + if(localPlayer.HasGUID) { + Task.Resolution.Success + } + else { + Task.Resolution.Incomplete + } + } + + def Execute(resolver : ActorRef) : Unit = { + log.info(s"Player $localPlayer is registered") + resolver ! scala.util.Success(this) + localAnnounce ! NewPlayerLoaded(localPlayer) //alerts WSA + } + + override def onFailure(ex : Throwable) : Unit = { + localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WSA + } + }, List(GUIDTask.RegisterAvatar(tplayer)(continent.GUID)) + ) + } + /** * Construct tasking that registers all aspects of a `Player` avatar. * `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled. @@ -2386,7 +2632,7 @@ class WorldSessionActor extends Actor with MDCContextAware { override def onFailure(ex : Throwable) : Unit = { localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WSA } - }, List(GUIDTask.RegisterAvatar(tplayer)(continent.GUID)) + }, List(GUIDTask.RegisterPlayer(tplayer)(continent.GUID)) ) } @@ -2541,6 +2787,28 @@ class WorldSessionActor extends Actor with MDCContextAware { ) } + /** + * Before calling `Interstellar.GetWorld` to change zones, perform the following task (which can be a nesting of subtasks). + * @param priorTask the tasks to perform + * @param zoneId the zone to load afterwards + * @return a `TaskResolver.GiveTask` message + */ + def TaskBeforeZoneChange(priorTask : TaskResolver.GiveTask, zoneId : String) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localService = galaxy + private val localMsg = InterstellarCluster.GetWorld(zoneId) + + override def isComplete : Task.Resolution.Value = priorTask.task.isComplete + + def Execute(resolver : ActorRef) : Unit = { + localService ! localMsg + resolver ! scala.util.Success(this) + } + }, List(priorTask) + ) + } + /** * After a client has connected to the server, their account is used to generate a list of characters. * On the character selection screen, each of these characters is made to exist temporarily when one is selected. @@ -2653,21 +2921,21 @@ class WorldSessionActor extends Actor with MDCContextAware { * @param bep the change in experience points, positive by assertion * @return the player's current battle experience points */ - def AwardBattleExperiencePoints(tplayer : Player, bep : Long) : Long = { - val oldBep = tplayer.BEP + def AwardBattleExperiencePoints(avatar : Avatar, bep : Long) : Long = { + val oldBep = avatar.BEP if(bep <= 0) { - log.error(s"trying to set $bep battle experience points on $tplayer; value can not be negative") + log.error(s"trying to set $bep battle experience points on $avatar; value can not be negative") oldBep } else { val oldSlots = DetailedCharacterData.numberOfImplantSlots(oldBep) val newBep = oldBep + bep val newSlots = DetailedCharacterData.numberOfImplantSlots(newBep) - tplayer.BEP = newBep + avatar.BEP = newBep if(newSlots > oldSlots) { (oldSlots until newSlots).foreach(slotNumber => { - tplayer.Implants(slotNumber).Unlocked = true - log.info(s"unlocking implant slot $slotNumber for $tplayer") + avatar.Implants(slotNumber).Unlocked = true + log.info(s"unlocking implant slot $slotNumber for $avatar") }) } newBep @@ -3025,10 +3293,10 @@ class WorldSessionActor extends Actor with MDCContextAware { * @param building the building object */ def initBuilding(continentNumber : Int, buildingNumber : Int, building : Building) : Unit = { - building match { - case _ : WarpGate => + building.BuildingType match { + case StructureType.WarpGate => initGate(continentNumber, buildingNumber, building) - case _ : Building => + case _ => initFacility(continentNumber, buildingNumber, building) } } @@ -3111,18 +3379,226 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(BroadcastWarpgateUpdateMessage(continentNumber, buildingNumber, false, false, true)) } + /** + * Configure the buildings and each specific amenity for that building in a given zone by sending the client packets. + * These actions are performed during the loading of a zone. + * @see `SetEmpireMessage`
+ * `PlanetsideAttributeMessage`
+ * `HackMessage` + * @param zone the zone being loaded + */ def configZone(zone : Zone) : Unit = { - zone.Buildings.foreach({case (id, building) => - sendResponse(SetEmpireMessage(PlanetSideGUID(id), building.Faction)) + zone.Buildings.values.foreach(building => { + sendResponse(SetEmpireMessage(PlanetSideGUID(building.ModelId), building.Faction)) building.Amenities.foreach(amenity => { val amenityId = amenity.GUID sendResponse(PlanetsideAttributeMessage(amenityId, 50, 0)) sendResponse(PlanetsideAttributeMessage(amenityId, 51, 0)) }) - sendResponse(HackMessage(3, PlanetSideGUID(id), PlanetSideGUID(0), 0, 3212836864L, HackState.HackCleared, 8)) + sendResponse(HackMessage(3, PlanetSideGUID(building.ModelId), PlanetSideGUID(0), 0, 3212836864L, HackState.HackCleared, 8)) }) } + /** + * The player has lost all his vitality and must be killed.
+ *
+ * Shift directly into a state of being dead on the client by setting health to zero points, + * whereupon the player will perform a dramatic death animation. + * Stamina is also set to zero points. + * If the player was in a vehicle at the time of demise, special conditions apply and + * the model must be manipulated so it behaves correctly. + * Do not move or completely destroy the `Player` object as its coordinates of death will be important.
+ *
+ * A maximum revive waiting timer is started. + * When this timer reaches zero, the avatar will attempt to spawn back on its faction-specific sanctuary continent. + * @pararm tplayer the player to be killed + */ + def KillPlayer(tplayer : Player) : Unit = { + val player_guid = tplayer.GUID + val pos = tplayer.Position + val respawnTimer = 300000 //milliseconds + tplayer.Die + sendResponse(PlanetsideAttributeMessage(player_guid, 0, 0)) + sendResponse(PlanetsideAttributeMessage(player_guid, 2, 0)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 0, 0)) + sendResponse(DestroyMessage(player_guid, player_guid, PlanetSideGUID(0), pos)) //how many players get this message? + sendResponse(AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, player.Faction, true)) + if(tplayer.VehicleSeated.nonEmpty) { + //make player invisible (if not, the cadaver sticks out the side in a seated position) + sendResponse(PlanetsideAttributeMessage(player_guid, 29, 1)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 29, 1)) + } + PlayerActionsToCancel() + + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, galaxy, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(tplayer.Faction), tplayer, 7)) + } + + /** + * An event has occurred that would cause the player character to stop certain stateful activities. + * These activities include shooting, hacking, accessing (a container), flying, and running. + * Other players in the same zone must be made aware that the player has stopped as well.
+ *
+ * Things whose configuration should not be changed:
+ * - if the player is seated + */ + def PlayerActionsToCancel() : Unit = { + progressBarUpdate.cancel + progressBarValue = None + accessedContainer match { + case Some(obj : Vehicle) => + if(obj.AccessingTrunk.contains(player.GUID)) { + obj.AccessingTrunk = None + UnAccessContents(obj) + } + accessedContainer = None + + case Some(_) => + accessedContainer = None + + case None => ; + } + shooting match { + case Some(guid) => + sendResponse(ChangeFireStateMessage_Stop(guid)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Stop(player.GUID, guid)) + shooting = None + case None => ; + } + if(flying) { + sendResponse(ChatMsg(ChatMessageType.CMT_FLY, false, "", "off", None)) + flying = false + } + if(speed > 1) { + sendResponse(ChatMsg(ChatMessageType.CMT_SPEED, false, "", "1.000", None)) + speed = 1f + } + } + + /** + * A part of the process of spawning the player into the game world. + * The function should work regardless of whether the player is alive or dead - it will make them alive. + * It adds the `WSA`-current `Player` to the current zone and sends out the expected packets. + */ + def AvatarCreate() : Unit = { + player.Spawn + player.Health = 50 //TODO temp + val packet = player.Definition.Packet + val dcdata = packet.DetailedConstructorData(player).get + sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, dcdata)) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.LoadPlayer(player.GUID, packet.ConstructorData(player).get)) + continent.Population ! Zone.Population.Spawn(avatar, player) + log.debug(s"ObjectCreateDetailedMessage: $dcdata") + } + + /** + * Produce a clone of the player that is equipped with the default infantry loadout. + * The loadout is hardcoded. + * The player is expected to be in a Standard Exo-Suit. + * @param tplayer the original player + * @return the duplication of the player, in Standard Exo-Suit and with default equipment loadout + */ + def RespawnClone(tplayer : Player) : Player = { + val faction = tplayer.Faction + val obj = Player.Respawn(tplayer) + obj.Slot(0).Equipment = Tool(StandardPistol(faction)) + obj.Slot(2).Equipment = Tool(suppressor) + obj.Slot(4).Equipment = Tool(StandardMelee(faction)) + obj.Slot(6).Equipment = AmmoBox(bullet_9mm) + obj.Slot(9).Equipment = AmmoBox(bullet_9mm) + obj.Slot(12).Equipment = AmmoBox(bullet_9mm) + obj.Slot(33).Equipment = AmmoBox(bullet_9mm_AP) + obj.Slot(36).Equipment = AmmoBox(StandardPistolAmmo(faction)) + obj.Slot(39).Equipment = SimpleItem(remote_electronics_kit) + obj + } + + /** + * Remove items from a deceased player that is not expected to be found on a corpse. + * Most all players have their melee slot knife (which can not be un-equipped normally) removed. + * MAX's have their primary weapon in the designated slot removed. + * @param obj the player to be turned into a corpse + */ + def FriskCorpse(obj : Player) : Unit = { + if(obj.isBackpack) { + obj.Slot(4).Equipment match { + case None => ; + case Some(knife) => + obj.Slot(4).Equipment = None + taskResolver ! RemoveEquipmentFromSlot(obj, knife, 4) + } + obj.Slot(0).Equipment match { + case Some(arms : Tool) => + if(GlobalDefinitions.isMaxArms(arms.Definition)) { + obj.Slot(0).Equipment = None + taskResolver ! RemoveEquipmentFromSlot(obj, arms, 0) + } + case _ => ; + } + } + } + + /** + * Creates a player that has the characteristics of a corpse. + * To the game, that is a backpack (or some pastry, festive graphical modification allowing). + * @see `CorpseConverter.converter` + * @param tplayer the player + */ + def TurnPlayerIntoCorpse(tplayer : Player) : Unit = { + sendResponse( + ObjectCreateDetailedMessage(ObjectClass.avatar, tplayer.GUID, CorpseConverter.converter.DetailedConstructorData(tplayer).get) + ) + } + + /** + * If the corpse has been well-looted, it has no items in its primary holsters nor any items in its inventory. + * @param obj the corpse + * @return `true`, if the `obj` is actually a corpse and has no objects in its holsters or backpack; + * `false`, otherwise + */ + def WellLootedCorpse(obj : Player) : Boolean = { + obj.isBackpack && obj.Holsters().count(_.Equipment.nonEmpty) == 0 && obj.Inventory.Size == 0 + } + + /** + * If the corpse has been well-looted, remove it from the ground. + * @param obj the corpse + * @return `true`, if the `obj` is actually a corpse and has no objects in its holsters or backpack; + * `false`, otherwise + */ + def TryDisposeOfLootedCorpse(obj : Player) : Boolean = { + if(WellLootedCorpse(obj)) { + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + context.system.scheduler.scheduleOnce(1 second, avatarService, AvatarServiceMessage.RemoveSpecificCorpse(List(obj))) + true + } + else { + false + } + } + + /** + * Attempt to tranfer to the player's faction-specific sanctuary continent. + * If the server thinks the player is already on his sanctuary continent, + * it will disconnect the player under the assumption that an error has occurred. + * Eventually, this functionality should support better error-handling before it jumps to the conclusion: + * "Disconnecting the client is the safest option." + * @see `Zones.SanctuaryZoneNumber` + * @param tplayer the player + * @param currentZone the current cone number + */ + def RequestSanctuaryZoneSpawn(tplayer : Player, currentZone : Int) : Unit = { + val sanctNumber = Zones.SanctuaryZoneNumber(tplayer.Faction) + if(currentZone == sanctNumber) { + sendResponse(DisconnectMessage("Player failed to load on faction's sanctuary continent. Please relog.")) + } + else { + galaxy ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 7) + } + } + def failWithError(error : String) = { log.error(error) sendResponse(ConnectionClose()) @@ -3162,6 +3638,7 @@ object WorldSessionActor { private final case class PokeClient() private final case class ServerLoaded() + private final case class NewPlayerLoaded(tplayer : Player) private final case class PlayerLoaded(tplayer : Player) private final case class PlayerFailedToLoad(tplayer : Player) private final case class ListAccountCharacters() diff --git a/pslogin/src/main/scala/Zones.scala b/pslogin/src/main/scala/Zones.scala index 352c27294..1944179f9 100644 --- a/pslogin/src/main/scala/Zones.scala +++ b/pslogin/src/main/scala/Zones.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever import akka.actor.ActorContext import net.psforever.objects.zones.Zone +import net.psforever.types.PlanetSideEmpire object Zones { val z1 = new Zone("z1", Maps.map1, 1) @@ -19,6 +20,13 @@ object Zones { import net.psforever.types.PlanetSideEmpire Building(2).get.Faction = PlanetSideEmpire.VS + Building(2).get.ModelId = 20 + Building(38).get.ModelId = 0 + Building(42).get.ModelId = 0 + Building(48).get.Faction = PlanetSideEmpire.VS + Building(48).get.ModelId = 59 + Building(49).get.Faction = PlanetSideEmpire.VS + Building(49).get.ModelId = 69 } } @@ -39,7 +47,7 @@ object Zones { super.Init(context) import net.psforever.types.PlanetSideEmpire - Buildings.values.foreach(building => { building.Faction = PlanetSideEmpire.VS }) + Buildings.values.foreach { _.Faction = PlanetSideEmpire.VS } Building(29).get.Faction = PlanetSideEmpire.NC //South Villa Gun Tower } } @@ -81,4 +89,32 @@ object Zones { val i3 = new Zone("i3", Maps.map98, 31) val i4 = new Zone("i4", Maps.map99, 32) + + /** + * Get the zone identifier name for the sanctuary continent of a given empire. + * @param faction the empire + * @return the zone id, with a blank string as an invalidating result + */ + def SanctuaryZoneId(faction : PlanetSideEmpire.Value) : String = { + faction match { + case PlanetSideEmpire.NC => "home1" + case PlanetSideEmpire.TR => "home2" + case PlanetSideEmpire.VS => "home3" + case PlanetSideEmpire.NEUTRAL => "" //invalid, not black ops + } + } + + /** + * Get the zone number for the sanctuary continent of a given empire. + * @param faction the empire + * @return the zone number, within the sequence 1-32, and with 0 as an invalidating result + */ + def SanctuaryZoneNumber(faction : PlanetSideEmpire.Value) : Int = { + faction match { + case PlanetSideEmpire.NC => 11 + case PlanetSideEmpire.TR => 12 + case PlanetSideEmpire.VS => 13 + case PlanetSideEmpire.NEUTRAL => 0 //invalid, not black ops + } + } } diff --git a/pslogin/src/main/scala/services/avatar/AvatarAction.scala b/pslogin/src/main/scala/services/avatar/AvatarAction.scala index 13ece68ee..80d49828d 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarAction.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarAction.scala @@ -1,7 +1,9 @@ // Copyright (c) 2017 PSForever package services.avatar +import net.psforever.objects.Player import net.psforever.objects.equipment.Equipment +import net.psforever.objects.zones.Zone import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.types.{ExoSuitType, Vector3} @@ -24,6 +26,7 @@ object AvatarAction { 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 Release(player : Player, zone : Zone, time : Option[Long] = None) extends Action final case class Reload(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class WeaponDryFire(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action diff --git a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala b/pslogin/src/main/scala/services/avatar/AvatarResponse.scala index 0ffe562f8..f3c605ed6 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarResponse.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever package services.avatar +import net.psforever.objects.Player import net.psforever.objects.equipment.Equipment import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.packet.game.objectcreate.ConstructorData @@ -24,6 +25,7 @@ object AvatarResponse { 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 Release(player : Player) extends Response final case class Reload(weapon_guid : PlanetSideGUID) extends Response final case class WeaponDryFire(weapon_guid : PlanetSideGUID) extends Response // final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response diff --git a/pslogin/src/main/scala/services/avatar/AvatarService.scala b/pslogin/src/main/scala/services/avatar/AvatarService.scala index dcf249020..2978f1a11 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarService.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarService.scala @@ -1,11 +1,14 @@ // Copyright (c) 2017 PSForever package services.avatar -import akka.actor.Actor +import akka.actor.{Actor, ActorRef, Props} +import services.avatar.support.CorpseRemovalActor import services.{GenericEventBus, Service} class AvatarService extends Actor { - //import AvatarServiceResponse._ + private val undertaker : ActorRef = context.actorOf(Props[CorpseRemovalActor], "corpse-removal-agent") + undertaker ! "startup" + private [this] val log = org.log4s.getLogger override def preStart = { @@ -87,6 +90,14 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarResponse.PlayerState(msg, spectator, weapon)) ) + case AvatarAction.Release(player, zone, time) => + undertaker ! (time match { + case Some(t) => CorpseRemovalActor.AddCorpse(player, zone, t) + case None => CorpseRemovalActor.AddCorpse(player, zone) + }) + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player.GUID, AvatarResponse.Release(player)) + ) case AvatarAction.Reload(player_guid, weapon_guid) => AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Reload(weapon_guid)) @@ -95,9 +106,14 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.WeaponDryFire(weapon_guid)) ) + case _ => ; } + //message to Undertaker + case AvatarServiceMessage.RemoveSpecificCorpse(corpses) => + undertaker ! AvatarServiceMessage.RemoveSpecificCorpse( corpses.filter(corpse => {corpse.HasGUID && corpse.isBackpack}) ) + /* case AvatarService.PlayerStateMessage(msg) => // log.info(s"NEW: ${m}") diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala index e3e35cd38..04b96a901 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala @@ -1,4 +1,10 @@ // Copyright (c) 2017 PSForever package services.avatar +import net.psforever.objects.Player + final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action) + +object AvatarServiceMessage { + final case class RemoveSpecificCorpse(corpse : List[Player]) +} diff --git a/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala b/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala new file mode 100644 index 000000000..ace1fc920 --- /dev/null +++ b/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala @@ -0,0 +1,199 @@ +// Copyright (c) 2017 PSForever +package services.avatar.support + +import akka.actor.{Actor, ActorRef, Cancellable} +import net.psforever.objects.guid.TaskResolver +import net.psforever.objects.{DefaultCancellable, Player} +import net.psforever.objects.zones.Zone +import net.psforever.types.Vector3 +import services.{Service, ServiceManager} +import services.ServiceManager.Lookup +import services.avatar.{AvatarAction, AvatarServiceMessage} + +import scala.annotation.tailrec +import scala.concurrent.duration._ + +class CorpseRemovalActor extends Actor { + private var burial : Cancellable = DefaultCancellable.obj + + private var corpses : List[CorpseRemovalActor.Entry] = List() + + private var taskResolver : ActorRef = Actor.noSender + + private[this] val log = org.log4s.getLogger + + override def postStop() = { + //Cart Master: See you on Thursday. + corpses.foreach { BurialTask } + corpses = Nil + } + + def receive : Receive = { + case "startup" => + ServiceManager.serviceManager ! Lookup("taskResolver") //ask for a resolver to deal with the GUID system + + case ServiceManager.LookupResult("taskResolver", endpoint) => + //Cart Master: Bring out your dead! + taskResolver = endpoint + context.become(Processing) + + case _ => ; + } + + def Processing : Receive = { + case CorpseRemovalActor.AddCorpse(corpse, zone, time) => + if(corpse.isBackpack) { + if(corpses.isEmpty) { + //we were the only entry so the event must be started from scratch + corpses = List(CorpseRemovalActor.Entry(corpse, zone, time)) + RetimeFirstTask() + } + else { + //unknown number of entries; append, sort, then re-time tasking + val oldHead = corpses.head + corpses = (corpses :+ CorpseRemovalActor.Entry(corpse, zone, time)).sortBy(_.timeAlive) + if(oldHead != corpses.head) { + RetimeFirstTask() + } + } + } + else { + //Cart Master: 'Ere. He says he's not dead! + log.warn(s"$corpse does not qualify as a corpse; ignored queueing request") + } + + case AvatarServiceMessage.RemoveSpecificCorpse(targets) => + if(targets.nonEmpty) { + //Cart Master: No, I've got to go to the Robinsons'. They've lost nine today. + burial.cancel + if(targets.size == 1) { + log.debug(s"a target corpse submitted for early cleanup: ${targets.head}") + //simple selection + CorpseRemovalActor.recursiveFindCorpse(corpses.iterator, targets.head) match { + case None => ; + case Some(index) => + BurialTask(corpses(index)) + corpses = corpses.take(index) ++ corpses.drop(index+1) + } + } + else { + log.debug(s"multiple target corpses submitted for early cleanup: $targets") + //cumbersome partition + //a - find targets from corpses + (for { + a <- targets + b <- corpses + if b.corpse == a && + b.corpse.Continent.equals(a.Continent) && + b.corpse.HasGUID && a.HasGUID && b.corpse.GUID == a.GUID + } yield b).foreach { BurialTask } + //b - corpses after the found targets are + //removed (note: cull any non-GUID entries while at it) + corpses = (for { + a <- targets + b <- corpses + if b.corpse.HasGUID && a.HasGUID && + (b.corpse != a || + !b.corpse.Continent.equals(a.Continent) || + !b.corpse.HasGUID || !a.HasGUID || b.corpse.GUID != a.GUID) + } yield b).sortBy(_.timeAlive) + } + RetimeFirstTask() + } + + case CorpseRemovalActor.Dispose() => + burial.cancel + val now : Long = System.nanoTime + val (buried, rotting) = corpses.partition(entry => { now - entry.time >= entry.timeAlive }) + corpses = rotting + buried.foreach { BurialTask } + RetimeFirstTask() + + case CorpseRemovalActor.FailureToWork(target, zone, ex) => + //Cart Master: Oh, I can't take him like that. It's against regulations. + log.error(s"corpse $target from $zone not properly unregistered - $ex") + + case _ => ; + } + + def RetimeFirstTask(now : Long = System.nanoTime) : Unit = { + //Cart Master: Thursday. + burial.cancel + if(corpses.nonEmpty) { + val short_timeout : FiniteDuration = math.max(1, corpses.head.timeAlive - (now - corpses.head.time)) nanoseconds + import scala.concurrent.ExecutionContext.Implicits.global + burial = context.system.scheduler.scheduleOnce(short_timeout, self, CorpseRemovalActor.Dispose()) + } + } + + def BurialTask(entry : CorpseRemovalActor.Entry) : Unit = { + //Cart master: Nine pence. + val target = entry.corpse + val zone = entry.zone + target.Position = Vector3.Zero //somewhere it will not disturb anything + entry.zone.Population ! Zone.Corpse.Remove(target) + context.parent ! AvatarServiceMessage(zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, target.GUID)) + taskResolver ! BurialTask(target, zone) + } + + def BurialTask(corpse : Player, zone : Zone) : TaskResolver.GiveTask = { + import net.psforever.objects.guid.{GUIDTask, Task} + TaskResolver.GiveTask ( + new Task() { + private val localCorpse = corpse + private val localZone = zone + private val localAnnounce = self + + override def isComplete : Task.Resolution.Value = if(!localCorpse.HasGUID) { + Task.Resolution.Success + } + else { + Task.Resolution.Incomplete + } + + def Execute(resolver : ActorRef) : Unit = { + resolver ! scala.util.Success(this) + } + + override def onFailure(ex : Throwable): Unit = { + localAnnounce ! CorpseRemovalActor.FailureToWork(localCorpse, localZone, ex) + } + }, List(GUIDTask.UnregisterPlayer(corpse)(zone.GUID)) + ) + } +} + +object CorpseRemovalActor { + final val time : Long = 180000000000L //3 min (180s) + + final case class AddCorpse(corpse : Player, zone : Zone, time : Long = CorpseRemovalActor.time) + + final case class Entry(corpse : Player, zone : Zone, timeAlive : Long = CorpseRemovalActor.time, time : Long = System.nanoTime()) + + final case class FailureToWork(corpse : Player, zone : Zone, ex : Throwable) + + final case class Dispose() + + /** + * A recursive function that finds and removes a specific player from a list of players. + * @param iter an `Iterator` of `CorpseRemovalActor.Entry` objects + * @param player the target `Player` + * @param index the index of the discovered `Player` object + * @return the index of the `Player` object in the list to be removed; + * `None`, otherwise + */ + @tailrec final def recursiveFindCorpse(iter : Iterator[CorpseRemovalActor.Entry], player : Player, index : Int = 0) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + val corpse = iter.next.corpse + if(corpse == player && corpse.Continent.equals(player.Continent) && corpse.GUID == player.GUID) { + Some(index) + } + else { + recursiveFindCorpse(iter, player, index + 1) + } + } + } +} diff --git a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala index adc9668a2..2aaefff4d 100644 --- a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala +++ b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala @@ -7,6 +7,7 @@ import net.psforever.objects.guid.TaskResolver import net.psforever.objects.vehicles.Seat import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.Vector3 import services.ServiceManager import services.ServiceManager.Lookup import services.vehicle.{VehicleAction, VehicleServiceMessage} @@ -80,6 +81,7 @@ class DeconstructionActor extends Actor { vehiclesToScrap.foreach(entry => { val vehicle = entry.vehicle val zone = entry.zone + vehicle.Position = Vector3.Zero //somewhere it will not disturb anything entry.zone.Transport ! Zone.DespawnVehicle(vehicle) context.parent ! DeconstructionActor.DeleteVehicle(vehicle.GUID, zone.Id) //call up to the main event system context.parent ! VehicleServiceMessage.RevokeActorControl(vehicle) //call up to a sibling manager diff --git a/pslogin/src/test/scala/AvatarServiceTest.scala b/pslogin/src/test/scala/AvatarServiceTest.scala index 56e02385f..deda78b11 100644 --- a/pslogin/src/test/scala/AvatarServiceTest.scala +++ b/pslogin/src/test/scala/AvatarServiceTest.scala @@ -1,15 +1,21 @@ // Copyright (c) 2017 PSForever import akka.actor.Props +import akka.routing.RandomPool import net.psforever.objects._ +import net.psforever.objects.guid.{GUIDTask, TaskResolver} +import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap} import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.types.{CharacterGender, ExoSuitType, PlanetSideEmpire, Vector3} -import services.Service +import services.{Service, ServiceManager} import services.avatar._ +import scala.concurrent.duration._ + class AvatarService1Test extends ActorTest { "AvatarService" should { "construct" in { - system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) assert(true) } } @@ -18,7 +24,8 @@ class AvatarService1Test extends ActorTest { class AvatarService2Test extends ActorTest { "AvatarService" should { "subscribe" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") assert(true) } @@ -27,8 +34,9 @@ class AvatarService2Test extends ActorTest { class AvatarService3Test extends ActorTest { "AvatarService" should { + ServiceManager.boot(system) "subscribe to a specific channel" in { - val service = system.actorOf(Props[AvatarService], "service") + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! Service.Leave() assert(true) @@ -39,7 +47,8 @@ class AvatarService3Test extends ActorTest { class AvatarService4Test extends ActorTest { "AvatarService" should { "subscribe" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! Service.LeaveAll() assert(true) @@ -50,7 +59,8 @@ class AvatarService4Test extends ActorTest { class AvatarService5Test extends ActorTest { "AvatarService" should { "pass an unhandled message" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! "hello" expectNoMsg() @@ -61,7 +71,8 @@ class AvatarService5Test extends ActorTest { class ArmorChangedTest extends ActorTest { "AvatarService" should { "pass ArmorChanged" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ArmorChanged(PlanetSideGUID(10), ExoSuitType.Reinforced, 0)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ArmorChanged(ExoSuitType.Reinforced, 0))) @@ -72,7 +83,8 @@ class ArmorChangedTest extends ActorTest { class ConcealPlayerTest extends ActorTest { "AvatarService" should { "pass ConcealPlayer" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ConcealPlayer(PlanetSideGUID(10))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ConcealPlayer())) @@ -85,7 +97,8 @@ class EquipmentInHandTest extends ActorTest { "AvatarService" should { "pass EquipmentInHand" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.EquipmentInHand(PlanetSideGUID(10), 2, tool)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentInHand(2, tool))) @@ -101,7 +114,8 @@ class EquipmentOnGroundTest extends ActorTest { "AvatarService" should { "pass EquipmentOnGround" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.EquipmentOnGround(PlanetSideGUID(10), Vector3(300f, 200f, 100f), Vector3(450f, 300f, 150f), toolDef.ObjectId, PlanetSideGUID(11), cdata)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentOnGround(Vector3(300f, 200f, 100f), Vector3(450f, 300f, 150f), toolDef.ObjectId, PlanetSideGUID(11), cdata))) @@ -110,14 +124,15 @@ class EquipmentOnGroundTest extends ActorTest { } class LoadPlayerTest extends ActorTest { - val obj = Player("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1) + val obj = Player(Avatar("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1)) obj.GUID = PlanetSideGUID(10) obj.Slot(5).Equipment.get.GUID = PlanetSideGUID(11) val pdata = obj.Definition.Packet.DetailedConstructorData(obj).get "AvatarService" should { "pass LoadPlayer" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.LoadPlayer(PlanetSideGUID(10), pdata)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.LoadPlayer(pdata))) @@ -128,7 +143,8 @@ class LoadPlayerTest extends ActorTest { class ObjectDeleteTest extends ActorTest { "AvatarService" should { "pass ObjectDelete" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ObjectDelete(PlanetSideGUID(10), PlanetSideGUID(11))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectDelete(PlanetSideGUID(11), 0))) @@ -142,7 +158,8 @@ class ObjectDeleteTest extends ActorTest { class ObjectHeldTest extends ActorTest { "AvatarService" should { "pass ObjectHeld" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ObjectHeld(PlanetSideGUID(10), 1)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectHeld(1))) @@ -153,7 +170,8 @@ class ObjectHeldTest extends ActorTest { class PlanetsideAttributeTest extends ActorTest { "AvatarService" should { "pass PlanetsideAttribute" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.PlanetsideAttribute(PlanetSideGUID(10), 5, 1200L)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.PlanetsideAttribute(5, 1200L))) @@ -166,7 +184,8 @@ class PlayerStateTest extends ActorTest { "AvatarService" should { "pass PlayerState" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.PlayerState(PlanetSideGUID(10), msg, false, false)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.PlayerState(msg, false, false))) @@ -177,7 +196,8 @@ class PlayerStateTest extends ActorTest { class ReloadTest extends ActorTest { "AvatarService" should { "pass Reload" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.Reload(PlanetSideGUID(10), PlanetSideGUID(40))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.Reload(PlanetSideGUID(40)))) @@ -191,7 +211,8 @@ class ChangeAmmoTest extends ActorTest { "AvatarService" should { "pass ChangeAmmo" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ChangeAmmo(PlanetSideGUID(10), PlanetSideGUID(40), 0, PlanetSideGUID(40), ammoDef.ObjectId, PlanetSideGUID(41), ammoDef.Packet.ConstructorData(ammoBox).get)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ChangeAmmo(PlanetSideGUID(40), 0, PlanetSideGUID(40), ammoDef.ObjectId, PlanetSideGUID(41), ammoDef.Packet.ConstructorData(ammoBox).get))) @@ -205,7 +226,8 @@ class ChangeFireModeTest extends ActorTest { "AvatarService" should { "pass ChangeFireMode" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ChangeFireMode(PlanetSideGUID(10), PlanetSideGUID(40), 0)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ChangeFireMode(PlanetSideGUID(40), 0))) @@ -216,7 +238,8 @@ class ChangeFireModeTest extends ActorTest { class ChangeFireStateStartTest extends ActorTest { "AvatarService" should { "pass ChangeFireState_Start" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ChangeFireState_Start(PlanetSideGUID(10), PlanetSideGUID(40))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ChangeFireState_Start(PlanetSideGUID(40)))) @@ -227,7 +250,8 @@ class ChangeFireStateStartTest extends ActorTest { class ChangeFireStateStopTest extends ActorTest { "AvatarService" should { "pass ChangeFireState_Stop" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ChangeFireState_Stop(PlanetSideGUID(10), PlanetSideGUID(40))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ChangeFireState_Stop(PlanetSideGUID(40)))) @@ -238,7 +262,8 @@ class ChangeFireStateStopTest extends ActorTest { class WeaponDryFireTest extends ActorTest { "AvatarService" should { "pass WeaponDryFire" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.WeaponDryFire(PlanetSideGUID(10), PlanetSideGUID(40))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.WeaponDryFire(PlanetSideGUID(40)))) @@ -246,6 +271,177 @@ class WeaponDryFireTest extends ActorTest { } } -object AvatarServiceTest { - //decoy +/* +Preparation for these three Release tests is involved. +The ServiceManager must not only be set up correctly, but must be given a TaskResolver. +The AvatarService is started and that starts CorpseRemovalActor, an essential part of this test. +The CorpseRemovalActor needs that TaskResolver created by the ServiceManager; +but, another independent TaskResolver will be needed for manual parts of the test. +(The ServiceManager's TaskResolver can be "borrowed" but that requires writing code to intercept it.) +The Zone needs to be set up and initialized properly with a ZoneActor. +The ZoneActor builds the GUID Actor and the ZonePopulationActor. + +ALL of these Actors will talk to each other. +The lines of communication can short circuit if the next Actor does not have the correct information. +Putting Actor startup in the main class, outside of the body of the test, helps. +Frequent pauses to allow everything to sort their messages also helps. +Even with all this work, the tests have a high chance of failure just due to being asynchronous. + */ +class AvatarReleaseTest extends ActorTest { + ServiceManager.boot(system) ! ServiceManager.Register(RandomPool(1).props(Props[TaskResolver]), "taskResolver") + val service = system.actorOf(Props[AvatarService], "release-test-service") + val zone = new Zone("test", new ZoneMap("test-map"), 0) + val taskResolver = system.actorOf(Props[TaskResolver], "release-test-resolver") + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "release-test-zone") + zone.Actor ! Zone.Init() + val obj = Player(Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1)) + obj.Continent = "test" + obj.Release + + "AvatarService" should { + "pass Release" in { + expectNoMsg(100 milliseconds) //spacer + + service ! Service.Join("test") + taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) + assert(zone.Corpses.isEmpty) + zone.Population ! Zone.Corpse.Add(obj) + expectNoMsg(100 milliseconds) //spacer + + assert(zone.Corpses.size == 1) + assert(obj.HasGUID) + val guid = obj.GUID + service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone, Some(1000000000))) //alive for one second + + val reply1 = receiveOne(100 milliseconds) + assert(reply1.isInstanceOf[AvatarServiceResponse]) + val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] + assert(reply1msg.toChannel == "/test/Avatar") + assert(reply1msg.avatar_guid == guid) + assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release]) + assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj) + + val reply2 = receiveOne(2 seconds) + assert(reply2.isInstanceOf[AvatarServiceResponse]) + val reply2msg = reply2.asInstanceOf[AvatarServiceResponse] + assert(reply2msg.toChannel.equals("/test/Avatar")) + assert(reply2msg.avatar_guid == Service.defaultPlayerGUID) + assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) + assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) + + expectNoMsg(200 milliseconds) + assert(zone.Corpses.isEmpty) + assert(!obj.HasGUID) + } + } +} + +class AvatarReleaseEarly1Test extends ActorTest { + ServiceManager.boot(system) ! ServiceManager.Register(RandomPool(1).props(Props[TaskResolver]), "taskResolver") + val service = system.actorOf(Props[AvatarService], "release-test-service") + val zone = new Zone("test", new ZoneMap("test-map"), 0) + val taskResolver = system.actorOf(Props[TaskResolver], "release-test-resolver") + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "release-test-zone") + zone.Actor ! Zone.Init() + val obj = Player(Avatar("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1)) + obj.Continent = "test" + obj.Release + + "AvatarService" should { + "pass Release" in { + expectNoMsg(100 milliseconds) //spacer + + service ! Service.Join("test") + taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) + assert(zone.Corpses.isEmpty) + zone.Population ! Zone.Corpse.Add(obj) + expectNoMsg(100 milliseconds) //spacer + + assert(zone.Corpses.size == 1) + assert(obj.HasGUID) + val guid = obj.GUID + service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone)) //3+ minutes! + + val reply1 = receiveOne(100 milliseconds) + assert(reply1.isInstanceOf[AvatarServiceResponse]) + val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] + assert(reply1msg.toChannel == "/test/Avatar") + assert(reply1msg.avatar_guid == guid) + assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release]) + assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj) + + service ! AvatarServiceMessage.RemoveSpecificCorpse(List(obj)) //IMPORTANT: ONE ENTRY + val reply2 = receiveOne(100 milliseconds) + assert(reply2.isInstanceOf[AvatarServiceResponse]) + val reply2msg = reply2.asInstanceOf[AvatarServiceResponse] + assert(reply2msg.toChannel.equals("/test/Avatar")) + assert(reply2msg.avatar_guid == Service.defaultPlayerGUID) + assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) + assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) + + expectNoMsg(200 milliseconds) + assert(zone.Corpses.isEmpty) + assert(!obj.HasGUID) + } + } +} + +class AvatarReleaseEarly2Test extends ActorTest { + ServiceManager.boot(system) ! ServiceManager.Register(RandomPool(1).props(Props[TaskResolver]), "taskResolver") + val service = system.actorOf(Props[AvatarService], "release-test-service") + val zone = new Zone("test", new ZoneMap("test-map"), 0) + val taskResolver = system.actorOf(Props[TaskResolver], "release-test-resolver") + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "release-test-zone") + zone.Actor ! Zone.Init() + val objAlt = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 1, 1)) //necessary clutter + val obj = Player(Avatar("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1)) + obj.Continent = "test" + obj.Release + + "AvatarService" should { + "pass Release" in { + expectNoMsg(100 milliseconds) //spacer + + service ! Service.Join("test") + taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) + assert(zone.Corpses.isEmpty) + zone.Population ! Zone.Corpse.Add(obj) + expectNoMsg(100 milliseconds) //spacer + + assert(zone.Corpses.size == 1) + assert(obj.HasGUID) + val guid = obj.GUID + service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone)) //3+ minutes! + + val reply1 = receiveOne(100 milliseconds) + assert(reply1.isInstanceOf[AvatarServiceResponse]) + val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] + assert(reply1msg.toChannel == "/test/Avatar") + assert(reply1msg.avatar_guid == guid) + assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release]) + assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj) + + service ! AvatarServiceMessage.RemoveSpecificCorpse(List(objAlt, obj)) //IMPORTANT: TWO ENTRIES + val reply2 = receiveOne(100 milliseconds) + assert(reply2.isInstanceOf[AvatarServiceResponse]) + val reply2msg = reply2.asInstanceOf[AvatarServiceResponse] + assert(reply2msg.toChannel.equals("/test/Avatar")) + assert(reply2msg.avatar_guid == Service.defaultPlayerGUID) + assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) + assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) + + expectNoMsg(200 milliseconds) + assert(zone.Corpses.isEmpty) + assert(!obj.HasGUID) + } + } +} + +object AvatarServiceTest { + import java.util.concurrent.atomic.AtomicInteger + private val number = new AtomicInteger(1) + + def TestName : String = { + s"service${number.getAndIncrement()}" + } }