diff --git a/README.md b/README.md index bcceb53c..67956db7 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,11 @@ In order to use scala, you need the compiler `scalac`. This is equivalent to Jav Install this on to your system and the compiler and Scala REPL will be added to your PATH. ### Downloading PSCrypto -The server requires PSCrypto in order to run. [Download the latest release](https://github.com/psforever/PSCrypto/releases/download/v1.1/pscrypto-lib-1.1.zip) and extract the ZIP in to the top level of your source directory. SBT, IDEA, and Java will automatically find the required libraries when running the server. +The server requires PSCrypto in order to run. [Download the latest release](https://github.com/psforever/PSCrypto/releases/download/v1.1/pscrypto-lib-1.1.zip) and extract the the approprate dll for your operating system to the top level of your source directory (the root directory, not /pslogin/src/main/scala). SBT, IDEA, and Java will automatically find the required libraries when running the server. If you are not comfortable with compiled binaries, you can [build the libraries yourself](https://github.com/psforever/PSCrypto). +If you have any issues with PSCrypto being detected when trying to run the server try adding `-Djava.library.path=` (no path necessary) to your preferred IDE's build configuration, for example with IDEA: Run -> Edit Configuration -> VM Options + ### Using an IDE Scala code can be fairly complex and a good IDE helps you understand the code and what methods are available for certain types. IntelliJ IDEA has some of the most mature support for Scala of any IDE today. It has advanced type introspection and excellent code completion. It's recommended for those who are new to Scala in order to get familiar with the syntax. diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala index 067dafec..05caf2be 100644 --- a/common/src/main/scala/net/psforever/objects/Avatar.scala +++ b/common/src/main/scala/net/psforever/objects/Avatar.scala @@ -3,6 +3,7 @@ package net.psforever.objects import net.psforever.objects.definition.{AvatarDefinition, ImplantDefinition} import net.psforever.objects.equipment.EquipmentSize +import net.psforever.objects.loadouts.Loadout import net.psforever.types.{CertificationType, CharacterGender, ImplantType, PlanetSideEmpire} import scala.annotation.tailrec @@ -16,7 +17,7 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : /** 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`. + * Unlike other objects, all `ImplantSlot` objects are already 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." @@ -24,9 +25,12 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : * @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 (inventory slot number five) */ + /** Loadouts
+ * 0-9 are Infantry loadouts + * 10-14 are Vehicle loadouts + */ + private val loadouts : Array[Option[Loadout]] = Array.fill[Option[Loadout]](15)(None) + /** Locker */ private val locker : LockerContainer = new LockerContainer() { override def toString : String = { s"$name's ${Definition.Name}" @@ -153,6 +157,12 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : } } + def SaveLoadout(owner : Vehicle, label : String, line : Int) : Unit = { + if(line > 9 && line < loadouts.length) { + loadouts(line) = Some(Loadout.Create(owner, label)) + } + } + def LoadLoadout(line : Int) : Option[Loadout] = loadouts.lift(line).getOrElse(None) def DeleteLoadout(line : Int) : Unit = { diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index fc511e8d..91f6c77e 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -524,6 +524,10 @@ object GlobalDefinitions { val medical_terminal = new MedicalTerminalDefinition(529) + val pad_landing = new RepairRearmSiloDefinition(719) + + val repair_silo = new RepairRearmSiloDefinition(729) + val spawn_pad = new VehicleSpawnPadDefinition val mb_locker = new LockerDefinition diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index f5642611..72246fb4 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -4,6 +4,7 @@ package net.psforever.objects 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.loadouts.Loadout import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.packet.game.PlanetSideGUID import net.psforever.types._ diff --git a/common/src/main/scala/net/psforever/objects/Vehicle.scala b/common/src/main/scala/net/psforever/objects/Vehicle.scala index 3aaa32eb..6dabd99a 100644 --- a/common/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/common/src/main/scala/net/psforever/objects/Vehicle.scala @@ -349,6 +349,39 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ def VisibleSlots : Set[Int] = weapons.keySet + override def Slot(slotNum : Int) : EquipmentSlot = { + weapons.get(slotNum) +// .orElse(utilities.get(slotNum) match { +// case Some(_) => +// //TODO what do now? +// None +// case None => ; +// None +// }) + .orElse(Some(Inventory.Slot(slotNum))).get + } + + override def Find(guid : PlanetSideGUID) : Option[Int] = { + weapons.find({ case (_, obj) => + obj.Equipment match { + case Some(item) => + if(item.HasGUID && item.GUID == guid) { + true + } + else { + false + } + case None => + false + } + }) match { + case Some((index, _)) => + Some(index) + case None => + Inventory.Find(guid) + } + } + /** * A reference to the `Vehicle` `Trunk` space. * @return this `Vehicle` `Trunk` diff --git a/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala new file mode 100644 index 00000000..5277f4ba --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala @@ -0,0 +1,102 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.loadouts + +import net.psforever.types.ExoSuitType + +/** + * A blueprint of a player's uniform, their holster items, and their inventory items, saved in a specific state. + * This previous state can be restored on any given player template + * by reconstructing the items (if permitted) and re-assigning the uniform (if available).
+ *
+ * The fifth tab on an `order_terminal` window is occupied by the list of "Favorite" `Loadout` blueprints. + * The ten-long list is initialized with `FavoritesMessage` packets assigned to the "Infantry" list. + * Specific entries are added or removed using `FavoritesRequest` packets, + * re-established using other conventional game packets. + * @param label the name by which this inventory will be known when displayed in a Favorites list; + * field gets inherited + * @param visible_slots simplified representation of the `Equipment` that can see "seen" on the target; + * field gets inherited + * @param inventory simplified representation of the `Equipment` in the target's inventory or trunk; + * field gets inherited + * @param exosuit the exo-suit in which the avatar will be dressed; + * may be restricted + * @param subtype the mechanized assault exo-suit specialization number that indicates whether the MAX performs: + * anti-infantry (1), anti-vehicular (2), or anti-air work (3); + * the default value is 0 + */ +final case class InfantryLoadout(label : String, + visible_slots : List[Loadout.SimplifiedEntry], + inventory : List[Loadout.SimplifiedEntry], + exosuit : ExoSuitType.Value, + subtype : Int) extends Loadout(label, visible_slots, inventory) + +object InfantryLoadout { + import net.psforever.objects.Player + import net.psforever.objects.GlobalDefinitions + import net.psforever.objects.equipment.Equipment + + /** + * The sub-type of the player's uniform. + * Applicable to mechanized assault units, mainly. + * The subtype is reported as a number but indicates the specialization - anti-infantry, ani-vehicular, anti-air - of the suit + * as indicated by the arm weapon(s). + * @param player the player + * @return the numeric subtype + */ + def DetermineSubtype(player : Player) : Int = { + DetermineSubtypeA(player.ExoSuit, player.Slot(0).Equipment) + } + + /** + * The sub-type of the player's uniform. + * Applicable to mechanized assault units, mainly. + * The subtype is reported as a number but indicates the specialization - anti-infantry, ani-vehicular, anti-air - of the suit + * as indicated by the arm weapon(s). + * @param suit the player's uniform; + * the target is for MAX armors + * @param weapon any weapon the player may have it his "first pistol slot;" + * to a MAX, that is its "primary weapon slot" + * @return the numeric subtype + */ + def DetermineSubtypeA(suit : ExoSuitType.Value, weapon : Option[Equipment]) : Int = { + if(suit == ExoSuitType.MAX) { + weapon match { + case Some(item) => + item.Definition match { + case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.nchev_scattercannon | GlobalDefinitions.vshev_quasar => + 1 + case GlobalDefinitions.trhev_pounder | GlobalDefinitions.nchev_falcon | GlobalDefinitions.vshev_comet => + 2 + case GlobalDefinitions.trhev_burster | GlobalDefinitions.nchev_sparrow | GlobalDefinitions.vshev_starfire => + 3 + case _ => + 0 + } + case None => + 0 + } + } + else { + 0 + } + } + + /** + * The sub-type of the player's uniform, as used in `FavoritesMessage`.
+ *
+ * The values for `Standard`, `Infiltration`, and the generic `MAX` are not perfectly known. + * The latter-most exo-suit option is presumed. + * @param suit the player's uniform + * @param subtype the mechanized assault exo-suit subtype as determined by their arm weapons + * @return the numeric subtype + */ + def DetermineSubtypeB(suit : ExoSuitType.Value, subtype : Int) : Int = { + suit match { + case ExoSuitType.Standard => 0 + case ExoSuitType.Agile => 1 + case ExoSuitType.Reinforced => 2 + case ExoSuitType.MAX => 3 + subtype //4, 5, 6 + case ExoSuitType.Infiltration => 7 + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/Loadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala similarity index 62% rename from common/src/main/scala/net/psforever/objects/Loadout.scala rename to common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala index ec997d0a..5ff4f92d 100644 --- a/common/src/main/scala/net/psforever/objects/Loadout.scala +++ b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala @@ -1,91 +1,42 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects +package net.psforever.objects.loadouts +import net.psforever.objects._ import net.psforever.objects.definition._ import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem -import net.psforever.types.ExoSuitType import scala.annotation.tailrec /** - * From a `Player` their current exo-suit and their `Equipment`, retain a set of instructions to reconstruct this arrangement.
+ * The base of all specific kinds of blueprint containers. + * This previous state can be restored on any appropriate template from which the loadout was copied + * by reconstructing the items (if permitted). + * The three fields are the name assigned to the loadout, + * the visible items that are created (which obey different rules depending on the source), + * and the concealed items that are created and added to the source's `Inventory`.
+ * For example, the `visible_slots` on a `Player`-borne loadout will transform into the form `Array[EquipmentSlot]`; + * `Vehicle`-originating loadouts transform into the form `Map[Int, Equipment]`. *
- * `Loadout` objects are composed of the following information, as if a blueprint:
- * - the avatar's current exo-suit
- * - the type of specialization, called a "subtype" (mechanized assault exo-suits only)
- * - the contents of the avatar's occupied holster slots
- * - the contents of the avatar's occupied inventory
- * `Equipment` contents of the holsters and of the formal inventory region will be condensed into a simplified form. - * These are also "blueprints." - * At its most basic, this simplification will merely comprise the former object's `EquipmentDefinition`. - * For items that are already simple - `Kit` objects and `SimpleItem` objects - this form will not be too far removed. - * For more complicated affairs like `Tool` objects and `AmmoBox` objects, only essential information will be retained.
- *
- * The deconstructed blueprint can be applied to any avatar. - * They are, however, typically tied to unique users and unique characters. - * For reasons of certifications, however, permissions on that avatar may affect what `Equipment` can be distributed. - * Even a whole blueprint can be denied if the user lacks the necessary exo-suit certification. - * A completely new piece of `Equipment` is constructed when the `Loadout` is regurgitated.
- *
- * The fifth tab on an `order_terminal` window is for "Favorite" blueprints for `Loadout` entries. - * The ten-long list is initialized with `FavoritesMessage` packets. + * The lists of user-specific loadouts are initialized with `FavoritesMessage` packets. * Specific entries are loaded or removed using `FavoritesRequest` packets. * @param label the name by which this inventory will be known when displayed in a Favorites list * @param visible_slots simplified representation of the `Equipment` that can see "seen" on the target * @param inventory simplified representation of the `Equipment` in the target's inventory or trunk - * @param exosuit na - * @param subtype na */ -final case class Loadout(private val label : String, - private val visible_slots : List[Loadout.SimplifiedEntry], - private val inventory : List[Loadout.SimplifiedEntry], - private val exosuit : ExoSuitType.Value, - private val subtype : Int) { - /** - * The label by which this `Loadout` is called. - * @return the label - */ - def Label : String = label - - /** - * The exo-suit in which the avatar will be dressed. - * Might be restricted and, thus, restrict the rest of the `Equipment` from being constructed and given. - * @return the exo-suit - */ - def ExoSuit : ExoSuitType.Value = exosuit - - /** - * The mechanized assault exo-suit specialization number that indicates whether the MAX performs: - * anti-infantry (1), - * anti-vehicular (2), - * or anti-air work (3). - * The major distinction is the type of arm weapons that MAX is equipped. - * When the blueprint doesn't call for a MAX, the number will be 0. - * @return the specialization number - */ - def Subtype : Int = subtype - - /** - * The `Equipment` in the `Player`'s holster slots when this `Loadout` is created. - * @return a `List` of the holster item blueprints - */ - def VisibleSlots : List[Loadout.SimplifiedEntry] = visible_slots - - /** - * The `Equipment` in the `Player`'s inventory region when this `Loadout` is created. - * @return a `List` of the inventory item blueprints - */ - def Inventory : List[Loadout.SimplifiedEntry] = inventory -} +abstract class Loadout(label : String, + visible_slots : List[Loadout.SimplifiedEntry], + inventory : List[Loadout.SimplifiedEntry]) object Loadout { - def apply(label : String, visible : List[SimplifiedEntry], inventory : List[SimplifiedEntry]) : Loadout = { - new Loadout(label, visible, inventory, ExoSuitType.Standard, 0) - } - + /** + * Produce the blueprint on a player. + * @param player the player + * @param label the name of this loadout + * @return an `InfantryLoadout` object populated with appropriate information about the current state of the player + */ def Create(player : Player, label : String) : Loadout = { - new Loadout( + InfantryLoadout( label, packageSimplifications(player.Holsters()), packageSimplifications(player.Inventory.Items.values.toList), @@ -94,11 +45,18 @@ object Loadout { ) } + /** + * Produce the blueprint of a vehicle. + * @param vehicle the vehicle + * @param label the name of this loadout + * @return a `VehicleLoadout` object populated with appropriate information about the current state of the vehicle + */ def Create(vehicle : Vehicle, label : String) : Loadout = { - Loadout( + VehicleLoadout( label, packageSimplifications(vehicle.Weapons.map({ case ((index, weapon)) => InventoryItem(weapon.Equipment.get, index) }).toList), - packageSimplifications(vehicle.Trunk.Items.values.toList) + packageSimplifications(vehicle.Trunk.Items.values.toList), + vehicle.Definition ) } @@ -152,35 +110,32 @@ object Loadout { */ final case class ShorthandKit(definition : KitDefinition) extends Simplification + /** + * The sub-type of the player's uniform. + * Applicable to mechanized assault units, mainly. + * The subtype is reported as a number but indicates the specialization - anti-infantry, ani-vehicular, anti-air - of the suit + * as indicated by the arm weapon(s). + * @param player the player + * @return the numeric subtype + */ def DetermineSubtype(player : Player) : Int = { - if(player.ExoSuit == ExoSuitType.MAX) { - player.Slot(0).Equipment match { - case Some(item) => - item.Definition match { - case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.nchev_scattercannon | GlobalDefinitions.vshev_quasar => - 1 - case GlobalDefinitions.trhev_pounder | GlobalDefinitions.nchev_falcon | GlobalDefinitions.vshev_comet => - 2 - case GlobalDefinitions.trhev_burster | GlobalDefinitions.nchev_sparrow | GlobalDefinitions.vshev_starfire => - 3 - case _ => - 0 - } - case None => - 0 - } - } - else { - 0 - } + InfantryLoadout.DetermineSubtype(player) } + /** + * The sub-type of the vehicle. + * Vehicle's have no subtype. + * @param vehicle the vehicle + * @return the numeric subtype, always 0 + */ + def DetermineSubtype(vehicle : Vehicle) : Int = 0 + /** * Overloaded entry point for constructing simplified blueprints from holster slot equipment. * @param equipment the holster slots * @return a `List` of simplified `Equipment` */ - private def packageSimplifications(equipment : Array[EquipmentSlot]) : List[SimplifiedEntry] = { + protected def packageSimplifications(equipment : Array[EquipmentSlot]) : List[SimplifiedEntry] = { recursiveHolsterSimplifications(equipment.iterator) } @@ -189,7 +144,7 @@ object Loadout { * @param equipment the enumerated contents of the inventory * @return a `List` of simplified `Equipment` */ - private def packageSimplifications(equipment : List[InventoryItem]) : List[SimplifiedEntry] = { + protected def packageSimplifications(equipment : List[InventoryItem]) : List[SimplifiedEntry] = { equipment.map(entry => { SimplifiedEntry(buildSimplification(entry.obj), entry.start) }) } diff --git a/common/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala new file mode 100644 index 00000000..5f7e0b63 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala @@ -0,0 +1,27 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.loadouts + +import net.psforever.objects.definition._ + +/** + * A blueprint of a vehicle's mounted weapons and its inventory items, saved in a specific state. + * This previous state can be restored on an apporpriate vehicle template + * by reconstructing the items (if permitted). + * Mismatched vehicles may produce no loadout or an imperfect loadout depending on specifications.
+ *
+ * The second tab on an `repair_silo` window is occupied by the list of "Favorite" `Loadout` blueprints. + * The five-long list is initialized with `FavoritesMessage` packets assigned to the "Vehicle" list. + * Specific entries are added or removed using `FavoritesRequest` packets, + * re-established using other conventional game packets. + * @param label the name by which this inventory will be known when displayed in a Favorites list; + * field gets inherited + * @param visible_slots simplified representation of the `Equipment` that can see "seen" on the target; + * field gets inherited + * @param inventory simplified representation of the `Equipment` in the target's inventory or trunk; + * field gets inherited + * @param vehicle_definition the original type of vehicle whose state is being populated + */ +final case class VehicleLoadout(label : String, + visible_slots : List[Loadout.SimplifiedEntry], + inventory : List[Loadout.SimplifiedEntry], + vehicle_definition : VehicleDefinition) extends Loadout(label, visible_slots, inventory) 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 2c062abb..9067ae34 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 @@ -4,6 +4,7 @@ package net.psforever.objects.serverobject.terminals import net.psforever.objects._ import net.psforever.objects.definition._ import net.psforever.objects.equipment.Equipment +import net.psforever.objects.loadouts.Loadout import net.psforever.packet.game.ItemTransactionMessage import net.psforever.types.ExoSuitType @@ -336,7 +337,7 @@ object EquipmentTerminalDefinition { * `TerminalDefinition.MakeKit` */ def BuildSimplifiedPattern(entry : Loadout.Simplification) : Equipment = { - import net.psforever.objects.Loadout._ + import net.psforever.objects.loadouts.Loadout._ entry match { case obj : ShorthandTool => val ammo : List[AmmoBoxDefinition] = obj.ammo.map(fmode => { fmode.ammo.definition }) diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/MedicalTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/MedicalTerminalDefinition.scala index fbd7f012..66581aac 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/MedicalTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/MedicalTerminalDefinition.scala @@ -1,18 +1,12 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals -import net.psforever.objects.Player -import net.psforever.packet.game.ItemTransactionMessage - /** * The definition for any `Terminal` that is of a type "medical_terminal". - * This includes the limited proximity-based functionality of the formal medical terminals - * and the actual proximity-based functionality of the cavern crystals.
- *
- * Do not confuse the "medical_terminal" category and the actual `medical_terminal` object (529). - * Objects created by this definition being linked by their use of `ProximityTerminalUseMessage` is more accurate. + * This includes the functionality of the formal medical terminals and some of the cavern crystals. + * Do not confuse the game's internal "medical_terminal" object category and the actual `medical_terminal` object (529). */ -class MedicalTerminalDefinition(objectId : Int) extends TerminalDefinition(objectId) { +class MedicalTerminalDefinition(objectId : Int) extends TerminalDefinition(objectId) with ProximityDefinition { Name = if(objectId == 38) { "adv_med_terminal" } @@ -31,6 +25,4 @@ class MedicalTerminalDefinition(objectId : Int) extends TerminalDefinition(objec else { throw new IllegalArgumentException("medical terminal must be either object id 38, 225, 226, 529, or 689") } - - def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal() } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalABDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalABDefinition.scala index 192d87dc..c90aca31 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalABDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalABDefinition.scala @@ -3,6 +3,7 @@ package net.psforever.objects.serverobject.terminals import akka.actor.ActorContext import net.psforever.objects.Player +import net.psforever.objects.loadouts.InfantryLoadout import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.structures.Amenity import net.psforever.packet.game.ItemTransactionMessage @@ -41,7 +42,7 @@ class OrderTerminalABDefinition(object_id : Int) extends EquipmentTerminalDefini override def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = buyFunc(player, msg) /** - * Process a `TransactionType.InfantryLoadout` action by the user. + * Process a `TransactionType.Loadout` action by the user. * `Loadout` objects are blueprints composed of exo-suit specifications and simplified `Equipment`-to-slot mappings. * If a valid loadout is found, its data is transformed back into actual `Equipment` for return to the user. * Loadouts that would suit the player into a mechanized assault exo-suit are not permitted. @@ -52,16 +53,16 @@ class OrderTerminalABDefinition(object_id : Int) extends EquipmentTerminalDefini override def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { if(msg.item_page == 4) { //Favorites tab player.LoadLoadout(msg.unk1) match { - case Some(loadout) => - if(loadout.ExoSuit != ExoSuitType.MAX) { - val holsters = loadout.VisibleSlots.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) - val inventory = loadout.Inventory.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) - Terminal.InfantryLoadout(loadout.ExoSuit, loadout.Subtype, holsters, inventory) + case Some(loadout : InfantryLoadout) => + if(loadout.exosuit != ExoSuitType.MAX) { + val holsters = loadout.visible_slots.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + val inventory = loadout.inventory.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + Terminal.InfantryLoadout(loadout.exosuit, loadout.subtype, holsters, inventory) } else { Terminal.NoDeal() } - case None => + case Some(_) | None => Terminal.NoDeal() } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala index a1bfca86..d9730bd6 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.terminals import net.psforever.objects.Player +import net.psforever.objects.loadouts.InfantryLoadout import net.psforever.objects.inventory.InventoryItem import net.psforever.packet.game.ItemTransactionMessage import net.psforever.objects.serverobject.terminals.EquipmentTerminalDefinition._ @@ -27,7 +28,7 @@ class OrderTerminalDefinition extends EquipmentTerminalDefinition(612) { override def Buy(player: Player, msg : ItemTransactionMessage) : Terminal.Exchange = buyFunc(player, msg) /** - * Process a `TransactionType.InfantryLoadout` action by the user. + * Process a `TransactionType.Loadout` action by the user. * `Loadout` objects are blueprints composed of exo-suit specifications and simplified `Equipment`-to-slot mappings. * If a valid loadout is found, its data is transformed back into actual `Equipment` for return to the user. * @param player the player @@ -37,11 +38,11 @@ class OrderTerminalDefinition extends EquipmentTerminalDefinition(612) { override def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { if(msg.item_page == 4) { //Favorites tab player.LoadLoadout(msg.unk1) match { - case Some(loadout) => - val holsters = loadout.VisibleSlots.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) - val inventory = loadout.Inventory.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) - Terminal.InfantryLoadout(loadout.ExoSuit, loadout.Subtype, holsters, inventory) - case None => + case Some(loadout : InfantryLoadout) => + val holsters = loadout.visible_slots.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + val inventory = loadout.inventory.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + Terminal.InfantryLoadout(loadout.exosuit, loadout.subtype, holsters, inventory) + case Some(_) | None => Terminal.NoDeal() } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityDefinition.scala new file mode 100644 index 00000000..f1ccfcc4 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityDefinition.scala @@ -0,0 +1,15 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.terminals + +import net.psforever.objects.Player +import net.psforever.packet.game.ItemTransactionMessage + +/** + * The definition for any `Terminal` that possesses a proximity-based effect. + * This includes the limited proximity-based functionality of the formal medical terminals + * and the actual proximity-based functionality of the cavern crystals. + * Objects created by this definition being linked by their use of `ProximityTerminalUseMessage`. + */ +trait ProximityDefinition { + def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal() +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminal.scala index c98e7da5..8b9b7e67 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminal.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminal.scala @@ -1,8 +1,6 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals -import net.psforever.packet.game.PlanetSideGUID - /** * A server object that is a "terminal" that can be accessed for amenities and services, * triggered when a certain distance from the unit itself (proximity-based).
@@ -11,28 +9,14 @@ import net.psforever.packet.game.PlanetSideGUID * For example, the cavern crystals are considered owner-neutral elements that are not attached to a `Building` object. * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ -class ProximityTerminal(tdef : MedicalTerminalDefinition) extends Terminal(tdef) { - private var users : Set[PlanetSideGUID] = Set.empty - - def NumberUsers : Int = users.size - - def AddUser(player_guid : PlanetSideGUID) : Int = { - users += player_guid - NumberUsers - } - - def RemoveUser(player_guid : PlanetSideGUID) : Int = { - users -= player_guid - NumberUsers - } -} +class ProximityTerminal(tdef : TerminalDefinition with ProximityDefinition) extends Terminal(tdef) with ProximityUnit object ProximityTerminal { /** * Overloaded constructor. * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ - def apply(tdef : MedicalTerminalDefinition) : ProximityTerminal = { + def apply(tdef : TerminalDefinition with ProximityDefinition) : ProximityTerminal = { new ProximityTerminal(tdef) } @@ -45,7 +29,7 @@ object ProximityTerminal { * @param context a context to allow the object to properly set up `ActorSystem` functionality * @return the `Terminal` object */ - def Constructor(tdef : MedicalTerminalDefinition)(id : Int, context : ActorContext) : Terminal = { + def Constructor(tdef : TerminalDefinition with ProximityDefinition)(id : Int, context : ActorContext) : Terminal = { import akka.actor.Props val obj = ProximityTerminal(tdef) obj.Actor = context.actorOf(Props(classOf[ProximityTerminalControl], obj), s"${tdef.Name}_$id") diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala index 407fd0cb..753cdb65 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala @@ -2,36 +2,24 @@ package net.psforever.objects.serverobject.terminals import akka.actor.Actor -import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} -import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage /** - * * An `Actor` that handles messages being dispatched to a specific `ProximityTerminal`. * Although this "terminal" itself does not accept the same messages as a normal `Terminal` object, * it returns the same type of messages - wrapped in a `TerminalMessage` - to the `sender`. * @param term the proximity unit (terminal) */ -class ProximityTerminalControl(term : ProximityTerminal) extends Actor with FactionAffinityBehavior.Check { +class ProximityTerminalControl(term : Terminal with ProximityUnit) extends Actor with FactionAffinityBehavior.Check with ProximityUnit.Use { def FactionObject : FactionAffinity = term - def receive : Receive = checkBehavior.orElse { - case CommonMessages.Use(player) => - val hadNoUsers = term.NumberUsers == 0 - if(term.AddUser(player.GUID) == 1 && hadNoUsers) { - sender ! TerminalMessage(player, null, Terminal.StartProximityEffect(term)) - } + def TerminalObject : Terminal with ProximityUnit = term - case CommonMessages.Unuse(player) => - val hadUsers = term.NumberUsers > 0 - if(term.RemoveUser(player.GUID) == 0 && hadUsers) { - sender ! TerminalMessage(player, null, Terminal.StopProximityEffect(term)) - } - - case _ => - sender ! Terminal.NoDeal() - } + def receive : Receive = checkBehavior + .orElse(proximityBehavior) + .orElse { + case _ => ; + } override def toString : String = term.Definition.Name } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityUnit.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityUnit.scala new file mode 100644 index 00000000..25843fa1 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityUnit.scala @@ -0,0 +1,60 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.terminals + +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage +import net.psforever.packet.game.PlanetSideGUID + +/** + * A server object that provides a service, triggered when a certain distance from the unit itself (proximity-based). + * Unlike conventional terminals, this one is not necessarily structure-owned. + * For example, the cavern crystals are considered owner-neutral elements that are not attached to a `Building` object. + */ +trait ProximityUnit { + this : Terminal => + + /** + * A list of targets that are currently affected by this proximity unit. + */ + private var targets : Set[PlanetSideGUID] = Set.empty + + def NumberUsers : Int = targets.size + + def AddUser(player_guid : PlanetSideGUID) : Int = { + targets += player_guid + NumberUsers + } + + def RemoveUser(player_guid : PlanetSideGUID) : Int = { + targets -= player_guid + NumberUsers + } +} + +object ProximityUnit { + import akka.actor.Actor + + /** + * A mixin `trait` for an `Actor`'s `PartialFunction` that handles messages, + * in this case handling messages that controls the telegraphed state of the `ProximityUnit` object as the number of users changes. + */ + trait Use { + this : Actor => + + def TerminalObject : Terminal with ProximityUnit + + val proximityBehavior : Receive = { + case CommonMessages.Use(player) => + val hadNoUsers = TerminalObject.NumberUsers == 0 + if(TerminalObject.AddUser(player.GUID) == 1 && hadNoUsers) { + sender ! TerminalMessage(player, null, Terminal.StartProximityEffect(TerminalObject)) + } + + case CommonMessages.Unuse(player) => + val hadUsers = TerminalObject.NumberUsers > 0 + if(TerminalObject.RemoveUser(player.GUID) == 0 && hadUsers) { + sender ! TerminalMessage(player, null, Terminal.StopProximityEffect(TerminalObject)) + } + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/RepairRearmSiloDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/RepairRearmSiloDefinition.scala new file mode 100644 index 00000000..f7463bf0 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/RepairRearmSiloDefinition.scala @@ -0,0 +1,44 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.terminals + +import net.psforever.objects.Player +import net.psforever.objects.inventory.InventoryItem +import net.psforever.objects.loadouts.VehicleLoadout +import net.psforever.objects.serverobject.terminals.EquipmentTerminalDefinition.BuildSimplifiedPattern +import net.psforever.packet.game.ItemTransactionMessage + +/** + * The `Definition` for any `Terminal` that is of a type "repair_silo." + * Has both proximity-based operation and direct access purchasing power. + */ +class RepairRearmSiloDefinition(objectId : Int) extends EquipmentTerminalDefinition(objectId) with ProximityDefinition { + Name = if(objectId == 719) { + "pad_landing" + } + else if(objectId == 729) { + "repair_silo" + } + else { + throw new IllegalArgumentException("repair re-arm terminal must be either object id 719 or 729") + } + + private val buyFunc : (Player, ItemTransactionMessage)=>Terminal.Exchange = EquipmentTerminalDefinition.Buy(Map.empty, Map.empty, Map.empty) + + override def Buy(player: Player, msg : ItemTransactionMessage) : Terminal.Exchange = buyFunc(player, msg) + + override def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { + if(msg.item_page == 4) { //Favorites tab + player.LoadLoadout(msg.unk1 + 10) match { + case Some(loadout : VehicleLoadout) => + val weapons = loadout.visible_slots.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + val inventory = loadout.inventory.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + Terminal.VehicleLoadout(loadout.vehicle_definition, weapons, inventory) + case _ => + Terminal.NoDeal() + } + } + else { + Terminal.NoDeal() + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala index b031a990..7a8676bb 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.terminals import net.psforever.objects.Player +import net.psforever.objects.definition.VehicleDefinition import net.psforever.objects.serverobject.structures.Amenity import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} import net.psforever.types.{TransactionType, Vector3} @@ -73,7 +74,7 @@ class Terminal(tdef : TerminalDefinition) extends Amenity { case TransactionType.Sell => tdef.Sell(player, msg) - case TransactionType.InfantryLoadout => + case TransactionType.Loadout => tdef.Loadout(player, msg) case _ => @@ -190,17 +191,19 @@ object Terminal { */ final case class InfantryLoadout(exosuit : ExoSuitType.Value, subtype : Int = 0, holsters : List[InventoryItem], inventory : List[InventoryItem]) extends Exchange + final case class VehicleLoadout(vehicle_definition : VehicleDefinition, weapons : List[InventoryItem], inventory : List[InventoryItem]) extends Exchange + /** * Start the special effects caused by a proximity-base service. * @param terminal the proximity-based unit */ - final case class StartProximityEffect(terminal : ProximityTerminal) extends Exchange + final case class StartProximityEffect(terminal : Terminal with ProximityUnit) extends Exchange /** * Stop the special effects caused by a proximity-base service. * @param terminal the proximity-based unit */ - final case class StopProximityEffect(terminal : ProximityTerminal) extends Exchange + final case class StopProximityEffect(terminal : Terminal with ProximityUnit) extends Exchange /** * Overloaded constructor. diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala index ad63babf..4efa324e 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala @@ -15,8 +15,7 @@ class TerminalControl(term : Terminal) extends Actor with FactionAffinityBehavio case Terminal.Request(player, msg) => sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg)) - case _ => - sender ! Terminal.NoDeal() + case _ => ; } override def toString : String = term.Definition.Name diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala index be739f4c..562c6c91 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala @@ -24,7 +24,7 @@ abstract class TerminalDefinition(objectId : Int) extends net.psforever.objects. def Sell(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal() /** - * The unimplemented functionality for this `Terminal`'s `TransactionType.InfantryLoadout` activity. + * The unimplemented functionality for this `Terminal`'s `TransactionType.Loadout` activity. */ def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal() } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/VehicleTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/VehicleTerminalDefinition.scala index a64fad15..8785ae7a 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/VehicleTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/VehicleTerminalDefinition.scala @@ -3,6 +3,7 @@ package net.psforever.objects.serverobject.terminals import net.psforever.objects.definition.VehicleDefinition import net.psforever.objects.{Player, Vehicle} +import net.psforever.objects.loadouts.VehicleLoadout import net.psforever.objects.inventory.InventoryItem import net.psforever.packet.game.ItemTransactionMessage @@ -83,7 +84,7 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition // "aphelion_flight" -> (()=>Unit) ) - import net.psforever.objects.{Loadout => _Loadout} //distinguish from Terminal.Loadout message + import net.psforever.objects.loadouts.{Loadout => _Loadout} //distinguish from Terminal.Loadout message import _Loadout._ /** * A `Map` of the default contents of a `Vehicle` inventory, called the trunk. @@ -101,29 +102,31 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition val ammo_flux = ShorthandAmmoBox(flux_cannon_thresher_battery, flux_cannon_thresher_battery.Capacity) val ammo_bomb = ShorthandAmmoBox(liberator_bomb, liberator_bomb.Capacity) Map( - //"quadstealth" -> _Loadout("default_quadstealth", List(), List()), - "quadassault" -> _Loadout("default_quadassault", List(), + //"quadstealth" -> VehicleLoadout("default_quadstealth", List(), List(), quadstealth), + "quadassault" -> VehicleLoadout("default_quadassault", List(), List( SimplifiedEntry(ammo_12mm, 30), SimplifiedEntry(ammo_12mm, 34), SimplifiedEntry(ammo_12mm, 74), SimplifiedEntry(ammo_12mm, 78) - ) + ), + quadassault ), { val ammo = ShorthandAmmoBox(hellfire_ammo, hellfire_ammo.Capacity) - "fury" -> _Loadout("default_fury", List(), + "fury" -> VehicleLoadout("default_fury", List(), List( SimplifiedEntry(ammo, 30), SimplifiedEntry(ammo, 34), SimplifiedEntry(ammo, 74), SimplifiedEntry(ammo, 78) - ) + ), + fury ) }, - //"ant" -> _Loadout("default_ant", List(), List()), - //"ams" -> _Loadout("default_ams", List(), List()), - "two_man_assault_buggy" -> _Loadout("default_two_man_assault_buggy", List(), + //"ant" -> VehicleLoadout("default_ant", List(), List(), ant), + //"ams" -> VehicleLoadout("default_ams", List(), List(), ams), + "two_man_assault_buggy" -> VehicleLoadout("default_two_man_assault_buggy", List(), List( SimplifiedEntry(ammo_12mm, 30), SimplifiedEntry(ammo_12mm, 34), @@ -131,11 +134,12 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_12mm, 90), SimplifiedEntry(ammo_12mm, 94), SimplifiedEntry(ammo_12mm, 98) - ) + ), + two_man_assault_buggy ), { val ammo = ShorthandAmmoBox(skyguard_flak_cannon_ammo, skyguard_flak_cannon_ammo.Capacity) - "skyguard" -> _Loadout("default_skyguard", List(), + "skyguard" -> VehicleLoadout("default_skyguard", List(), List( SimplifiedEntry(ammo_12mm, 30), SimplifiedEntry(ammo_12mm, 34), @@ -143,10 +147,11 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo, 90), SimplifiedEntry(ammo, 94), SimplifiedEntry(ammo, 98) - ) + ), + skyguard ) }, - "threemanheavybuggy" -> _Loadout("default_threemanheavybuggy", List(), + "threemanheavybuggy" -> VehicleLoadout("default_threemanheavybuggy", List(), List( SimplifiedEntry(ammo_12mm, 30), SimplifiedEntry(ammo_12mm, 34), @@ -154,11 +159,12 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_mortar, 90), SimplifiedEntry(ammo_mortar, 94), SimplifiedEntry(ammo_mortar, 98) - ) + ), + threemanheavybuggy ), { val ammo = ShorthandAmmoBox(firebird_missile, firebird_missile.Capacity) - "twomanheavybuggy" -> _Loadout("default_twomanheavybuggy", List(), + "twomanheavybuggy" -> VehicleLoadout("default_twomanheavybuggy", List(), List( SimplifiedEntry(ammo, 30), SimplifiedEntry(ammo, 34), @@ -166,10 +172,11 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo, 90), SimplifiedEntry(ammo, 94), SimplifiedEntry(ammo, 98) - ) + ), + twomanheavybuggy ) }, - "twomanhoverbuggy" -> _Loadout("default_twomanhoverbuggy", List(), + "twomanhoverbuggy" -> VehicleLoadout("default_twomanhoverbuggy", List(), List( SimplifiedEntry(ammo_flux, 30), SimplifiedEntry(ammo_flux, 34), @@ -177,9 +184,10 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_flux, 90), SimplifiedEntry(ammo_flux, 94), SimplifiedEntry(ammo_flux, 98) - ) + ), + twomanhoverbuggy ), - "mediumtransport" -> _Loadout("default_mediumtransport", List(), + "mediumtransport" -> VehicleLoadout("default_mediumtransport", List(), List( SimplifiedEntry(ammo_20mm, 30), SimplifiedEntry(ammo_20mm, 34), @@ -190,9 +198,10 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_20mm, 150), SimplifiedEntry(ammo_20mm, 154), SimplifiedEntry(ammo_20mm, 158) - ) + ), + mediumtransport ), - "battlewagon" -> _Loadout("default_battlewagon", List(), + "battlewagon" -> VehicleLoadout("default_battlewagon", List(), List( SimplifiedEntry(ammo_15mm, 30), SimplifiedEntry(ammo_15mm, 34), @@ -203,11 +212,12 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_15mm, 150), SimplifiedEntry(ammo_15mm, 154), SimplifiedEntry(ammo_15mm, 158) - ) + ), + battlewagon ), { val ammo = ShorthandAmmoBox(gauss_cannon_ammo, gauss_cannon_ammo.Capacity) - "thunderer" -> _Loadout("default_thunderer", List(), + "thunderer" -> VehicleLoadout("default_thunderer", List(), List( SimplifiedEntry(ammo, 30), SimplifiedEntry(ammo, 34), @@ -218,12 +228,13 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo, 150), SimplifiedEntry(ammo, 154), SimplifiedEntry(ammo, 158) - ) + ), + thunderer ) }, { val ammo = ShorthandAmmoBox(fluxpod_ammo, fluxpod_ammo.Capacity) - "aurora" -> _Loadout("default_aurora", List(), + "aurora" -> VehicleLoadout("default_aurora", List(), List( SimplifiedEntry(ammo, 30), SimplifiedEntry(ammo, 34), @@ -234,10 +245,11 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo, 150), SimplifiedEntry(ammo, 154), SimplifiedEntry(ammo, 158) - ) + ), + aurora ) }, - "apc_tr" -> _Loadout("default_apc_tr", List(), + "apc_tr" -> VehicleLoadout("default_apc_tr", List(), List( SimplifiedEntry(ammo_75mm, 30), SimplifiedEntry(ammo_75mm, 34), @@ -259,9 +271,10 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_15mm, 278), SimplifiedEntry(ammo_15mm, 282), SimplifiedEntry(ammo_15mm, 286) - ) + ), + apc_tr ), - "apc_nc" -> _Loadout("default_apc_nc", List(), + "apc_nc" -> VehicleLoadout("default_apc_nc", List(), List( SimplifiedEntry(ammo_75mm, 30), SimplifiedEntry(ammo_75mm, 34), @@ -283,9 +296,10 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_20mm, 278), SimplifiedEntry(ammo_20mm, 282), SimplifiedEntry(ammo_20mm, 286) - ) + ), + apc_nc ), - "apc_vs" -> _Loadout("default_apc_vs", List(), + "apc_vs" -> VehicleLoadout("default_apc_vs", List(), List( SimplifiedEntry(ammo_75mm, 30), SimplifiedEntry(ammo_75mm, 34), @@ -307,9 +321,10 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_flux, 278), SimplifiedEntry(ammo_flux, 282), SimplifiedEntry(ammo_flux, 286) - ) + ), + apc_vs ), - "lightning" -> _Loadout("default_lightning", List(), + "lightning" -> VehicleLoadout("default_lightning", List(), List( SimplifiedEntry(ammo_25mm, 30), SimplifiedEntry(ammo_25mm, 34), @@ -317,11 +332,12 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_75mm, 90), SimplifiedEntry(ammo_75mm, 94), SimplifiedEntry(ammo_75mm, 98) - ) + ), + lightning ), { val ammo = ShorthandAmmoBox(bullet_105mm, bullet_105mm.Capacity) - "prowler" -> _Loadout("default_prowler", List(), + "prowler" -> VehicleLoadout("default_prowler", List(), List( SimplifiedEntry(ammo_15mm, 30), SimplifiedEntry(ammo_15mm, 34), @@ -329,12 +345,13 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo, 90), SimplifiedEntry(ammo, 94), SimplifiedEntry(ammo, 98) - ) + ), + prowler ) }, { val ammo = ShorthandAmmoBox(bullet_150mm, bullet_150mm.Capacity) - "vanguard" -> _Loadout("default_vanguard", List(), + "vanguard" -> VehicleLoadout("default_vanguard", List(), List( SimplifiedEntry(ammo_20mm, 30), SimplifiedEntry(ammo_20mm, 34), @@ -342,13 +359,14 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo, 90), SimplifiedEntry(ammo, 94), SimplifiedEntry(ammo, 98) - ) + ), + vanguard ) }, { val ammo1 = ShorthandAmmoBox(pulse_battery, pulse_battery.Capacity) val ammo2 = ShorthandAmmoBox(heavy_rail_beam_battery, heavy_rail_beam_battery.Capacity) - "magrider" -> _Loadout("default_magrider", List(), + "magrider" -> VehicleLoadout("default_magrider", List(), List( SimplifiedEntry(ammo1, 30), SimplifiedEntry(ammo1, 34), @@ -356,23 +374,25 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo2, 90), SimplifiedEntry(ammo2, 94), SimplifiedEntry(ammo2, 98) - ) + ), + magrider ) }, - //"flail" -> _Loadout("default_flail", List(), List()), - //"switchblade" -> _Loadout("default_switchblade", List(), List()), - //"router" -> _Loadout("default_router", List(), List()), - "mosquito" -> _Loadout("default_mosquito", List(), + //"flail" -> VehicleLoadout("default_flail", List(), List(), flail), + //"switchblade" -> VehicleLoadout("default_switchblade", List(), List(), switchblade), + //"router" -> VehicleLoadout("default_router", List(), List(), router), + "mosquito" -> VehicleLoadout("default_mosquito", List(), List( SimplifiedEntry(ammo_12mm, 30), SimplifiedEntry(ammo_12mm, 34), SimplifiedEntry(ammo_12mm, 74), SimplifiedEntry(ammo_12mm, 78) - ) + ), + mosquito ), { val ammo = ShorthandAmmoBox(reaver_rocket, reaver_rocket.Capacity) - "lightgunship" -> _Loadout("default_lightgunship", List(), + "lightgunship" -> VehicleLoadout("default_lightgunship", List(), List( SimplifiedEntry(ammo, 30), SimplifiedEntry(ammo, 34), @@ -380,22 +400,24 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo, 90), SimplifiedEntry(ammo_20mm, 94), SimplifiedEntry(ammo_20mm, 98) - ) + ), + lightgunship ) }, { val ammo1 = ShorthandAmmoBox(wasp_rocket_ammo, wasp_rocket_ammo.Capacity) val ammo2 = ShorthandAmmoBox(wasp_gun_ammo, wasp_gun_ammo.Capacity) - "wasp" -> _Loadout("default_wasp", List(), + "wasp" -> VehicleLoadout("default_wasp", List(), List( SimplifiedEntry(ammo1, 30), SimplifiedEntry(ammo1, 34), SimplifiedEntry(ammo2, 74), SimplifiedEntry(ammo2, 78) - ) + ), + wasp ) }, - "liberator" -> _Loadout("default_liberator", List(), + "liberator" -> VehicleLoadout("default_liberator", List(), List( SimplifiedEntry(ammo_35mm, 30), SimplifiedEntry(ammo_35mm, 34), @@ -406,9 +428,10 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_bomb, 150), SimplifiedEntry(ammo_bomb, 154), SimplifiedEntry(ammo_bomb, 158) - ) + ), + liberator ), - "vulture" -> _Loadout("default_vulture", List(), + "vulture" -> VehicleLoadout("default_vulture", List(), List( SimplifiedEntry(ammo_35mm, 30), SimplifiedEntry(ammo_35mm, 34), @@ -418,9 +441,10 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_bomb, 98), SimplifiedEntry(ammo_bomb, 102), SimplifiedEntry(ammo_bomb, 106) - ) //TODO confirm + ), //TODO confirm + vulture ), - "dropship" -> _Loadout("default_dropship", List(), + "dropship" -> VehicleLoadout("default_dropship", List(), List( SimplifiedEntry(ammo_20mm, 30), SimplifiedEntry(ammo_20mm, 34), @@ -434,9 +458,10 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_20mm, 162), SimplifiedEntry(ammo_20mm, 166), SimplifiedEntry(ammo_20mm, 170) - ) + ), + dropship ), - "galaxy_gunship" -> _Loadout("galaxy_gunship", List(), + "galaxy_gunship" -> VehicleLoadout("galaxy_gunship", List(), List( SimplifiedEntry(ammo_35mm, 30), SimplifiedEntry(ammo_35mm, 34), @@ -450,10 +475,11 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition SimplifiedEntry(ammo_mortar, 178), SimplifiedEntry(ammo_mortar, 182), SimplifiedEntry(ammo_mortar, 186) - ) + ), + galaxy_gunship ) - //"phantasm" -> _Loadout("default_phantasm", List(), List()), - //"lodestar" -> _Loadout("default_lodestar", List(), List()), + //"phantasm" -> VehicleLoadout("default_phantasm", List(), List(), phantasm), + //"lodestar" -> VehicleLoadout("default_lodestar", List(), List(), lodestar), ) } @@ -461,12 +487,12 @@ abstract class VehicleTerminalDefinition(objId : Int) extends TerminalDefinition vehicles.get(msg.item_name) match { case Some(vehicle) => val (weapons, inventory) = trunk.get(msg.item_name) match { - case Some(loadout) => + case Some(loadout : VehicleLoadout) => ( - loadout.VisibleSlots.map(entry => { InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index) }), - loadout.Inventory.map(entry => { InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index) }) + loadout.visible_slots.map(entry => { InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index) }), + loadout.inventory.map(entry => { InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index) }) ) - case None => + case _ => (List.empty, List.empty) } Terminal.BuyVehicle(vehicle(), weapons, inventory) diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index e7ebd353..9d586476 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -312,7 +312,7 @@ object GamePacketOpcode extends Enumeration { = Value private def noDecoder(opcode : GamePacketOpcode.Type) = (a : BitVector) => - Attempt.failure(Err(s"Could not find a marshaller for game packet ${opcode}")) + Attempt.failure(Err(s"Could not find a marshaller for game packet $opcode")) /// Mapping of packet IDs to decoders. Notice that we are using the @switch annotation which ensures that the Scala /// compiler will be able to optimize this as a lookup table (switch statement). Microbenchmarks show a nearly 400x @@ -549,7 +549,7 @@ object GamePacketOpcode extends Enumeration { // OPCODES 0xc0-cf case 0xc0 => noDecoder(CaptureFlagUpdateMessage) case 0xc1 => noDecoder(VanuModuleUpdateMessage) - case 0xc2 => noDecoder(FacilityBenefitShieldChargeRequestMessage) + case 0xc2 => game.FacilityBenefitShieldChargeRequestMessage.decode case 0xc3 => game.ProximityTerminalUseMessage.decode case 0xc4 => game.QuantityDeltaUpdateMessage.decode case 0xc5 => noDecoder(ChainLashMessage) @@ -608,7 +608,7 @@ object GamePacketOpcode extends Enumeration { case 0xf1 => game.MailMessage.decode case 0xf2 => noDecoder(GameVarUpdate) case 0xf3 => noDecoder(ClientCheatedMessage) - case default => noDecoder(opcode) + case _ => noDecoder(opcode) } implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint8L) diff --git a/common/src/main/scala/net/psforever/packet/game/FacilityBenefitShieldChargeRequestMessage.scala b/common/src/main/scala/net/psforever/packet/game/FacilityBenefitShieldChargeRequestMessage.scala new file mode 100644 index 00000000..6b1d6110 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/FacilityBenefitShieldChargeRequestMessage.scala @@ -0,0 +1,24 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * Dispatched by the client when driving a vehicle in the sphere of influence of an allied base + * that is an amp station facility or that possesses the lattice-connected benefit of an amp station. + * The vehicle that is being driven will not have perfect fully-charged shields at the time. + * @param vehicle_guid the vehicle whose shield is being charged + */ +final case class FacilityBenefitShieldChargeRequestMessage(vehicle_guid : PlanetSideGUID) + extends PlanetSideGamePacket { + type Packet = FacilityBenefitShieldChargeRequestMessage + def opcode = GamePacketOpcode.FacilityBenefitShieldChargeRequestMessage + def encode = FacilityBenefitShieldChargeRequestMessage.encode(this) +} + +object FacilityBenefitShieldChargeRequestMessage extends Marshallable[FacilityBenefitShieldChargeRequestMessage] { + implicit val codec : Codec[FacilityBenefitShieldChargeRequestMessage] = + ("vehicle_guid" | PlanetSideGUID.codec).as[FacilityBenefitShieldChargeRequestMessage] +} diff --git a/common/src/main/scala/net/psforever/packet/game/FavoritesMessage.scala b/common/src/main/scala/net/psforever/packet/game/FavoritesMessage.scala index 7fa3d32d..3e28c027 100644 --- a/common/src/main/scala/net/psforever/packet/game/FavoritesMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/FavoritesMessage.scala @@ -2,6 +2,7 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.LoadoutType import scodec.Codec import scodec.codecs._ import shapeless.{::, HNil} @@ -18,12 +19,6 @@ import shapeless.{::, HNil} * Infantry equipment favorites are appended with a code for the type of exo-suit that they will load on a player. * This does not match the same two field numbering system as in `ArmorChangedMessage` packets.
*
- * Lists:
- * ` - * 0 - Equipment Terminal (infantry)
- * 1 - Repair/Rearm Silo (standard vehicles)
- * ` - *
* Armors:
* ` * 1 - Agile
@@ -33,13 +28,7 @@ import shapeless.{::, HNil} * 6 - AV MAX
* ` *
- * Exploration 1:
- * The identifier for the list is two bits so four separated lists of `Favorites` are supportable. - * Two of the lists are common enough and we can assume one of the others is related to Battleframe Robotics. - * These lists also do not include `Squad Defintion...` presets. - * What are the unknown lists?
- *
- * Exploration 2:
+ * Exploration:
* There are three unaccounted exo-suit indices - 0, 3, and 7; * and, there are two specific kinds of exo-suit that are not defined - Infiltration and Standard. * It is possible that one of the indices also defines the generic MAX (see `ArmorChangedMessage`). @@ -50,11 +39,11 @@ import shapeless.{::, HNil} * @param label the identifier for this entry * @param armor the type of exo-suit, if an Infantry loadout */ -final case class FavoritesMessage(list : Int, +final case class FavoritesMessage(list : LoadoutType.Value, player_guid : PlanetSideGUID, line : Int, label : String, - armor : Option[Int] = None) + armor : Option[Int]) extends PlanetSideGamePacket { type Packet = FavoritesMessage def opcode = GamePacketOpcode.FavoritesMessage @@ -62,12 +51,36 @@ final case class FavoritesMessage(list : Int, } object FavoritesMessage extends Marshallable[FavoritesMessage] { - implicit val codec : Codec[FavoritesMessage] = ( - ("list" | uint2L) >>:~ { value => + /** + * Overloaded constructor, for infantry loadouts specifically. + * @param list the destination list + * @param player_guid the player + * @param line the zero-indexed line number of this entry in its list + * @param label the identifier for this entry + * @param armor the type of exo-suit, if an Infantry loadout + * @return a `FavoritesMessage` object + */ + def apply(list : LoadoutType.Value, player_guid : PlanetSideGUID, line : Int, label : String, armor : Int) : FavoritesMessage = { + FavoritesMessage(list, player_guid, line, label, Some(armor)) + } + + /** + * Overloaded constructor, for vehicle loadouts specifically. + * @param list the destination list + * @param player_guid the player + * @param line the zero-indexed line number of this entry in its list + * @param label the identifier for this entry + * @return a `FavoritesMessage` object + */ + def apply(list : LoadoutType.Value, player_guid : PlanetSideGUID, line : Int, label : String) : FavoritesMessage = { + FavoritesMessage(list, player_guid, line, label, None) + } +implicit val codec : Codec[FavoritesMessage] = ( + ("list" | LoadoutType.codec) >>:~ { value => ("player_guid" | PlanetSideGUID.codec) :: ("line" | uint4L) :: ("label" | PacketHelpers.encodedWideStringAligned(2)) :: - conditional(value == 0, "armor" | uintL(3)) + conditional(value == LoadoutType.Infantry, "armor" | uintL(3)) }).xmap[FavoritesMessage] ( { case lst :: guid :: ln :: str :: arm :: HNil => @@ -75,7 +88,7 @@ object FavoritesMessage extends Marshallable[FavoritesMessage] { }, { case FavoritesMessage(lst, guid, ln, str, arm) => - val armset : Option[Int] = if(lst == 0 && arm.isEmpty) { Some(0) } else { arm } + val armset : Option[Int] = if(lst == LoadoutType.Infantry && arm.isEmpty) { Some(0) } else { arm } lst :: guid :: ln :: str :: armset :: HNil } ) diff --git a/common/src/main/scala/net/psforever/packet/game/FavoritesRequest.scala b/common/src/main/scala/net/psforever/packet/game/FavoritesRequest.scala index 3d9af568..ac3ccb10 100644 --- a/common/src/main/scala/net/psforever/packet/game/FavoritesRequest.scala +++ b/common/src/main/scala/net/psforever/packet/game/FavoritesRequest.scala @@ -2,21 +2,33 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.LoadoutType import scodec.Codec import scodec.codecs._ object FavoritesAction extends Enumeration { type Type = Value - val Unknown, - Save, - Delete = Value + val + Unknown, + Save, + Delete + = Value implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L) } +/** + * na + * @param player_guid the player + * @param list na + * @param action the behavior of this packet + * @param line what line of the applicable loadout ("Saved Favorites") list is modified + * @param label applicable when a load out is being saved; + * this is the string that will be displayed in the list of loadouts on that line + */ final case class FavoritesRequest(player_guid : PlanetSideGUID, - unk : Int, + list : LoadoutType.Value, action : FavoritesAction.Value, line : Int, label : Option[String]) @@ -29,7 +41,7 @@ final case class FavoritesRequest(player_guid : PlanetSideGUID, object FavoritesRequest extends Marshallable[FavoritesRequest] { implicit val codec : Codec[FavoritesRequest] = ( ("player_guid" | PlanetSideGUID.codec) :: - ("unk" | uint2L) :: + ("list" | LoadoutType.codec) :: (("action" | FavoritesAction.codec) >>:~ { action => ("line" | uint4L) :: conditional(action == FavoritesAction.Save, "label" | PacketHelpers.encodedWideString) diff --git a/common/src/main/scala/net/psforever/types/LoadoutType.scala b/common/src/main/scala/net/psforever/types/LoadoutType.scala new file mode 100644 index 00000000..ad180e77 --- /dev/null +++ b/common/src/main/scala/net/psforever/types/LoadoutType.scala @@ -0,0 +1,16 @@ +// Copyright (c) 2017 PSForever +package net.psforever.types + +import net.psforever.packet.PacketHelpers +import scodec.codecs.uint2L + +object LoadoutType extends Enumeration { + type Type = Value + + val + Infantry, + Vehicle + = Value + + implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L) +} diff --git a/common/src/main/scala/net/psforever/types/TransactionType.scala b/common/src/main/scala/net/psforever/types/TransactionType.scala index bf76e025..4fee2ec1 100644 --- a/common/src/main/scala/net/psforever/types/TransactionType.scala +++ b/common/src/main/scala/net/psforever/types/TransactionType.scala @@ -12,7 +12,7 @@ object TransactionType extends Enumeration { Sell, // or forget on certif term Unk4, Unk5, - InfantryLoadout, + Loadout, Unk7 = Value diff --git a/common/src/test/scala/game/FacilityBenefitShieldChargeRequestMessageTest.scala b/common/src/test/scala/game/FacilityBenefitShieldChargeRequestMessageTest.scala new file mode 100644 index 00000000..71b885c0 --- /dev/null +++ b/common/src/test/scala/game/FacilityBenefitShieldChargeRequestMessageTest.scala @@ -0,0 +1,28 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class FacilityBenefitShieldChargeRequestMessageTest extends Specification { + val string = hex"C2 4C00" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case FacilityBenefitShieldChargeRequestMessage(guid) => + guid mustEqual PlanetSideGUID(76) + case _ => + ko + } + } + + "encode" in { + val msg = FacilityBenefitShieldChargeRequestMessage(PlanetSideGUID(76)) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} + diff --git a/common/src/test/scala/game/FavoritesMessageTest.scala b/common/src/test/scala/game/FavoritesMessageTest.scala index 4033eb82..660d088d 100644 --- a/common/src/test/scala/game/FavoritesMessageTest.scala +++ b/common/src/test/scala/game/FavoritesMessageTest.scala @@ -4,6 +4,7 @@ package game import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ +import net.psforever.types.LoadoutType import scodec.bits._ class FavoritesMessageTest extends Specification { @@ -13,7 +14,7 @@ class FavoritesMessageTest extends Specification { "decode (for infantry)" in { PacketCoding.DecodePacket(stringInfantry).require match { case FavoritesMessage(list, player_guid, line, label, armor) => - list mustEqual 0 + list mustEqual LoadoutType.Infantry player_guid mustEqual PlanetSideGUID(3760) line mustEqual 0 label mustEqual "Agile (basic)" @@ -25,7 +26,7 @@ class FavoritesMessageTest extends Specification { } "encode (for infantry)" in { - val msg = FavoritesMessage(0, PlanetSideGUID(3760), 0, "Agile (basic)", Option(1)) + val msg = FavoritesMessage(LoadoutType.Infantry, PlanetSideGUID(3760), 0, "Agile (basic)", 1) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringInfantry @@ -34,7 +35,7 @@ class FavoritesMessageTest extends Specification { "decode (for vehicles)" in { PacketCoding.DecodePacket(stringVehicles).require match { case FavoritesMessage(list, player_guid, line, label, armor) => - list mustEqual 1 + list mustEqual LoadoutType.Vehicle player_guid mustEqual PlanetSideGUID(4210) line mustEqual 0 label mustEqual "Skyguard" @@ -45,7 +46,7 @@ class FavoritesMessageTest extends Specification { } "encode (for vehicles)" in { - val msg = FavoritesMessage(1, PlanetSideGUID(4210), 0, "Skyguard") + val msg = FavoritesMessage(LoadoutType.Vehicle, PlanetSideGUID(4210), 0, "Skyguard") val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringVehicles diff --git a/common/src/test/scala/game/FavoritesRequestTest.scala b/common/src/test/scala/game/FavoritesRequestTest.scala index 23adfccc..ca66f047 100644 --- a/common/src/test/scala/game/FavoritesRequestTest.scala +++ b/common/src/test/scala/game/FavoritesRequestTest.scala @@ -4,6 +4,7 @@ package game import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ +import net.psforever.types.LoadoutType import scodec.bits._ class FavoritesRequestTest extends Specification { @@ -11,9 +12,9 @@ class FavoritesRequestTest extends Specification { "decode (for infantry)" in { PacketCoding.DecodePacket(stringInfantry).require match { - case FavoritesRequest(player_guid, unk, action, line, label) => + case FavoritesRequest(player_guid, list, action, line, label) => player_guid mustEqual PlanetSideGUID(75) - unk mustEqual 0 + list mustEqual LoadoutType.Infantry action mustEqual FavoritesAction.Save line mustEqual 1 label.isDefined mustEqual true @@ -24,7 +25,7 @@ class FavoritesRequestTest extends Specification { } "encode (for infantry)" in { - val msg = FavoritesRequest(PlanetSideGUID(75), 0, FavoritesAction.Save, 1, Some("Example")) + val msg = FavoritesRequest(PlanetSideGUID(75), LoadoutType.Infantry, FavoritesAction.Save, 1, Some("Example")) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual stringInfantry diff --git a/common/src/test/scala/objects/AvatarTest.scala b/common/src/test/scala/objects/AvatarTest.scala index 95fb698f..6c331798 100644 --- a/common/src/test/scala/objects/AvatarTest.scala +++ b/common/src/test/scala/objects/AvatarTest.scala @@ -3,6 +3,7 @@ package objects import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects._ +import net.psforever.objects.loadouts._ import net.psforever.objects.definition.ImplantDefinition import net.psforever.types.{CharacterGender, ImplantType, PlanetSideEmpire} import org.specs2.mutable._ @@ -291,13 +292,13 @@ class AvatarTest extends Specification { 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 + case Some(items : InfantryLoadout) => + items.label mustEqual "test" + items.exosuit mustEqual obj.ExoSuit + items.subtype mustEqual 0 - items.VisibleSlots.length mustEqual 3 - val holsters = items.VisibleSlots.sortBy(_.index) + items.visible_slots.length mustEqual 3 + val holsters = items.visible_slots.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 @@ -307,8 +308,8 @@ class AvatarTest extends Specification { 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) + 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 @@ -321,7 +322,7 @@ class AvatarTest extends Specification { 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 => + case _ => ko } } @@ -347,13 +348,13 @@ class AvatarTest extends Specification { 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 => + case Some(items : InfantryLoadout) => + items.label mustEqual "test" + items.exosuit mustEqual obj.ExoSuit + items.subtype mustEqual 0 + items.visible_slots.length mustEqual 3 + items.inventory.length mustEqual 0 //empty + case _ => ko } } @@ -366,13 +367,13 @@ class AvatarTest extends Specification { 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 => + case Some(items : InfantryLoadout) => + items.label mustEqual "test" + items.exosuit mustEqual obj.ExoSuit + items.subtype mustEqual 0 + items.visible_slots.length mustEqual 0 //empty + items.inventory.length mustEqual 6 + case _ => ko } } diff --git a/common/src/test/scala/objects/LoadoutTest.scala b/common/src/test/scala/objects/LoadoutTest.scala index fb01d696..67d0f310 100644 --- a/common/src/test/scala/objects/LoadoutTest.scala +++ b/common/src/test/scala/objects/LoadoutTest.scala @@ -2,6 +2,7 @@ package objects import net.psforever.objects._ +import net.psforever.objects.loadouts._ import net.psforever.types.{CharacterGender, ExoSuitType, PlanetSideEmpire} import net.psforever.objects.GlobalDefinitions._ import org.specs2.mutable._ @@ -36,14 +37,14 @@ class LoadoutTest extends Specification { "create a loadout that contains a player's inventory" in { val player = CreatePlayer() - val obj = Loadout.Create(player, "test") + val obj = Loadout.Create(player, "test").asInstanceOf[InfantryLoadout] - obj.Label mustEqual "test" - obj.ExoSuit mustEqual obj.ExoSuit - obj.Subtype mustEqual 0 + obj.label mustEqual "test" + obj.exosuit mustEqual ExoSuitType.Standard + obj.subtype mustEqual 0 - obj.VisibleSlots.length mustEqual 3 - val holsters = obj.VisibleSlots.sortBy(_.index) + obj.visible_slots.length mustEqual 3 + val holsters = obj.visible_slots.sortBy(_.index) holsters.head.index mustEqual 0 holsters.head.item.asInstanceOf[Loadout.ShorthandTool].definition mustEqual beamer holsters(1).index mustEqual 2 @@ -51,8 +52,8 @@ class LoadoutTest extends Specification { holsters(2).index mustEqual 4 holsters(2).item.asInstanceOf[Loadout.ShorthandTool].definition mustEqual forceblade - obj.Inventory.length mustEqual 5 - val inventory = obj.Inventory.sortBy(_.index) + 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 @@ -65,26 +66,86 @@ class LoadoutTest extends Specification { inventory(4).item.asInstanceOf[Loadout.ShorthandSimpleItem].definition mustEqual remote_electronics_kit } + "create a loadout that contains a vehicle's inventory" in { + val vehicle = Vehicle(mediumtransport) + vehicle.Inventory += 30 -> AmmoBox(bullet_9mm) + vehicle.Inventory += 33 -> AmmoBox(bullet_9mm_AP) + val obj = Loadout.Create(vehicle, "test").asInstanceOf[VehicleLoadout] + + obj.label mustEqual "test" + obj.vehicle_definition mustEqual mediumtransport + + obj.visible_slots.length mustEqual 2 + val holsters = obj.visible_slots.sortBy(_.index) + holsters.head.index mustEqual 5 + holsters.head.item.asInstanceOf[Loadout.ShorthandTool].definition mustEqual mediumtransport_weapon_systemA + holsters(1).index mustEqual 6 + holsters(1).item.asInstanceOf[Loadout.ShorthandTool].definition mustEqual mediumtransport_weapon_systemB + + obj.inventory.length mustEqual 2 + val inventory = obj.inventory.sortBy(_.index) + inventory.head.index mustEqual 30 + inventory.head.item.asInstanceOf[Loadout.ShorthandAmmoBox].definition mustEqual bullet_9mm + inventory(1).index mustEqual 33 + inventory(1).item.asInstanceOf[Loadout.ShorthandAmmoBox].definition mustEqual bullet_9mm_AP + } + "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) - val ldout1 = Loadout.Create(player, "weaponless") + val ldout1 = Loadout.Create(player, "weaponless").asInstanceOf[InfantryLoadout] slot.Equipment = None slot.Equipment = Tool(trhev_dualcycler) - val ldout2 = Loadout.Create(player, "cycler") + val ldout2 = Loadout.Create(player, "cycler").asInstanceOf[InfantryLoadout] slot.Equipment = None slot.Equipment = Tool(trhev_pounder) - val ldout3 = Loadout.Create(player, "pounder") + val ldout3 = Loadout.Create(player, "pounder").asInstanceOf[InfantryLoadout] slot.Equipment = None slot.Equipment = Tool(trhev_burster) - val ldout4 = Loadout.Create(player, "burster") + val ldout4 = Loadout.Create(player, "burster").asInstanceOf[InfantryLoadout] - ldout1.Subtype mustEqual 0 - ldout2.Subtype mustEqual 1 - ldout3.Subtype mustEqual 2 - ldout4.Subtype mustEqual 3 + ldout1.subtype mustEqual 0 + ldout2.subtype mustEqual 1 + ldout3.subtype mustEqual 2 + ldout4.subtype mustEqual InfantryLoadout.DetermineSubtype(player) //example + } + + "players have additional uniform subtype" 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.ExoSuit = ExoSuitType.Standard + val ldout0 = Loadout.Create(player, "standard").asInstanceOf[InfantryLoadout] + player.ExoSuit = ExoSuitType.Agile + val ldout1 = Loadout.Create(player, "agile").asInstanceOf[InfantryLoadout] + player.ExoSuit = ExoSuitType.Reinforced + val ldout2 = Loadout.Create(player, "rein").asInstanceOf[InfantryLoadout] + player.ExoSuit = ExoSuitType.Infiltration + val ldout7 = Loadout.Create(player, "inf").asInstanceOf[InfantryLoadout] + + Player.SuitSetup(player, ExoSuitType.MAX) + val ldout3 = Loadout.Create(player, "weaponless").asInstanceOf[InfantryLoadout] + slot.Equipment = None + slot.Equipment = Tool(trhev_dualcycler) + val ldout4 = Loadout.Create(player, "cycler").asInstanceOf[InfantryLoadout] + slot.Equipment = None + slot.Equipment = Tool(trhev_pounder) + val ldout5 = Loadout.Create(player, "pounder").asInstanceOf[InfantryLoadout] + slot.Equipment = None + slot.Equipment = Tool(trhev_burster) + val ldout6 = Loadout.Create(player, "burster").asInstanceOf[InfantryLoadout] + + InfantryLoadout.DetermineSubtypeB(ldout0.exosuit, ldout0.subtype) mustEqual 0 + InfantryLoadout.DetermineSubtypeB(ldout1.exosuit, ldout1.subtype) mustEqual 1 + InfantryLoadout.DetermineSubtypeB(ldout2.exosuit, ldout2.subtype) mustEqual 2 + InfantryLoadout.DetermineSubtypeB(ldout3.exosuit, ldout3.subtype) mustEqual 3 + InfantryLoadout.DetermineSubtypeB(ldout4.exosuit, ldout4.subtype) mustEqual 4 + InfantryLoadout.DetermineSubtypeB(ldout5.exosuit, ldout5.subtype) mustEqual 5 + InfantryLoadout.DetermineSubtypeB(ldout6.exosuit, ldout6.subtype) mustEqual 6 + InfantryLoadout.DetermineSubtypeB(ldout7.exosuit, ldout7.subtype) mustEqual 7 } } diff --git a/common/src/test/scala/objects/VehicleTest.scala b/common/src/test/scala/objects/VehicleTest.scala index 0124917d..e0495c4a 100644 --- a/common/src/test/scala/objects/VehicleTest.scala +++ b/common/src/test/scala/objects/VehicleTest.scala @@ -277,6 +277,38 @@ class VehicleTest extends Specification { filteredMap(0).UtilType mustEqual UtilityType.order_terminalb filteredMap(2).UtilType mustEqual UtilityType.order_terminalb } + + "access its mounted weapons by Slot" in { + val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + harasser_vehicle.Weapons(2).Equipment.get.GUID = PlanetSideGUID(10) + + harasser_vehicle.Slot(2).Equipment.get.GUID mustEqual PlanetSideGUID(10) + } + + "access its trunk by Slot" in { + val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + val ammobox = AmmoBox(GlobalDefinitions.armor_canister) + ammobox.GUID = PlanetSideGUID(10) + harasser_vehicle.Inventory += 30 -> ammobox + + harasser_vehicle.Slot(30).Equipment.get.GUID mustEqual PlanetSideGUID(10) + } + + "find its mounted weapons by GUID" in { + val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + harasser_vehicle.Weapons(2).Equipment.get.GUID = PlanetSideGUID(10) + + harasser_vehicle.Find(PlanetSideGUID(10)) mustEqual Some(2) + } + + "find items in its trunk by GUID" in { + val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + val ammobox = AmmoBox(GlobalDefinitions.armor_canister) + ammobox.GUID = PlanetSideGUID(10) + harasser_vehicle.Inventory += 30 -> ammobox + + harasser_vehicle.Find(PlanetSideGUID(10)) mustEqual Some(30) + } } } diff --git a/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala b/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala index c4d44e14..ee4b4a47 100644 --- a/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala +++ b/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala @@ -68,10 +68,10 @@ class OrderTerminalABTest extends Specification { player.ExoSuit = ExoSuitType.MAX avatar.SaveLoadout(player, "test2", 1) - val msg1 = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.InfantryLoadout, 4, "", 0, PlanetSideGUID(0)) + val msg1 = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Loadout, 4, "", 0, PlanetSideGUID(0)) terminal.Request(player, msg1) mustEqual Terminal.InfantryLoadout(ExoSuitType.Standard, 0, Nil, Nil) - val msg2 = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.InfantryLoadout, 4, "", 1, PlanetSideGUID(0)) + val msg2 = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Loadout, 4, "", 1, PlanetSideGUID(0)) terminal.Request(player, msg2) mustEqual Terminal.NoDeal() } } diff --git a/common/src/test/scala/objects/terminal/OrderTerminalTest.scala b/common/src/test/scala/objects/terminal/OrderTerminalTest.scala index 024c8794..76e30ba1 100644 --- a/common/src/test/scala/objects/terminal/OrderTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/OrderTerminalTest.scala @@ -72,12 +72,54 @@ class OrderTerminalTest extends Specification { terminal.Request(player, msg) mustEqual Terminal.NoDeal() } - //TODO loudout tests - "player can not buy equipment from the wrong page ('9mmbullet_AP', page 1)" in { val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 1, "9mmbullet_AP", 0, PlanetSideGUID(0)) terminal.Request(player, msg) mustEqual Terminal.NoDeal() } + + "player can retrieve an infantry loadout" in { + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(avatar) + player2.ExoSuit = ExoSuitType.Agile + player2.Slot(0).Equipment = Tool(GlobalDefinitions.beamer) + player2.Slot(6).Equipment = Tool(GlobalDefinitions.beamer) + avatar.SaveLoadout(player2, "test", 0) + + val msg = terminal.Request(player2, ItemTransactionMessage(PlanetSideGUID(10), TransactionType.Loadout, 4, "", 0, PlanetSideGUID(0))) + msg.isInstanceOf[Terminal.InfantryLoadout] mustEqual true + val loadout = msg.asInstanceOf[Terminal.InfantryLoadout] + loadout.exosuit mustEqual ExoSuitType.Agile + loadout.subtype mustEqual 0 + loadout.holsters.size mustEqual 1 + loadout.holsters.head.obj.Definition mustEqual GlobalDefinitions.beamer + loadout.holsters.head.start mustEqual 0 + loadout.inventory.head.obj.Definition mustEqual GlobalDefinitions.beamer + loadout.inventory.head.start mustEqual 6 + } + + "player can not retrieve an infantry loadout from the wrong page" in { + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(avatar) + player2.ExoSuit = ExoSuitType.Agile + player2.Slot(0).Equipment = Tool(GlobalDefinitions.beamer) + player2.Slot(6).Equipment = Tool(GlobalDefinitions.beamer) + avatar.SaveLoadout(player2, "test", 0) + + val msg = terminal.Request(player2, ItemTransactionMessage(PlanetSideGUID(10), TransactionType.Loadout, 3, "", 0, PlanetSideGUID(0))) //page 3 + msg.isInstanceOf[Terminal.NoDeal] mustEqual true + } + + "player can not retrieve an infantry loadout from the wrong line" in { + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(avatar) + player2.ExoSuit = ExoSuitType.Agile + player2.Slot(0).Equipment = Tool(GlobalDefinitions.beamer) + player2.Slot(6).Equipment = Tool(GlobalDefinitions.beamer) + avatar.SaveLoadout(player2, "test", 0) + + val msg = terminal.Request(player2, ItemTransactionMessage(PlanetSideGUID(10), TransactionType.Loadout, 4, "", 1, PlanetSideGUID(0))) + msg.isInstanceOf[Terminal.NoDeal] mustEqual true + } } } diff --git a/common/src/test/scala/objects/terminal/ProximityTerminalControlTest.scala b/common/src/test/scala/objects/terminal/ProximityTerminalControlTest.scala index f817be6a..a28235fe 100644 --- a/common/src/test/scala/objects/terminal/ProximityTerminalControlTest.scala +++ b/common/src/test/scala/objects/terminal/ProximityTerminalControlTest.scala @@ -25,8 +25,7 @@ class ProximityTerminalControl2Test extends ActorTest() { val (_, terminal) = TerminalControlTest.SetUpAgents(GlobalDefinitions.medical_terminal, PlanetSideEmpire.TR) terminal.Actor !"hello" - val reply = receiveOne(Duration.create(500, "ms")) - assert(reply.isInstanceOf[Terminal.NoDeal]) + expectNoMsg(Duration.create(500, "ms")) } } diff --git a/common/src/test/scala/objects/terminal/ProximityTest.scala b/common/src/test/scala/objects/terminal/ProximityTest.scala new file mode 100644 index 00000000..7b799d1d --- /dev/null +++ b/common/src/test/scala/objects/terminal/ProximityTest.scala @@ -0,0 +1,170 @@ +// Copyright (c) 2017 PSForever +package objects.terminal + +import akka.actor.Props +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage +import net.psforever.objects.serverobject.terminals.{ProximityTerminal, ProximityTerminalControl, ProximityUnit, Terminal} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import objects.ActorTest +import org.specs2.mutable.Specification + +import scala.concurrent.duration._ + +class ProximityTest extends Specification { + "ProximityUnit" should { + "construct (with a Terminal object)" in { + val obj = new ProximityTest.SampleTerminal() + obj.NumberUsers mustEqual 0 + } + + "keep track of users (add)" in { + val obj = new ProximityTest.SampleTerminal() + obj.NumberUsers mustEqual 0 + obj.AddUser(PlanetSideGUID(10)) mustEqual obj.NumberUsers + obj.NumberUsers mustEqual 1 + obj.AddUser(PlanetSideGUID(20)) mustEqual obj.NumberUsers + obj.NumberUsers mustEqual 2 + } + + "keep track of users (remove)" in { + val obj = new ProximityTest.SampleTerminal() + obj.AddUser(PlanetSideGUID(10)) + obj.AddUser(PlanetSideGUID(20)) + obj.NumberUsers mustEqual 2 + obj.RemoveUser(PlanetSideGUID(10)) mustEqual obj.NumberUsers + obj.NumberUsers mustEqual 1 + obj.RemoveUser(PlanetSideGUID(20)) mustEqual obj.NumberUsers + obj.NumberUsers mustEqual 0 + } + + "can not add a user twice" in { + val obj = new ProximityTest.SampleTerminal() + obj.AddUser(PlanetSideGUID(10)) + obj.NumberUsers mustEqual 1 + obj.AddUser(PlanetSideGUID(10)) + obj.NumberUsers mustEqual 1 + } + + "can not remove a user that was not added" in { + val obj = new ProximityTest.SampleTerminal() + obj.AddUser(PlanetSideGUID(10)) + obj.NumberUsers mustEqual 1 + obj.RemoveUser(PlanetSideGUID(20)) + obj.NumberUsers mustEqual 1 + } + } + + "ProximityTerminal" should { + "construct" in { + ProximityTerminal(GlobalDefinitions.medical_terminal) + ok + } + } +} + +class ProximityTerminalControl1bTest extends ActorTest { + "ProximityTerminalControl" should { + "send out a start message" in { + val obj = ProximityTerminal(GlobalDefinitions.medical_terminal) + obj.Actor = system.actorOf(Props(classOf[ProximityTerminalControl], obj), "prox-ctrl") + val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + player.GUID = PlanetSideGUID(10) + + assert(obj.NumberUsers == 0) + obj.Actor ! CommonMessages.Use(player) + val msg = receiveOne(200 milliseconds) + assert(obj.NumberUsers == 1) + assert(msg.isInstanceOf[TerminalMessage]) + val msgout = msg.asInstanceOf[TerminalMessage] + assert(msgout.player == player) + assert(msgout.msg == null) + assert(msgout.response.isInstanceOf[Terminal.StartProximityEffect]) + } + } +} + +class ProximityTerminalControl2bTest extends ActorTest { + "ProximityTerminalControl" should { + "will not send out one start message unless first user" in { + val obj = ProximityTerminal(GlobalDefinitions.medical_terminal) + obj.Actor = system.actorOf(Props(classOf[ProximityTerminalControl], obj), "prox-ctrl") + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + player1.GUID = PlanetSideGUID(10) + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + player2.GUID = PlanetSideGUID(11) + assert(obj.NumberUsers == 0) + + obj.Actor ! CommonMessages.Use(player1) + val msg = receiveOne(200 milliseconds) + assert(obj.NumberUsers == 1) + assert(msg.isInstanceOf[TerminalMessage]) + assert(msg.asInstanceOf[TerminalMessage].response.isInstanceOf[Terminal.StartProximityEffect]) + obj.Actor ! CommonMessages.Use(player2) + expectNoMsg(500 milliseconds) + assert(obj.NumberUsers == 2) + } + } +} + +class ProximityTerminalControl3bTest extends ActorTest { + "ProximityTerminalControl" should { + "send out a stop message" in { + val obj = ProximityTerminal(GlobalDefinitions.medical_terminal) + obj.Actor = system.actorOf(Props(classOf[ProximityTerminalControl], obj), "prox-ctrl") + val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + player.GUID = PlanetSideGUID(10) + + assert(obj.NumberUsers == 0) + obj.Actor ! CommonMessages.Use(player) + receiveOne(200 milliseconds) + assert(obj.NumberUsers == 1) + obj.Actor ! CommonMessages.Unuse(player) + val msg = receiveOne(200 milliseconds) + assert(obj.NumberUsers == 0) + assert(msg.isInstanceOf[TerminalMessage]) + val msgout = msg.asInstanceOf[TerminalMessage] + assert(msgout.player == player) + assert(msgout.msg == null) + assert(msgout.response.isInstanceOf[Terminal.StopProximityEffect]) + } + } +} + +class ProximityTerminalControl4bTest extends ActorTest { + "ProximityTerminalControl" should { + "will not send out one stop message until last user" in { + val obj = ProximityTerminal(GlobalDefinitions.medical_terminal) + obj.Actor = system.actorOf(Props(classOf[ProximityTerminalControl], obj), "prox-ctrl") + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + player1.GUID = PlanetSideGUID(10) + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + player2.GUID = PlanetSideGUID(11) + assert(obj.NumberUsers == 0) + + obj.Actor ! CommonMessages.Use(player1) + receiveOne(200 milliseconds) //StartProximityEffect + assert(obj.NumberUsers == 1) + obj.Actor ! CommonMessages.Use(player2) + expectNoMsg(500 milliseconds) + assert(obj.NumberUsers == 2) + obj.Actor ! CommonMessages.Unuse(player1) + expectNoMsg(500 milliseconds) + assert(obj.NumberUsers == 1) + obj.Actor ! CommonMessages.Unuse(player2) + val msg = receiveOne(200 milliseconds) + assert(obj.NumberUsers == 0) + assert(msg.isInstanceOf[TerminalMessage]) + val msgout = msg.asInstanceOf[TerminalMessage] + assert(msgout.player == player2) + assert(msgout.msg == null) + assert(msgout.response.isInstanceOf[Terminal.StopProximityEffect]) + } + } +} + +object ProximityTest { + class SampleTerminal extends Terminal(GlobalDefinitions.dropship_vehicle_terminal) with ProximityUnit +} diff --git a/common/src/test/scala/objects/terminal/RepairRearmSiloTest.scala b/common/src/test/scala/objects/terminal/RepairRearmSiloTest.scala new file mode 100644 index 00000000..516e834c --- /dev/null +++ b/common/src/test/scala/objects/terminal/RepairRearmSiloTest.scala @@ -0,0 +1,91 @@ +// Copyright (c) 2017 PSForever +package objects.terminal + +import akka.actor.ActorRef +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects._ +import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} +import net.psforever.types.{CharacterGender, PlanetSideEmpire, TransactionType} +import org.specs2.mutable.Specification + +class RepairRearmSiloTest extends Specification { + "RepairRearmSilo" should { + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val silo = Terminal(GlobalDefinitions.repair_silo) + silo.Owner = new Building(0, Zone.Nowhere, StructureType.Building) + silo.Owner.Faction = PlanetSideEmpire.TR + + "define" in { + GlobalDefinitions.repair_silo.ObjectId mustEqual 729 + } + + "construct" in { + val obj = Terminal(GlobalDefinitions.repair_silo) + obj.Actor mustEqual ActorRef.noSender + } + + "player can buy a box of ammunition ('bullet_35mm')" in { + val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 3, "35mmbullet", 0, PlanetSideGUID(0)) + val reply = silo.Request(player, msg) + reply.isInstanceOf[Terminal.BuyEquipment] mustEqual true + val reply2 = reply.asInstanceOf[Terminal.BuyEquipment] + reply2.item.isInstanceOf[AmmoBox] mustEqual true + reply2.item.asInstanceOf[AmmoBox].Definition mustEqual GlobalDefinitions.bullet_35mm + reply2.item.asInstanceOf[AmmoBox].Capacity mustEqual 100 + } + + "player can not buy fake equipment ('sabot')" in { + val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 3, "sabot", 0, PlanetSideGUID(0)) + + silo.Request(player, msg) mustEqual Terminal.NoDeal() + } + + "player can not buy equipment from the wrong page ('35mmbullet', page 1)" in { + val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 1, "35mmbullet", 0, PlanetSideGUID(0)) + + silo.Request(player, msg) mustEqual Terminal.NoDeal() + } + + "player can retrieve a vehicle loadout" in { + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(avatar) + val vehicle = Vehicle(GlobalDefinitions.fury) + vehicle.Slot(30).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm) + avatar.SaveLoadout(vehicle, "test", 10) + + val msg = silo.Request(player2, ItemTransactionMessage(PlanetSideGUID(10), TransactionType.Loadout, 4, "", 0, PlanetSideGUID(0))) + msg.isInstanceOf[Terminal.VehicleLoadout] mustEqual true + val loadout = msg.asInstanceOf[Terminal.VehicleLoadout] + loadout.vehicle_definition mustEqual GlobalDefinitions.fury + loadout.weapons.size mustEqual 1 + loadout.weapons.head.obj.Definition mustEqual GlobalDefinitions.fury_weapon_systema + loadout.weapons.head.start mustEqual 1 + loadout.inventory.head.obj.Definition mustEqual GlobalDefinitions.bullet_9mm + loadout.inventory.head.start mustEqual 30 + } + + "player can not retrieve a vehicle loadout from the wrong line" in { + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(avatar) + val vehicle = Vehicle(GlobalDefinitions.fury) + vehicle.Slot(30).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm) + avatar.SaveLoadout(vehicle, "test", 10) + + val msg = silo.Request(player2, ItemTransactionMessage(PlanetSideGUID(10), TransactionType.Loadout, 3, "", 0, PlanetSideGUID(0))) //page 3 + msg.isInstanceOf[Terminal.NoDeal] mustEqual true + } + + "player can not retrieve a vehicle loadout from the wrong line" in { + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player(avatar) + val vehicle = Vehicle(GlobalDefinitions.fury) + vehicle.Slot(30).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm) + avatar.SaveLoadout(vehicle, "test", 10) + + val msg = silo.Request(player2, ItemTransactionMessage(PlanetSideGUID(10), TransactionType.Loadout, 4, "", 1, PlanetSideGUID(0))) //line 11 + msg.isInstanceOf[Terminal.NoDeal] mustEqual true + } + } +} diff --git a/common/src/test/scala/objects/terminal/TerminalControlTest.scala b/common/src/test/scala/objects/terminal/TerminalControlTest.scala index 428d8d87..e8432d3d 100644 --- a/common/src/test/scala/objects/terminal/TerminalControlTest.scala +++ b/common/src/test/scala/objects/terminal/TerminalControlTest.scala @@ -26,8 +26,7 @@ class TerminalControl2Test extends ActorTest() { val (_, terminal) = TerminalControlTest.SetUpAgents(GlobalDefinitions.cert_terminal, PlanetSideEmpire.TR) terminal.Actor !"hello" - val reply = receiveOne(Duration.create(500, "ms")) - assert(reply.isInstanceOf[Terminal.NoDeal]) + expectNoMsg(Duration.create(500, "ms")) } } diff --git a/pslogin/src/main/scala/LoginSessionActor.scala b/pslogin/src/main/scala/LoginSessionActor.scala index 6260563c..55e234d6 100644 --- a/pslogin/src/main/scala/LoginSessionActor.scala +++ b/pslogin/src/main/scala/LoginSessionActor.scala @@ -128,7 +128,7 @@ class LoginSessionActor extends Actor with MDCContextAware { if(token.isDefined) log.info(s"New login UN:$username Token:${token.get}. $clientVersion") else - log.info(s"New login UN:$username PW:$password. $clientVersion") + log.info(s"New login UN:$username. $clientVersion") // This is temporary until a schema has been developed //val loginSucceeded = accountLookup(username, password.getOrElse(token.get)) diff --git a/pslogin/src/main/scala/Maps.scala b/pslogin/src/main/scala/Maps.scala index 4051208c..4c750b62 100644 --- a/pslogin/src/main/scala/Maps.scala +++ b/pslogin/src/main/scala/Maps.scala @@ -110,9 +110,21 @@ object Maps { LocalObject(1576, Terminal.Constructor(order_terminal)) LocalObject(1577, Terminal.Constructor(order_terminal)) LocalObject(1578, Terminal.Constructor(order_terminal)) + LocalObject(1744, ProximityTerminal.Constructor(pad_landing)) //air pad A + LocalObject(1745, Terminal.Constructor(pad_landing)) //air pad A + LocalObject(1747, ProximityTerminal.Constructor(pad_landing)) //air pad B + LocalObject(1748, Terminal.Constructor(pad_landing)) //air pad B + LocalObject(1756, ProximityTerminal.Constructor(pad_landing)) //air pad C + LocalObject(1757, Terminal.Constructor(pad_landing)) //air pad C + LocalObject(1765, ProximityTerminal.Constructor(pad_landing)) //air pad D + LocalObject(1766, Terminal.Constructor(pad_landing)) //air pad D 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(2049, ProximityTerminal.Constructor(repair_silo)) //repair terminal A + LocalObject(2050, Terminal.Constructor(repair_silo)) //rearm terminal A + LocalObject(2061, ProximityTerminal.Constructor(repair_silo)) //repair terminal B + LocalObject(2062, Terminal.Constructor(repair_silo)) //rearm terminal B LocalObject(2239, Terminal.Constructor(spawn_terminal)) LocalObject(2244, Terminal.Constructor(spawn_terminal)) LocalObject(2245, Terminal.Constructor(spawn_terminal)) @@ -214,6 +226,18 @@ object Maps { ObjectToBuilding(1576, 2) ObjectToBuilding(1577, 2) ObjectToBuilding(1578, 2) + ObjectToBuilding(1744, 2) + ObjectToBuilding(1745, 2) + ObjectToBuilding(1747, 2) + ObjectToBuilding(1748, 2) + ObjectToBuilding(1756, 2) + ObjectToBuilding(1757, 2) + ObjectToBuilding(1765, 2) + ObjectToBuilding(1766, 2) + ObjectToBuilding(2049, 2) + ObjectToBuilding(2050, 2) + ObjectToBuilding(2061, 2) + ObjectToBuilding(2062, 2) ObjectToBuilding(2145, 2) ObjectToBuilding(2146, 2) ObjectToBuilding(2147, 2) @@ -351,6 +375,10 @@ object Maps { LocalObject(1591, Terminal.Constructor(order_terminal)) LocalObject(1592, Terminal.Constructor(order_terminal)) LocalObject(1593, Terminal.Constructor(order_terminal)) + LocalObject(1846, ProximityTerminal.Constructor(pad_landing)) //air pad S + LocalObject(1847, Terminal.Constructor(pad_landing)) //air pad S + LocalObject(1849, ProximityTerminal.Constructor(pad_landing)) //air pad N + LocalObject(1850, Terminal.Constructor(pad_landing)) //air pad N 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 @@ -374,6 +402,10 @@ object Maps { ObjectToBuilding(1591, 49) ObjectToBuilding(1592, 49) ObjectToBuilding(1593, 49) + ObjectToBuilding(1846, 49) + ObjectToBuilding(1847, 49) + ObjectToBuilding(1849, 49) + ObjectToBuilding(1850, 49) ObjectToBuilding(2156, 49) ObjectToBuilding(2157, 49) ObjectToBuilding(2333, 49) @@ -428,6 +460,8 @@ object Maps { def Building2() : Unit = { //HART building C LocalBuilding(2, FoundationBuilder(Building.Structure(StructureType.Building))) + LocalObject(12, ProximityTerminal.Constructor(repair_silo)) //repair terminal A + LocalObject(13, Terminal.Constructor(repair_silo)) //rearm terminal A //ItemTransaction: ItemTransactionMessage(PlanetSideGUID(2050),Buy,3,25mmbullet,0,PlanetSideGUID(0)) LocalObject(186, Terminal.Constructor(cert_terminal)) LocalObject(187, Terminal.Constructor(cert_terminal)) LocalObject(188, Terminal.Constructor(cert_terminal)) @@ -475,6 +509,8 @@ object Maps { 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(12, 2) + ObjectToBuilding(13, 2) ObjectToBuilding(186, 2) ObjectToBuilding(187, 2) ObjectToBuilding(188, 2) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 3b1ce7b1..bc4d076f 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -9,12 +9,14 @@ import scodec.Attempt.{Failure, Successful} import scodec.bits._ import org.log4s.MDC import MDCContextAware.Implicits._ +import csr.{CSRWarp, CSRZone, Traveler} 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.loadouts._ import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} import net.psforever.objects.serverobject.mount.Mountable @@ -28,8 +30,7 @@ import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} import net.psforever.objects.serverobject.pad.process.{AutoDriveControls, VehicleSpawnControlGuided} import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate} -import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, ProximityTerminal, Terminal} -import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.vehicles.{AccessPermissionGroup, Utility, VehicleLockState} @@ -39,6 +40,7 @@ import net.psforever.types._ import services._ import services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse} import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} +import services.vehicle.VehicleAction.UnstowEquipment import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} import scala.annotation.tailrec @@ -70,6 +72,7 @@ class WorldSessionActor extends Actor with MDCContextAware { var usingProximityTerminal : Set[PlanetSideGUID] = Set.empty var delayedProximityTerminalResets : Map[PlanetSideGUID, Cancellable] = Map.empty var controlled : Option[Int] = None //keep track of avatar's ServerVehicleOverride state + var traveler : Traveler = null var clientKeepAlive : Cancellable = DefaultCancellable.obj var progressBarUpdate : Cancellable = DefaultCancellable.obj @@ -488,6 +491,11 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + case VehicleResponse.InventoryState2(obj_guid, parent_guid, value) => + if(tplayer_guid != guid) { + sendResponse(InventoryStateMessage(obj_guid, 0, parent_guid, value)) + } + 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(tplayer_guid != guid) { @@ -829,7 +837,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) => //TODO optimizations against replacing Equipment with the exact same Equipment and potentially for recycling existing Equipment log.info(s"$tplayer wants to change equipment loadout to their option #${msg.unk1 + 1}") - sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.InfantryLoadout, true)) + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, true)) val dropPred = DropPredicate(tplayer) val (dropHolsters, beforeHolsters) = clearHolsters(tplayer.Holsters().iterator).partition(dropPred) val (dropInventory, beforeInventory) = tplayer.Inventory.Clear().partition(dropPred) @@ -848,7 +856,7 @@ class WorldSessionActor extends Actor with MDCContextAware { taskResolver ! GUIDTask.UnregisterEquipment(elem.obj)(continent.GUID) }) //report change - sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, 0)) + sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, subtype)) avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype)) sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 4, tplayer.Armor)) avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, tplayer.Armor)) @@ -892,7 +900,53 @@ class WorldSessionActor extends Actor with MDCContextAware { val objDef = obj.Definition avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, objDef.ObjectId, obj.GUID, objDef.Packet.ConstructorData(obj).get)) }) - sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.InfantryLoadout, true)) + + case Terminal.VehicleLoadout(definition, weapons, inventory) => + log.info(s"$tplayer wants to change their vehicle equipment loadout to their option #${msg.unk1 + 1}") + FindLocalVehicle match { + case Some(vehicle) => + sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Loadout, true)) + val (_, afterInventory) = inventory.partition( DropPredicate(tplayer) ) //dropped items are lost + //remove old inventory + val deleteEquipment : (Int,Equipment)=>Unit = DeleteEquipmentFromVehicle(vehicle) + vehicle.Inventory.Clear().foreach({ case InventoryItem(obj, index) => deleteEquipment(index, obj) }) + val stowEquipment : (Int,Equipment)=>TaskResolver.GiveTask = StowNewEquipmentInVehicle(vehicle) + (if(vehicle.Definition == definition) { + //vehicles are the same type; transfer over weapon ammo + //TODO ammo switching? no vehicle weapon does that currently but ... + //TODO want to completely swap weapons, but holster icon vanishes temporarily after swap + //TODO BFR arms must be swapped properly + val channel = s"${vehicle.Actor}" + weapons.foreach({ case InventoryItem(obj, index) => + val savedWeapon = obj.asInstanceOf[Tool] + val existingWeapon = vehicle.Weapons(index).Equipment.get.asInstanceOf[Tool] + (0 until existingWeapon.MaxAmmoSlot).foreach({ index => + val existingBox = existingWeapon.AmmoSlots(index).Box + existingBox.Capacity = savedWeapon.AmmoSlots(index).Box.Capacity + //use VehicleAction.InventoryState2; VehicleAction.InventoryState temporarily glitches ammo count in ui + vehicleService ! VehicleServiceMessage(channel, VehicleAction.InventoryState2(PlanetSideGUID(0), existingBox.GUID, existingWeapon.GUID, existingBox.Capacity)) + }) + }) + afterInventory + } + else { + //do not transfer over weapon ammo + if(vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset) { + afterInventory + } + else { + //accommodate as much of inventory as possible + //TODO map x,y -> x,y rather than reorganize items + val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) //dropped items can be forgotten + stow + } + }).foreach({ case InventoryItem(obj, index) => + taskResolver ! stowEquipment(index, obj) + }) + case None => + log.error(s"can not apply the loadout - can not find a vehicle") + sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Loadout, false)) + } case Terminal.LearnCertification(cert, cost) => if(!tplayer.Certifications.contains(cert)) { @@ -1054,6 +1108,12 @@ class WorldSessionActor extends Actor with MDCContextAware { case VehicleSpawnPad.StartPlayerSeatedInVehicle(vehicle, pad) => val vehicle_guid = vehicle.GUID + PlayerActionsToCancel() + if(player.VisibleSlots.contains(player.DrawnSlot)) { + player.DrawnSlot = Player.HandsDownSlot + sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, true)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectHeld(player.GUID, player.LastDrawnSlot)) + } sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 1L)) //mount points off? sendResponse(PlanetsideAttributeMessage(vehicle_guid, 21, player.GUID.guid)) //fte and ownership? @@ -1063,7 +1123,7 @@ class WorldSessionActor extends Actor with MDCContextAware { vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(vehicle_guid) } sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on? - //sendResponse(PlanetsideAttributeMessage(vehicle_guid, 0, vehicle.Definition.MaxHealth))) + //sendResponse(PlanetsideAttributeMessage(vehicle_guid, 0, 10))//vehicle.Definition.MaxHealth)) sendResponse(PlanetsideAttributeMessage(vehicle_guid, 68, 0L)) //??? sendResponse(PlanetsideAttributeMessage(vehicle_guid, 113, 0L)) //??? ReloadVehicleAccessPermissions(vehicle) @@ -1147,6 +1207,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val popNC = poplist.count(_.faction == PlanetSideEmpire.NC) val popVS = poplist.count(_.faction == PlanetSideEmpire.VS) + StartBundlingPackets() zone.Buildings.foreach({ case(id, building) => initBuilding(continentNumber, id, building) }) sendResponse(ZonePopulationUpdateMessage(continentNumber, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO)) sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.NEUTRAL)) @@ -1231,7 +1292,9 @@ class WorldSessionActor extends Actor with MDCContextAware { RequestSanctuaryZoneSpawn(player, zone_number) case InterstellarCluster.ClientInitializationComplete() => + StopBundlingPackets() LivePlayerList.Add(sessionId, avatar) + traveler = new Traveler(self, continent.Id) //PropertyOverrideMessage sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1)) sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list @@ -1280,6 +1343,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case SetCurrentAvatar(tplayer) => player = tplayer val guid = tplayer.GUID + StartBundlingPackets() sendResponse(SetCurrentAvatarMessage(guid,0,0)) sendResponse(PlayerStateShiftMessage(ShiftState(1, tplayer.Position, tplayer.Orientation.z))) if(spectator) { @@ -1312,6 +1376,7 @@ class WorldSessionActor extends Actor with MDCContextAware { //SquadDefinitionActionMessage and SquadDetailDefinitionUpdateMessage //MapObjectStateBlockMessage and ObjectCreateMessage //TacticsMessage + StopBundlingPackets() sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on @@ -1432,9 +1497,10 @@ class WorldSessionActor extends Actor with MDCContextAware { 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.Position = Vector3(3940.3984f, 4343.625f, 266.45312f) + 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 @@ -1477,6 +1543,8 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ BeginZoningMessage() => log.info("Reticulating splines ...") + traveler.zone = continent.Id + StartBundlingPackets() configZone(continent) sendResponse(TimeOfDayMessage(1191182336)) @@ -1550,6 +1618,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case _ => ; } }) + StopBundlingPackets() avatarService ! Service.Join(player.Continent) localService ! Service.Join(player.Continent) vehicleService ! Service.Join(player.Continent) @@ -1706,6 +1775,33 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + CSRZone.read(traveler, msg) match { + case (true, zone, pos) => + if(player.isAlive) { + player.Die //die to suspend client-driven position change updates + PlayerActionsToCancel() + player.Position = pos + traveler.zone = zone + continent.Population ! Zone.Population.Release(avatar) + continent.Population ! Zone.Population.Leave(avatar) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, player.GUID)) + taskResolver ! TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(player)(continent.GUID), zone) + } + + case (false, _, _) => ; + } + + CSRWarp.read(traveler, msg) match { + case (true, pos) => + if(player.isAlive) { + PlayerActionsToCancel() + sendResponse(PlayerStateShiftMessage(ShiftState(0, pos, player.Orientation.z, None))) + player.Position = pos + } + + case (false, _) => ; + } + // TODO: Prevents log spam, but should be handled correctly if (messagetype != ChatMessageType.CMT_TOGGLE_GM) { log.info("Chat: " + msg) @@ -1765,15 +1861,15 @@ class WorldSessionActor extends Actor with MDCContextAware { case x :: xs => val (deleteFunc, modifyFunc) : ((Int, AmmoBox)=>Unit, (AmmoBox, Int)=>Unit) = obj match { case (veh : Vehicle) => - (DeleteAmmunitionInVehicle(veh), ModifyAmmunitionInVehicle(veh)) + (DeleteEquipmentFromVehicle(veh), ModifyAmmunitionInVehicle(veh)) case _ => - (DeleteAmmunition(obj), ModifyAmmunition(obj)) + (DeleteEquipment(obj), ModifyAmmunition(obj)) } val (stowFuncTask, stowFunc) : ((Int, AmmoBox)=>TaskResolver.GiveTask, (Int, AmmoBox)=>Unit) = obj match { case (veh : Vehicle) => - (StowNewAmmunitionInVehicles(veh), StowAmmunitionInVehicles(veh)) + (StowNewEquipmentInVehicle(veh), StowEquipmentInVehicles(veh)) case _ => - (StowNewAmmunition(obj), StowAmmunition(obj)) + (StowNewEquipment(obj), StowEquipment(obj)) } xs.foreach(item => { obj.Inventory -= x.start @@ -1985,9 +2081,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case x :: xs => val (deleteFunc, modifyFunc) : ((Int, AmmoBox)=>Unit, (AmmoBox, Int)=>Unit) = obj match { case (veh : Vehicle) => - (DeleteAmmunitionInVehicle(veh), ModifyAmmunitionInVehicle(veh)) + (DeleteEquipmentFromVehicle(veh), ModifyAmmunitionInVehicle(veh)) case _ => - (DeleteAmmunition(obj), ModifyAmmunition(obj)) + (DeleteEquipment(obj), ModifyAmmunition(obj)) } xs.foreach(item => { deleteFunc(item.start, item.obj.asInstanceOf[AmmoBox]) @@ -2079,7 +2175,14 @@ class WorldSessionActor extends Actor with MDCContextAware { findFunc(parent) case None => None - }) match { + }) + .orElse(FindLocalVehicle match { + case Some(parent) => + findFunc(parent) + case None => + None + }) + match { case Some((parent, Some(slot))) => taskResolver ! RemoveEquipmentFromSlot(parent, obj, slot) log.info(s"RequestDestroy: equipment $object_guid") @@ -2283,6 +2386,15 @@ class WorldSessionActor extends Actor with MDCContextAware { //TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks) sendResponse(BindPlayerMessage(1, "@ams", true, true, 0, 0, 0, obj.Position)) } + else if(obj.Definition.isInstanceOf[RepairRearmSiloDefinition]) { + FindLocalVehicle match { + case Some(vehicle) => + sendResponse(UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + sendResponse(UseItemMessage(avatar_guid, unk1, vehicle.GUID, unk2, unk3, unk4, unk5, unk6, unk7, unk8, vehicle.Definition.ObjectId)) + case None => + log.error("UseItem: expected seated vehicle, but found none") + } + } else { sendResponse(UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) } @@ -2305,13 +2417,14 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ObjectDeleteMessage(PlanetSideGUID(unk1), 2)) } - case None => ; + case None => + log.error(s"UseItem: can not find object $object_guid") } case msg @ ProximityTerminalUseMessage(player_guid, object_guid, _) => - log.info(s"ProximityTerminal: $msg") + log.info(s"ProximityTerminalUse: $msg") continent.GUID(object_guid) match { - case Some(obj : ProximityTerminal) => + case Some(obj : Terminal with ProximityUnit) => if(usingProximityTerminal.contains(object_guid)) { SelectProximityUnit(obj) } @@ -2319,9 +2432,9 @@ class WorldSessionActor extends Actor with MDCContextAware { StartUsingProximityUnit(obj) } case Some(obj) => ; - log.warn(s"ProximityTerminal: object is not a terminal - $obj") + log.warn(s"ProximityTerminalUse: object does not have proximity effects - $obj") case None => - log.warn(s"ProximityTerminal: no object with guid $object_guid found") + log.warn(s"ProximityTerminalUse: no object with guid $object_guid found") } case msg @ UnuseItemMessage(player_guid, object_guid) => @@ -2358,20 +2471,46 @@ class WorldSessionActor extends Actor with MDCContextAware { log.error(s"ItemTransaction: $terminal_guid does not exist") } - case msg @ FavoritesRequest(player_guid, unk, action, line, label) => + case msg @ FavoritesRequest(player_guid, list, action, line, label) => + log.info(s"FavoritesRequest: $msg") if(player.GUID == player_guid) { - val name = label.getOrElse("missing_loadout_name") + val lineno = if(list == LoadoutType.Vehicle) { line + 10 } else { line } + val name = label.getOrElse(s"missing_loadout_${line+1}") action match { - case FavoritesAction.Unknown => ; case FavoritesAction.Save => - avatar.SaveLoadout(player, name, line) - sendResponse(FavoritesMessage(0, player_guid, line, name)) + (if(list == LoadoutType.Infantry) { + Some(player) + } + else if(list == LoadoutType.Vehicle) { + player.VehicleSeated match { + case Some(vehicle_guid) => + continent.GUID(vehicle_guid) + case None => + None + } + } + else { + None + }) match { + case Some(owner : Player) => //InfantryLoadout + avatar.SaveLoadout(owner, name, lineno) + import InfantryLoadout._ + sendResponse(FavoritesMessage(list, player_guid, line, name, DetermineSubtypeB(player.ExoSuit, DetermineSubtype(player)))) + case Some(owner : Vehicle) => //VehicleLoadout + avatar.SaveLoadout(owner, name, lineno) + sendResponse(FavoritesMessage(list, player_guid, line, name)) + case Some(_) | None => + log.error("FavoritesRequest: unexpected owner for favorites") + } + case FavoritesAction.Delete => - avatar.DeleteLoadout(line) - sendResponse(FavoritesMessage(0, player_guid, line, "")) + avatar.DeleteLoadout(lineno) + sendResponse(FavoritesMessage(list, player_guid, line, "")) + + case FavoritesAction.Unknown => + log.warn("FavoritesRequest: unknown favorites action") } } - log.info("FavoritesRequest: " + msg) case msg @ WeaponDelayFireMessage(seq_time, weapon_guid) => log.info("WeaponDelayFire: " + msg) @@ -2562,6 +2701,9 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PlanetsideAttributeMessage(object_guid, attribute_type, attribute_value)) } + case msg @ FacilityBenefitShieldChargeRequestMessage(guid) => + //log.info(s"ShieldChargeRequest: $msg") + case msg @ BattleplanMessage(char_id, player_name, zonr_id, diagrams) => log.info("Battleplan: "+msg) @@ -3303,32 +3445,52 @@ class WorldSessionActor extends Actor with MDCContextAware { } /** - * Given an object that contains a box of amunition in its `Inventory` at a certain location, + * Get the current `Vehicle` object that the player is riding/driving. + * The vehicle must be found solely through use of `player.VehicleSeated`. + * @return the vehicle + */ + def FindLocalVehicle : Option[Vehicle] = { + player.VehicleSeated match { + case Some(vehicle_guid) => + continent.GUID(vehicle_guid) match { + case Some(obj : Vehicle) => + Some(obj) + case _ => + None + } + case None => + None + } + } + + /** + * Given an object that contains an item (`Equipment`) in its `Inventory` at a certain location, * remove it permanently. * @param obj the `Container` - * @param start where the ammunition can be found - * @param item an object to unregister (should have been the ammunition that was removed); + * @param start where the item can be found + * @param item an object to unregister; * not explicitly checked */ - private def DeleteAmmunition(obj : PlanetSideGameObject with Container)(start : Int, item : AmmoBox) : Unit = { + private def DeleteEquipment(obj : PlanetSideGameObject with Container)(start : Int, item : Equipment) : Unit = { val item_guid = item.GUID - obj.Inventory -= start + obj.Slot(start).Equipment = None + //obj.Inventory -= start taskResolver ! GUIDTask.UnregisterEquipment(item)(continent.GUID) sendResponse(ObjectDeleteMessage(item_guid, 0)) } /** - * Given a vehicle that contains a box of amunition in its `Trunk` at a certain location, + * Given a vehicle that contains an item (`Equipment`) in its `Trunk` at a certain location, * remove it permanently. - * @see `DeleteAmmunition` + * @see `DeleteEquipment` * @param obj the `Vehicle` - * @param start where the ammunition can be found - * @param item an object to unregister (should have been the ammunition that was removed); + * @param start where the item can be found + * @param item an object to unregister; * not explicitly checked */ - private def DeleteAmmunitionInVehicle(obj : Vehicle)(start : Int, item : AmmoBox) : Unit = { + private def DeleteEquipmentFromVehicle(obj : Vehicle)(start : Int, item : Equipment) : Unit = { val item_guid = item.GUID - DeleteAmmunition(obj)(start, item) + DeleteEquipment(obj)(start, item) vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player.GUID, item_guid)) } @@ -3361,27 +3523,27 @@ class WorldSessionActor extends Actor with MDCContextAware { /** * Announce that an already-registered `AmmoBox` object exists in a given position in some `Container` object's inventory. - * @see `StowAmmunitionInVehicles` + * @see `StowEquipmentInVehicles` * @see `ChangeAmmoMessage` * @param obj the `Container` object * @param index an index in `obj`'s inventory * @param item an `AmmoBox` */ - def StowAmmunition(obj : PlanetSideGameObject with Container)(index : Int, item : AmmoBox) : Unit = { + def StowEquipment(obj : PlanetSideGameObject with Container)(index : Int, item : AmmoBox) : Unit = { obj.Inventory += index -> item sendResponse(ObjectAttachMessage(obj.GUID, item.GUID, index)) } /** * Announce that an already-registered `AmmoBox` object exists in a given position in some vehicle's inventory. - * @see `StowAmmunition` + * @see `StowEquipment` * @see `ChangeAmmoMessage` * @param obj the `Vehicle` object * @param index an index in `obj`'s inventory * @param item an `AmmoBox` */ - def StowAmmunitionInVehicles(obj : Vehicle)(index : Int, item : AmmoBox) : Unit = { - StowAmmunition(obj)(index, item) + def StowEquipmentInVehicles(obj : Vehicle)(index : Int, item : AmmoBox) : Unit = { + StowEquipment(obj)(index, item) vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player.GUID, obj.GUID, index, item)) } @@ -3389,14 +3551,14 @@ class WorldSessionActor extends Actor with MDCContextAware { * Prepare tasking that registers an `AmmoBox` object * and announces that it exists in a given position in some `Container` object's inventory. * `PutEquipmentInSlot` is the fastest way to achieve these goals. - * @see `StowNewAmmunitionInVehicles` + * @see `StowNewEquipmentInVehicle` * @see `ChangeAmmoMessage` * @param obj the `Container` object * @param index an index in `obj`'s inventory * @param item an `AmmoBox` * @return a `TaskResolver.GiveTask` chain that executes the action */ - def StowNewAmmunition(obj : PlanetSideGameObject with Container)(index : Int, item : AmmoBox) : TaskResolver.GiveTask = { + def StowNewEquipment(obj : PlanetSideGameObject with Container)(index : Int, item : Equipment) : TaskResolver.GiveTask = { PutEquipmentInSlot(obj, item, index) } @@ -3404,14 +3566,14 @@ class WorldSessionActor extends Actor with MDCContextAware { * Prepare tasking that registers an `AmmoBox` object * and announces that it exists in a given position in some vehicle's inventory. * `PutEquipmentInSlot` is the fastest way to achieve these goals. - * @see `StowNewAmmunition` + * @see `StowNewEquipment` * @see `ChangeAmmoMessage` * @param obj the `Container` object * @param index an index in `obj`'s inventory * @param item an `AmmoBox` * @return a `TaskResolver.GiveTask` chain that executes the action */ - def StowNewAmmunitionInVehicles(obj : Vehicle)(index : Int, item : AmmoBox) : TaskResolver.GiveTask = { + def StowNewEquipmentInVehicle(obj : Vehicle)(index : Int, item : Equipment) : TaskResolver.GiveTask = { TaskResolver.GiveTask( new Task() { private val localService = vehicleService @@ -3430,7 +3592,7 @@ class WorldSessionActor extends Actor with MDCContextAware { resolver ! scala.util.Success(this) } }, - List(StowNewAmmunition(obj)(index, item)) + List(StowNewEquipment(obj)(index, item)) ) } @@ -3860,11 +4022,6 @@ class WorldSessionActor extends Actor with MDCContextAware { shooting = None case None => ; } - if(player != null && player.isAlive && player.VisibleSlots.contains(player.DrawnSlot)) { - player.DrawnSlot = Player.HandsDownSlot - sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, true)) - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectHeld(player.GUID, player.LastDrawnSlot)) - } if(flying) { sendResponse(ChatMsg(ChatMessageType.CMT_FLY, false, "", "off", None)) flying = false @@ -4004,7 +4161,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * Special note is warranted in the case of a medical terminal or an advanced medical terminal. * @param terminal the proximity-based unit */ - def StartUsingProximityUnit(terminal : ProximityTerminal) : Unit = { + def StartUsingProximityUnit(terminal : Terminal with ProximityUnit) : Unit = { val term_guid = terminal.GUID if(!usingProximityTerminal.contains(term_guid)) { usingProximityTerminal += term_guid @@ -4025,7 +4182,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * Other sorts of proximity-based units are put on a timer. * @param terminal the proximity-based unit */ - def StopUsingProximityUnit(terminal : ProximityTerminal) : Unit = { + def StopUsingProximityUnit(terminal : Terminal with ProximityUnit) : Unit = { val term_guid = terminal.GUID if(usingProximityTerminal.contains(term_guid)) { usingProximityTerminal -= term_guid @@ -4044,7 +4201,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * If this timer completes, a message will be sent that will attempt to disassociate from the target proximity unit. * @param terminal the proximity-based unit */ - def SetDelayedProximityUnitReset(terminal : ProximityTerminal) : Unit = { + def SetDelayedProximityUnitReset(terminal : Terminal with ProximityUnit) : Unit = { val terminal_guid = terminal.GUID ClearDelayedProximityUnitReset(terminal_guid) import scala.concurrent.duration._ @@ -4089,7 +4246,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * and determinig which kind of unit is being utilized. * @param terminal the proximity-based unit */ - def SelectProximityUnit(terminal : ProximityTerminal) : Unit = { + def SelectProximityUnit(terminal : Terminal with ProximityUnit) : Unit = { terminal.Definition match { case GlobalDefinitions.adv_med_terminal | GlobalDefinitions.medical_terminal => ProximityMedicalTerminal(terminal) @@ -4098,6 +4255,10 @@ class WorldSessionActor extends Actor with MDCContextAware { SetDelayedProximityUnitReset(terminal) ProximityHealCrystal(terminal) + case GlobalDefinitions.repair_silo => + SetDelayedProximityUnitReset(terminal) + //TODO insert vehicle repair here; see ProximityMedicalTerminal for example + case _ => ; } } @@ -4108,7 +4269,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * If the player is both fully healed and fully repaired, stop using the terminal. * @param unit the medical terminal */ - def ProximityMedicalTerminal(unit : ProximityTerminal) : Unit = { + def ProximityMedicalTerminal(unit : Terminal with ProximityUnit) : Unit = { val healthFull : Boolean = if(player.Health < player.MaxHealth) { HealAction(player) } @@ -4132,7 +4293,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * If the player is fully healed, stop using the crystal. * @param unit the healing crystal */ - def ProximityHealCrystal(unit : ProximityTerminal) : Unit = { + def ProximityHealCrystal(unit : Terminal with ProximityUnit) : Unit = { val healthFull : Boolean = if(player.Health < player.MaxHealth) { HealAction(player) } @@ -4232,12 +4393,95 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ConnectionClose()) } - def sendResponse(cont : PlanetSideControlPacket) : Unit = { - sendResponse(PacketCoding.CreateControlPacket(cont)) + /** + * Persistent collector that intercepts `GamePacket` and `ControlPacket` messages that are being sent towards the network. + */ + private val packetBundlingCollector : MultiPacketCollector = new MultiPacketCollector() + /** + * Re-assigned function used to direct/intercept packets being sent towards the network. + * Defaults to directing the packets. + */ + private var packetBundlingFunc : (PlanetSidePacket)=>Option[PlanetSidePacket] = NoBundlingAction + + /** + * Start packet bundling by assigning the appropriate function. + * @see `sendResponse(PlanetSidePacket) : Unit` + */ + def StartBundlingPackets() : Unit = { + log.trace("WORLD SEND: STARTED BUNDLING PACKETS") + packetBundlingFunc = PerformBundlingAction } - def sendResponse(cont : PlanetSideGamePacket) : Unit = { + /** + * Stop packet bundling by assigning the appropriate function. + * If any bundles are in the collector's buffer, push that bundle out towards the network. + * @see `sendResponse(PlanetSidePacket) : Unit` + */ + def StopBundlingPackets() : Unit = { + log.trace("WORLD SEND: PACKET BUNDLING SUSPENDED") + packetBundlingFunc = NoBundlingAction + packetBundlingCollector.BundleOption match { + case Some(bundle) => + sendResponse(bundle) + case None => ; + } + } + + /** + * Transform the packet into either a `PlanetSideGamePacket` or a `PlanetSideControlPacket` and push it towards the network. + * @param cont the packet + * @return the same packet, to indicate it was sent + */ + private def NoBundlingAction(cont : PlanetSidePacket) : Option[PlanetSidePacket] = { + cont match { + case game : PlanetSideGamePacket => + sendResponse(PacketCoding.CreateGamePacket(0, game)) + case control : PlanetSideControlPacket => + sendResponse(PacketCoding.CreateControlPacket(control)) + case _ => ; + } + Some(cont) + } + + /** + * Intercept the packet being sent towards the network and + * add it to a bundle that will eventually be sent to the network itself. + * @param cont the packet + * @return always `None`, to indicate the packet was not sent + */ + private def PerformBundlingAction(cont : PlanetSidePacket) : Option[PlanetSidePacket] = { + log.trace("WORLD SEND, BUNDLED: " + cont) + packetBundlingCollector.Add(cont) + None + } + + /** + * Common entry point for transmitting packets to the network. + * Alternately, catch those packets and retain them to send out a bundled message. + * @param cont the packet + */ + def sendResponse(cont : PlanetSidePacket) : Unit = packetBundlingFunc(cont) + + /** + * `KeepAliveMessage` is a special `PlanetSideGamePacket` that is excluded from being bundled when it is sent to the network.
+ *
+ * The risk of the server getting caught in a state where the packets dispatched to the client are alwaysd bundled is posible. + * Starting the bundling functionality but forgetting to transition into a state where it is deactivated can lead to this problem. + * No packets except for `KeepAliveMessage` will ever be sent until the ever-accumulating packets overflow. + * To avoid this state, whenever a `KeepAliveMessage` is sent, the packet collector empties its current contents to the network. + * @see `StartBundlingPackets`
+ * `StopBundlingPackets`
+ * `clientKeepAlive` + * @param cont a `KeepAliveMessage` packet + */ + def sendResponse(cont : KeepAliveMessage) : Unit = { sendResponse(PacketCoding.CreateGamePacket(0, cont)) + packetBundlingCollector.BundleOption match { + case Some(bundle) => + log.trace("WORLD SEND: INTERMITTENT PACKET BUNDLE") + sendResponse(bundle) + case None => ; + } } def sendResponse(cont : PlanetSidePacketContainer) : Unit = { @@ -4246,7 +4490,6 @@ class WorldSessionActor extends Actor with MDCContextAware { } def sendResponse(cont : MultiPacketBundle) : Unit = { - log.trace("WORLD SEND: " + cont) sendResponse(cont.asInstanceOf[Any]) } @@ -4272,7 +4515,7 @@ object WorldSessionActor { private final case class ListAccountCharacters() private final case class SetCurrentAvatar(tplayer : Player) private final case class VehicleLoaded(vehicle : Vehicle) - private final case class DelayedProximityUnitStop(unit : ProximityTerminal) + private final case class DelayedProximityUnitStop(unit : Terminal with ProximityUnit) private final case class UnregisterCorpseOnVehicleDisembark(corpse : Player) /** diff --git a/pslogin/src/main/scala/Zones.scala b/pslogin/src/main/scala/Zones.scala index 55d18bb9..0e0bef78 100644 --- a/pslogin/src/main/scala/Zones.scala +++ b/pslogin/src/main/scala/Zones.scala @@ -88,13 +88,13 @@ object Zones { val c6 = new Zone("c6", Maps.ugd06, 28) - val i1 = new Zone("i1", Maps.map96, 29) + val i1 = new Zone("i1", Maps.map99, 29) - val i2 = new Zone("i2", Maps.map97, 30) + val i2 = new Zone("i2", Maps.map98, 30) - val i3 = new Zone("i3", Maps.map98, 31) + val i3 = new Zone("i3", Maps.map97, 31) - val i4 = new Zone("i4", Maps.map99, 32) + val i4 = new Zone("i4", Maps.map96, 32) /** * Get the zone identifier name for the sanctuary continent of a given empire. diff --git a/pslogin/src/main/scala/csr/CSRWarp.scala b/pslogin/src/main/scala/csr/CSRWarp.scala new file mode 100644 index 00000000..eb3048f8 --- /dev/null +++ b/pslogin/src/main/scala/csr/CSRWarp.scala @@ -0,0 +1,129 @@ +package csr + +import net.psforever.packet.PacketCoding +import net.psforever.packet.game.ChatMsg +import net.psforever.types.{ChatMessageType, Vector3} + +import scala.collection.mutable.ArrayBuffer +import scala.util.Try + +/* +The following is STILL for development and fun. +*/ +/** + * An implementation of the CSR command `/warp`, highly modified to serve the purposes of the testing phases of the server. + * See `help()` for details. + */ +object CSRWarp { + /** + * Accept and confirm that a message sent to a player is a valid `/warp` invocation. + * If so, parse the message and send the player to whichever destination in this zone was requested. + * @param traveler the player + * @param msg the message the player received + * @return true, if the player is being transported to another place; false, otherwise + */ + def read(traveler : Traveler, msg : ChatMsg) : (Boolean, Vector3) = { + if(!isProperRequest(msg)) + return (false, Vector3.Zero) //we do not handle this message + + val buffer = decomposeMessage(msg.contents) + if(buffer.length == 0 || buffer(0).equals("") || buffer(0).equals("-help")) { + CSRWarp.help(traveler) //print usage information to chat + return (false, Vector3.Zero) + } + var destId : String = "" + var coords : ArrayBuffer[Int] = ArrayBuffer.empty[Int] + var list : Boolean = false + var failedCoordInput = false + for(o <- buffer) { + val toInt = Try(o.toInt) + if(toInt.isSuccess) { + coords += toInt.get + } + else if(coords.nonEmpty && coords.size < 3) + failedCoordInput = true + if(o.equals("-list")) + list = true + else if(destId.equals("")) + destId = o + } + if(failedCoordInput || (coords.nonEmpty && coords.size < 3)) { + CSRWarp.error(traveler, "Needs three integer components ( )") + return (false, Vector3.Zero) + } + else { + coords.slice(0, 3).foreach(x => { + if(x < 0 || x > 8191) { + CSRWarp.error(traveler, "Out of range - 0 < n < 8191, but n = " + x) + return (false, Vector3.Zero) + } + }) + } + val zone = CSRZoneImpl.get(traveler.zone).get //the traveler is already in the appropriate zone + if(list && coords.isEmpty && destId.equals("")) { + CSRWarp.reply(traveler, CSRZoneImpl.listLocations(zone) + "; " + CSRZoneImpl.listWarpgates(zone)) + return (false, Vector3.Zero) + } + val dest : Option[Vector3] = if(coords.nonEmpty) Some(Vector3(coords(0), coords(1), coords(2))) + else CSRZoneImpl.getWarpLocation(zone, destId) //coords before destId + if(dest.isEmpty) { + CSRWarp.error(traveler, "Invalid location") + return (false, Vector3.Zero) + } + (true, dest.get) + } + + /** + * Check that the incoming message is an appropriate type for this command. + * @param msg the message + * @return true, if we will handle it; false, otherwise + */ + def isProperRequest(msg : ChatMsg) : Boolean = { + msg.messageType == ChatMessageType.CMT_WARP + } + + /** + * Break the message in the packet down for parsing. + * @param msg the contents portion of the message, a space-separated `String` + * @return the contents portion of the message, transformed into an `Array` + */ + private def decomposeMessage(msg : String) : Array[String] = { + msg.trim.toLowerCase.split("\\s+") + } + + /** + * Send a message back to the `Traveler` that will be printed into his chat window. + * @param traveler the player + * @param msg the message to be sent + */ + private def reply(traveler : Traveler, msg : String) : Unit = { + traveler ! PacketCoding.CreateGamePacket(0, ChatMsg(ChatMessageType.CMT_OPEN, true, "", msg, None)) + } + + /** + * Print usage information to the `Traveler`'s chat window.
+ *
+ * The "official" use information for help dictates the command should follow this format: + * `/warp <x><y><z> | to <character> | near <object> | above <object> | waypoint`. + * In our case, creating fixed coordinate points of interest is not terribly dissimilar from the "near" and "to" aspect. + * We can not currently implement most of the options for now, however.
+ *
+ * The destination prioritizes evaluation of the coordinates before the location string. + * When the user provides coordinates, he must provide all three components of the coordinate at once, else none will be accepted. + * If the coordinates are invalid, the location string will still be checked. + * "-list" is accepted while no serious attempt is made to indicate a destination (no location string or not enough coordinates). + * @param traveler the player + */ + private def help(traveler : Traveler) : Unit = { + CSRWarp.reply(traveler, "usage: /warp | | | [-list]") + } + + /** + * Print error information to the `Traveler`'s chat window.
+ * The most common reason for error is the lack of information, or wrong information. + * @param traveler the player + */ + private def error(traveler : Traveler, msg : String) : Unit = { + CSRWarp.reply(traveler, "Error! " + msg) + } +} diff --git a/pslogin/src/main/scala/csr/CSRZone.scala b/pslogin/src/main/scala/csr/CSRZone.scala new file mode 100644 index 00000000..a2f5329c --- /dev/null +++ b/pslogin/src/main/scala/csr/CSRZone.scala @@ -0,0 +1,114 @@ +package csr + +import net.psforever.packet.PacketCoding +import net.psforever.packet.game.ChatMsg +import net.psforever.types.{ChatMessageType, Vector3} + +/* +The following is STILL for development and fun. +*/ +/** + * An implementation of the CSR command `/zone`, slightly modified to serve the purposes of the testing phases of the server. + */ +object CSRZone { + /** + * Accept and confirm that a message sent to a player is a valid `/zone` invocation. + * If so, parse the message and send the player to whichever zone was requested. + * @param traveler the player + * @param msg the message the player received + * @return true, if the player is being transported to another zone; false, otherwise + */ + def read(traveler : Traveler, msg : ChatMsg) : (Boolean, String , Vector3 ) = { + if(!isProperRequest(msg)) + return (false,"", Vector3.Zero) //we do not handle this message + + val buffer = decomposeMessage(msg.contents) + if(buffer.length == 0 || buffer(0).equals("-help")) { + CSRZone.help(traveler) //print usage information to chat + return (false,"", Vector3.Zero) + } + + var zoneId = "" + var gateId = "" //the user can define which warpgate they may visit (actual keyword protocol missing) + var list = false //if the user wants a printed list of destination locations + for(o <- buffer) { + if(o.equals("-list")) { + if(zoneId.equals("") || gateId.equals("")) { + list = true + } + } + else if(zoneId.equals("")) + zoneId = o + else if(gateId.equals("")) + gateId = o + } + + val zoneOpt = CSRZoneImpl.get(zoneId) + if(zoneOpt.isEmpty) { + if(list) + CSRZone.reply(traveler, CSRZoneImpl.list) + else + CSRZone.error(traveler, "Give a valid zonename (use '/zone -list')") + return (false,"", Vector3.Zero) + } + val zone = zoneOpt.get + var destination : Vector3 = CSRZoneImpl.selectRandom(zone) //the destination in the new zone starts as random + + if(!gateId.equals("")) { //if we've defined a warpgate, and can find that warpgate, we re-assign the destination + val gateOpt = CSRZoneImpl.getWarpgate(zone, gateId) + if(gateOpt.isDefined) + destination = gateOpt.get + else + CSRZone.error(traveler, "Gate id not defined (use '/zone -list')") + } + else if(list) { + CSRZone.reply(traveler, CSRZoneImpl.listWarpgates(zone)) + return (false,"",Vector3.Zero) + } + (true, zone.zonename, destination) + } + + /** + * Check that the incoming message is an appropriate type for this command. + * @param msg the message + * @return true, if we will handle it; false, otherwise + */ + def isProperRequest(msg : ChatMsg) : Boolean ={ + msg.messageType == ChatMessageType.CMT_ZONE + } + + /** + * Break the message in the packet down for parsing. + * @param msg the contents portion of the message, a space-separated `String` + * @return the contents portion of the message, transformed into an `Array` + */ + private def decomposeMessage(msg : String) : Array[String] = { + msg.trim.toLowerCase.split("\\s+") + } + + /** + * Send a message back to the `Traveler` that will be printed into his chat window. + * @param traveler the player + * @param msg the message to be sent + */ + private def reply(traveler : Traveler, msg : String) : Unit = { + traveler ! PacketCoding.CreateGamePacket(0, ChatMsg(ChatMessageType.CMT_OPEN,true,"", msg, None)) + } + + /** + * Print usage information to the `Traveler`'s chat window. + * @param traveler the player + */ + private def help(traveler : Traveler) : Unit = { + CSRZone.reply(traveler, "usage: /zone [gatename] | [-list]") + } + + /** + * Print error information to the `Traveler`'s chat window.
+ * The most common reason for error is the lack of information, or wrong information. + * @param traveler the player + */ + private def error(traveler : Traveler, msg : String) : Unit = { + CSRZone.reply(traveler, "Error! "+msg) + } +} diff --git a/pslogin/src/main/scala/csr/CSRZoneImpl.scala b/pslogin/src/main/scala/csr/CSRZoneImpl.scala new file mode 100644 index 00000000..ff5a2a70 --- /dev/null +++ b/pslogin/src/main/scala/csr/CSRZoneImpl.scala @@ -0,0 +1,578 @@ +package csr + +// Copyright (c) 2017 PSForever +import net.psforever.types.Vector3 + +import scala.collection.mutable +import scala.util.Random + +/* +The following is STILL for development and fun. +*/ +/** + * A crude representation of the information needed to describe a continent (hitherto, a "zone"). + * The information is mainly catered to the simulation of the CSR commands `/zone` and `/warp`. + * (The exception is `alias` which is maintained for cosmetic purposes and clarification.) + * @param alias the common name of the zone + * @param map the map name of the zone (this map is loaded) + * @param zonename the zone's internal name + */ +class CSRZoneImpl(val alias : String, val map : String, val zonename : String) { + /** + * A listing of warpgates, geowarps, and island warpgates in this zone. + * The coordinates specified will only ever drop the user on a specific point within the protective bubble of the warpgate. + * This breaks from the expected zoning functionality where the user is placed in a random spot under the bubble. + * There is no prior usage details for the searchability format of this field's key values. + */ + private val gates : mutable.HashMap[String, Vector3] = mutable.HashMap() + /** + * A listing of special locations in this zone, i.e., major faciities, and some landmarks of interest. + * There is no prior usage details for the searchability format of this field's key values. + */ + private val locations : mutable.HashMap[String, Vector3] = mutable.HashMap() +} + +object CSRZoneImpl { + /** + * A listing of all zones that can be visited by their internal name. + * The keys in this map should be directly usable by the `/zone` command. + */ + private val zones = Map[String, CSRZoneImpl]( + "z1" -> CSRZoneImpl("Solsar", "map01", "z1"), + "z2" -> CSRZoneImpl("Hossin", "map02", "z2"), + "z3" -> CSRZoneImpl("Cyssor", "map03", "z3"), + "z4" -> CSRZoneImpl("Ishundar", "map04", "z4"), + "z5" -> CSRZoneImpl("Forseral", "map05", "z5"), + "z6" -> CSRZoneImpl("Ceryshen", "map06", "z6"), + "z7" -> CSRZoneImpl("Esamir", "map07", "z7"), + "z8" -> CSRZoneImpl("Oshur", "map08", "z8"), + "z9" -> CSRZoneImpl("Searhus", "map09", "z9"), + "z10" -> CSRZoneImpl("Amerish", "map10", "z10"), + "home1" -> CSRZoneImpl("NC Sanctuary", "map11", "home1"), + "home2" -> CSRZoneImpl("TR Sanctuary", "map12", "home2"), + "home3" -> CSRZoneImpl("VS Sanctuary", "map13", "home3"), + "tzshtr" -> CSRZoneImpl("VR Shooting Range TR", "map14", "tzshtr"), + "tzdrtr" -> CSRZoneImpl("VR Driving Range TR", "map15", "tzdrtr"), + "tzcotr" -> CSRZoneImpl("VR Combat csr.CSRZoneImpl TR", "map16", "tzcotr"), + "tzshvs" -> CSRZoneImpl("VR Shooting Range VS", "map14", "tzshvs"), + "tzdrvs" -> CSRZoneImpl("VR Driving Range VS", "map15", "tzdrvs"), + "tzcovs" -> CSRZoneImpl("VR Combat csr.CSRZoneImpl VS", "map16", "tzcovs"), + "tzshnc" -> CSRZoneImpl("VR Shooting Range NC", "map14", "tzshnc"), + "tzdrnc" -> CSRZoneImpl("VR Driving Range NC", "map15", "tzdrnc"), + "tzconc" -> CSRZoneImpl("VR Combat csr.CSRZoneImpl NC", "map16", "tzconc"), + "c1" -> CSRZoneImpl("Supai", "ugd01", "c1"), + "c2" -> CSRZoneImpl("Hunhau", "ugd02", "c2"), + "c3" -> CSRZoneImpl("Adlivun", "ugd03", "c3"), + "c4" -> CSRZoneImpl("Byblos", "ugd04", "c4"), + "c5" -> CSRZoneImpl("Annwn", "ugd05", "c5"), + "c6" -> CSRZoneImpl("Drugaskan", "ugd06", "c6"), + "i4" -> CSRZoneImpl("Nexus", "map96", "i4"), + "i3" -> CSRZoneImpl("Desolation", "map97", "i3"), + "i2" -> CSRZoneImpl("Ascension", "map98", "i2"), + "i1" -> CSRZoneImpl("Extinction", "map99", "i1"), + "homebo" -> CSRZoneImpl("Black_ops_hq", "Black_ops_hq", "homebo"), + "station1" -> CSRZoneImpl("TR Station", "Station1", "station1"), + "station2" -> CSRZoneImpl("NC Station", "Station2", "station2"), + "station3" -> CSRZoneImpl("VS Station", "Station3", "station3") + ) + /** + * A listing of all zones that can be visited by their common name. + * The keys in this map should be directly usable by the `/zone` command. + * Though the behavior is undocumented, access to this alias list is for the benefit of the user. + */ + private val alias = Map[String, String]( + "solsar" -> "z1", + "hossin" -> "z2", + "cyssor" -> "z3", + "ishundar" -> "z4", + "forseral" -> "z5", + "ceryshen" -> "z6", + "esamir" -> "z7", + "oshur" -> "z8", + "searhus" -> "z9", + "amerish" -> "z10", + "nc-sanctuary" -> "home1", + "tr-sanctuary" -> "home2", + "vs-sanctuary" -> "home3", + "tr-shooting" -> "tzshtr", + "tr-driving" -> "tzdrtr", + "tr-combat" -> "tzcotr", + "vs-shooting" -> "tzshvs", + "vs-driving" -> "tzdrvs", + "vs-combat" -> "tzcovs", + "nc-shooting" -> "tzshnc", + "nc-driving" -> "tzdrnc", + "nc-combat" -> "tzconc", + "supai" -> "c1", + "hunhau" -> "c2", + "adlivun" -> "c3", + "byblos" -> "c4", + "annwn" -> "c5", + "drugaskan" -> "c6", + "nexus" -> "i4", + "desolation" -> "i3", + "ascension" -> "i2", + "extinction" -> "i1", + "Black_ops_hq" -> "homebo", + "TR-Station" -> "station1", + "NC-Station" -> "station2", + "VS-Station" -> "station3" + ) + /** + * A value used for selecting where to appear in a zone from the list of locations when the user has no indicated one. + */ + private val rand = Random + setup() + + /** + * An abbreviated constructor for creating `CSRZone`s without invocation of `new`. + * @param alias the common name of the zone + * @param map the map name of the zone (this map is loaded) + * @param zonename the zone's internal name + */ + def apply(alias : String, map : String, zonename : String) : CSRZoneImpl = new CSRZoneImpl(alias, map, zonename) + + /** + * Get a valid `CSRZone`'s information. + * @param zoneId a name that describes the zone and should be searchable + * @return the `CSRZone`, or `None` + */ + def get(zoneId : String) : Option[CSRZoneImpl] = { + var zId = zoneId.toLowerCase + if(alias.get(zId).isDefined) + zId = alias(zId) + zones.get(zId) + } + + /** + * Get a location within the `CSRZone`. + * The location should be a facility or a warpgate or interesting. + * @param zone the `CSRZone` + * @param locId a name that describes a known location in the provided `CSRZone` and is searchable + * @return the coordinates of that location, or None + */ + def getWarpLocation(zone : CSRZoneImpl, locId : String) : Option[Vector3] = { + val low_locId = locId.toLowerCase + var location = zone.locations.get(low_locId) + if(location.isEmpty) + location = zone.gates.get(low_locId) + location + } + + /** + * Get the position of a warpgate within the zone. + * @param zone the `CSRZone` + * @param gateId a name that describes a known warpgate in the provided `CSRZone` and is searchable + * @return the coordinates of that warpgate, or None + */ + def getWarpgate(zone : CSRZoneImpl, gateId : String) : Option[Vector3] = { + zone.gates.get(gateId.toLowerCase) + } + + /** + * Get the names for all of the `CSRZones` that can be visited. + * @return all of the zonenames + */ + def list : String = { + "zonenames: z1 - z10, home1 - home3, tzshnc, tzdrnc, tzconc, tzshtr, tzdrtr, tzcotr, tzshvs, tzdrvs, tzcovs, c1 - c6, i1 - i4; zones are also aliased to their continent name" + } + + /** + * Get the name for all of the locations that can be visited in this `CSRZone`, excluding warpgates. + * @param zone the `CSRZone` + * @return all of the location keys + */ + def listLocations(zone : CSRZoneImpl) : String = { + var out : String = "warps: " + if(zone.locations.nonEmpty) { + out += zone.locations.keys.toArray.sorted.mkString(", ") + } + else + out = "none" + out + } + + /** + * Get the name for all of the warpgates that can be visited in this `CSRZone`. + * @param zone the `CSRZone` + * @return all of the warpgate keys + */ + def listWarpgates(zone : CSRZoneImpl) : String = { + var out : String = "gatenames: " + if(zone.gates.isEmpty) + out += "none" + else + out += zone.gates.keys.toArray.sorted.mkString(", ") + out + } + + /** + * Select, of all the `CSRZone` locations and warpgates, a pseudorandom destination to spawn the player in the zone if none has been specified. + * @param zone the `CSRZone` + * @return the coordinates of the spawn point + */ + def selectRandom(zone : CSRZoneImpl) : Vector3 = { + var outlets = zone.locations //random location? + if(outlets.nonEmpty) { + return outlets.values.toArray.apply(rand.nextInt(outlets.size)) + } + outlets = zone.gates //random warpgate? + if(outlets.nonEmpty) { + return outlets.values.toArray.apply(rand.nextInt(outlets.size)) + } + Vector3.Zero //fallback coordinates (that will always be valid) + } + + /** + * Load all zones with selected places of interest and the coordinates to place the player nearby that given place of interest. + * All of these keys should be searchable under the `/warp` command. + * Only the warpgate keys are searchable by the `/zone` command. + */ + def setup() : Unit = { + zones("z1").gates += ( + "gate1" -> Vector3(4150, 7341, 82), + "gate2" -> Vector3(5698, 3404, 129), + "gate3" -> Vector3(2650, 5363, 176), + "gate4" -> Vector3(3022, 1225, 66), + "geowarp1" -> Vector3(3678, 2895, 108), + "geowarp2" -> Vector3(5672, 4750, 70) + ) + zones("z1").locations += ( + "amun" -> Vector3(4337, 2278, 68), + "aton" -> Vector3(3772, 5463, 54), + "bastet" -> Vector3(5412, 5588, 56), + "hapi" -> Vector3(4256, 4436, 59), + "horus" -> Vector3(3725, 2114, 73), + "mont" -> Vector3(3354, 4205, 83), + "seth" -> Vector3(4495, 6026, 58), + "sobek" -> Vector3(3094, 3027, 75), + "thoth" -> Vector3(4615, 3373, 53), + "lake" -> Vector3(4317, 4008, 37), + "monolith" -> Vector3(5551, 5047, 64) + ) + zones("z2").gates += ( + "gate1" -> Vector3(1881, 4873, 19), + "gate2" -> Vector3(4648, 4625, 28), + "gate3" -> Vector3(3296, 2045, 21), + "gate4" -> Vector3(5614, 1781, 32), + "geowarp1" -> Vector3(5199, 4869, 39), + "geowarp2" -> Vector3(3911, 2407, 15) + ) + zones("z2").locations += ( + "acan" -> Vector3(3534, 4015, 30), + "bitol" -> Vector3(4525, 2632, 30), + "chac" -> Vector3(4111, 5950, 39), + "ghanon" -> Vector3(2565, 3707, 41), + "hurakan" -> Vector3(1840, 2934, 38), + "ixtab" -> Vector3(3478, 3143, 40), + "kisin" -> Vector3(3356, 5374, 31), + "mulac" -> Vector3(5592, 2738, 37), + "naum" -> Vector3(5390, 3454, 28), + "voltan" -> Vector3(4529, 3414, 28), + "zotz" -> Vector3(6677, 2342, 129), + "monolith" -> Vector3(2938, 2485, 14) + ) + zones("z3").gates += ( + "gate1" -> Vector3(2616, 6567, 58), + "gate2" -> Vector3(6980, 5336, 57), + "gate3" -> Vector3(1199, 1332, 66), + "gate4" -> Vector3(5815, 1974, 63), + "geowarp1" -> Vector3(2403, 4278, 60), + "geowarp2" -> Vector3(4722, 2665, 78) + ) + zones("z3").locations += ( + "aja" -> Vector3(754, 5435, 48), + "chuku" -> Vector3(4208, 7021, 54), + "bomazi" -> Vector3(1198, 4492, 58), + "ekera" -> Vector3(5719, 6555, 51), + "faro" -> Vector3(5030, 5700, 57), + "gunuku" -> Vector3(4994, 4286, 54), + "honsi" -> Vector3(4042, 4588, 89), + "itan" -> Vector3(5175, 3393, 48), + "kaang" -> Vector3(5813, 3862, 62), + "leza" -> Vector3(2691, 1561, 64), + "mukuru" -> Vector3(661, 2380, 54), + "nzame" -> Vector3(1670, 2706, 45), + "orisha" -> Vector3(7060, 1327, 59), + "pamba" -> Vector3(7403, 3123, 63), + "shango" -> Vector3(6846, 2319, 63), + "tore" -> Vector3(3017, 2272, 58), + "wele" -> Vector3(436, 7040, 60), + "monolith" -> Vector3(4515, 4105, 38), + "peak" -> Vector3(3215, 5063, 579) + ) + zones("z4").gates += ( + "gate1" -> Vector3(4702, 6768, 30), + "gate2" -> Vector3(5515, 3368, 69), + "gate3" -> Vector3(1564, 3356, 46), + "gate4" -> Vector3(3889, 1118, 56), + "geowarp1" -> Vector3(4202, 4325, 68), + "geowarp2" -> Vector3(2384, 1925, 37) + ) + zones("z4").locations += ( + "akkan" -> Vector3(2746, 4260, 39), + "baal" -> Vector3(825, 5470, 72), + "dagon" -> Vector3(1739, 5681, 40), + "enkidu" -> Vector3(3217, 3574, 37), + "girru" -> Vector3(4475, 5853, 78), + "hanish" -> Vector3(3794, 5540, 89), + "irkall" -> Vector3(4742, 5270, 66), + "kusag" -> Vector3(6532, 4692, 46), + "lahar" -> Vector3(6965, 5306, 38), + "marduk" -> Vector3(3059, 2144, 70), + "neti" -> Vector3(3966, 2417, 80), + "zaqar" -> Vector3(4796, 2177, 75), + "monolith" -> Vector3(5165, 4083, 35), + "stonehenge" -> Vector3(4992, 3776, 56) + ) + zones("z5").gates += ( + "gate1" -> Vector3(3432, 6498, 73), + "gate2" -> Vector3(7196, 3917, 47), + "gate3" -> Vector3(1533, 3540, 56), + "gate4" -> Vector3(3197, 1390, 45), + "geowarp1" -> Vector3(4899, 5633, 38), + "geowarp2" -> Vector3(5326, 2558, 54) + ) + zones("z5").locations += ( + "anu" -> Vector3(3479, 2556, 56), + "bel" -> Vector3(3665, 4626, 58), + "caer" -> Vector3(4570, 2601, 56), + "dagd" -> Vector3(5825, 4449, 55), + "eadon" -> Vector3(2725, 2853, 53), + "gwydion" -> Vector3(5566, 3739, 61), + "lugh" -> Vector3(6083, 5069, 72), + "neit" -> Vector3(4345, 4319, 76), + "ogma" -> Vector3(3588, 3227, 114), + "pwyll" -> Vector3(4683, 4764, 104), + "monolith" -> Vector3(3251, 3245, 160), + "islands1" -> Vector3(6680, 6217, 125), + "islands2" -> Vector3(1059, 6213, 120) + ) + zones("z6").gates += ( + "gate1" -> Vector3(5040, 4327, 46), + "gate2" -> Vector3(2187, 5338, 30), + "gate3" -> Vector3(4960, 1922, 15), + "gate4" -> Vector3(2464, 3088, 189), + "geowarp1" -> Vector3(3221, 5328, 242), + "geowarp2" -> Vector3(2237, 1783, 238) + ) + zones("z6").locations += ( + "akna" -> Vector3(4509, 3732, 219), + "anguta" -> Vector3(3999, 4170, 266), + "igaluk" -> Vector3(3241, 5658, 235), + "keelut" -> Vector3(3630, 1904, 265), + "nerrivik" -> Vector3(3522, 3703, 322), + "pinga" -> Vector3(5938, 3545, 96), + "sedna" -> Vector3(3932, 5160, 232), + "tarqaq" -> Vector3(2980, 2155, 237), + "tootega" -> Vector3(5171, 3251, 217), + "monolith" -> Vector3(4011, 4851, 32), + "bridge" -> Vector3(3729, 4859, 234) + ) + zones("z7").gates += ( + "gate1" -> Vector3(1516, 6448, 61), + "gate2" -> Vector3(5249, 3819, 69), + "gate3" -> Vector3(2763, 2961, 86), + "gate4" -> Vector3(6224, 1152, 78), + "geowarp1" -> Vector3(6345, 4802, 90), + "geowarp2" -> Vector3(3800, 2197, 64) + ) + zones("z7").locations += ( + "andvari" -> Vector3(3233, 7207, 78), + "dagur" -> Vector3(4026, 6191, 60), + "eisa" -> Vector3(3456, 4513, 75), + "freyr" -> Vector3(2853, 3840, 56), + "gjallar" -> Vector3(1056, 2656, 74), + "helheim" -> Vector3(5542, 2532, 53), + "jarl" -> Vector3(1960, 5462, 68), + "kvasir" -> Vector3(4096, 1571, 69), + "mani" -> Vector3(5057, 4989, 58), + "nott" -> Vector3(6783, 4329, 46), + "ran" -> Vector3(2378, 1919, 85), + "vidar" -> Vector3(3772, 3024, 67), + "ymir" -> Vector3(1911, 4008, 69), + "monolith" -> Vector3(6390, 1622, 63) + ) + zones("z8").gates += ( + "gate1" -> Vector3(5437, 5272, 32), + "gate2" -> Vector3(3251, 5650, 60), + "gate3" -> Vector3(5112, 2616, 40), + "gate4" -> Vector3(2666, 1665, 45), + "geowarp1" -> Vector3(3979, 5370, 47), + "geowarp2" -> Vector3(6018, 3136, 35) + ) + zones("z8").locations += ( + "atar" -> Vector3(3609, 2730, 47), + "dahaka" -> Vector3(4633, 5379, 54), + "hvar" -> Vector3(3857, 4764, 49), + "izha" -> Vector3(5396, 3852, 51), + "jamshid" -> Vector3(2371, 3378, 52), + "mithra" -> Vector3(2480, 4456, 44), + "rashnu" -> Vector3(3098, 3961, 59), + "yazata" -> Vector3(4620, 3983, 62), + "zal" -> Vector3(3966, 2164, 61), + "arch1" -> Vector3(4152, 3285, 31), + "arch2" -> Vector3(4688, 5272, 68), + "pride" -> Vector3(2913, 4412, 63) + ) + zones("z9").gates += ( + "gate1" -> Vector3(1505, 6981, 65), + "gate2" -> Vector3(6835, 3517, 56), + "gate3" -> Vector3(1393, 1376, 53), + "geowarp1" -> Vector3(7081, 5552, 46), + "geowarp2" -> Vector3(3776, 1092, 49) + ) + zones("z9").locations += ( + "akua" -> Vector3(5258, 4041, 346), + "drakulu" -> Vector3(3806, 2647, 151), + "hiro" -> Vector3(4618, 5761, 190), + "iva" -> Vector3(6387, 5199, 55), + "karihi" -> Vector3(3879, 5574, 236), + "laka" -> Vector3(4720, 6718, 49), + "matagi" -> Vector3(5308, 5093, 239), + "ngaru" -> Vector3(4103, 4077, 205), + "oro" -> Vector3(4849, 4456, 208), + "pele" -> Vector3(4549, 3712, 208), + "rehua" -> Vector3(3843, 2195, 60), + "sina" -> Vector3(5919, 2177, 91), + "tara" -> Vector3(1082, 4225, 60), + "wakea" -> Vector3(1785, 5241, 63), + "monolith" -> Vector3(3246, 6507, 105) + ) + zones("z10").gates += ( + "gate1" -> Vector3(6140, 6599, 71), + "gate2" -> Vector3(4814, 4608, 59), + "gate3" -> Vector3(3152, 3480, 54), + "gate4" -> Vector3(1605, 1446, 40), + "geowarp1" -> Vector3(3612, 6918, 38), + "geowarp2" -> Vector3(3668, 3327, 55) + ) + zones("z10").locations += ( + "azeban" -> Vector3(6316, 5160, 62), + "cetan" -> Vector3(3587, 2522, 48), + "heyoka" -> Vector3(4395, 2327, 47), + "ikanam" -> Vector3(2740, 2412, 57), + "kyoi" -> Vector3(5491, 2284, 62), + "mekala" -> Vector3(6087, 2925, 59), + "onatha" -> Vector3(3397, 5799, 48), + "qumu" -> Vector3(3990, 5152, 46), + "sungrey" -> Vector3(4609, 5624, 72), + "tumas" -> Vector3(4687, 6392, 69), + "verica" -> Vector3(4973, 3459, 47), + "xelas" -> Vector3(6609, 4479, 56), + "monolith" -> Vector3(5651, 6024, 38) + ) + zones("home1").gates += ( + "gate1" -> Vector3(4158, 6344, 44), + "gate2" -> Vector3(2214, 5797, 48), + "gate3" -> Vector3(5032, 3241, 53) + ) + zones("home1").locations += "hart_c" -> Vector3(2352, 5523, 66) + zones("home2").gates += ( + "gate1" -> Vector3(5283, 4317, 44), + "gate2" -> Vector3(3139, 4809, 40), + "gate3" -> Vector3(3659, 2894, 26) + ) + zones("home2").locations += "hart_c" -> Vector3(3125, 2864, 35) + zones("home3").gates += ( + "gate1" -> Vector3(5657, 4681, 98), + "gate2" -> Vector3(2639, 5366, 57), + "gate3" -> Vector3(4079, 2467, 155) + ) + zones("home3").locations += "hart_c" -> Vector3(3675, 2727, 91) + zones("tzshtr").locations += "roof" -> Vector3(499, 1568, 25) + zones("tzcotr").locations += "spawn" -> Vector3(960, 1002, 32) + zones("tzdrtr").locations += ( + "start" -> Vector3(2457, 1864, 23), + "air_pad" -> Vector3(1700, 1900, 32) + ) + zones("tzshvs").locations += "roof" -> Vector3(499, 1568, 25) + zones("tzcovs").locations += "spawn" -> Vector3(960, 1002, 32) + zones("tzdrvs").locations += ( + "start" -> Vector3(2457, 1864, 23), + "air_pad" -> Vector3(1700, 1900, 32) + ) + zones("tzshnc").locations += "roof" -> Vector3(499, 1568, 25) + zones("tzconc").locations += "spawn" -> Vector3(960, 1002, 32) + zones("tzdrnc").locations += ( + "start" -> Vector3(2457, 1864, 23), + "air_pad" -> Vector3(1700, 1900, 32) + ) + zones("c1").gates += ( + "geowarp1" -> Vector3(998, 2038, 103), + "geowarp2" -> Vector3(231, 1026, 82), + "geowarp3" -> Vector3(2071, 1405, 102), + "geowarp4" -> Vector3(1051, 370, 103) + ) + zones("c2").gates += ( + "geowarp1" -> Vector3(999, 2386, 243), + "geowarp2" -> Vector3(283, 1249, 172), + "geowarp3" -> Vector3(1887, 1307, 192), + "geowarp4" -> Vector3(1039, 155, 143) + ) + zones("c3").gates += ( + "geowarp1" -> Vector3(1095, 1725, 25), + "geowarp2" -> Vector3(226, 832, 42), + "geowarp3" -> Vector3(1832, 1026, 43), + "geowarp4" -> Vector3(981, 320, 46) + ) + zones("c4").gates += ( + "geowarp1" -> Vector3(902, 1811, 93), + "geowarp2" -> Vector3(185, 922, 113), + "geowarp3" -> Vector3(1696, 1188, 92), + "geowarp4" -> Vector3(887, 227, 115) + ) + zones("c5").gates += ( + "geowarp1" -> Vector3(1195, 1752, 244), + "geowarp2" -> Vector3(290, 1104, 235), + "geowarp3" -> Vector3(1803, 899, 243), + "geowarp4" -> Vector3(1042, 225, 246) + ) + zones("c6").gates += ( + "geowarp1" -> Vector3(1067, 2044, 95), + "geowarp2" -> Vector3(290, 693, 73), + "geowarp3" -> Vector3(1922, 928, 33), + "geowarp4" -> Vector3(1174, 249, 114) + ) + zones("i3").gates += ( + "gate1" -> Vector3(1219, 2580, 30), + "gate2" -> Vector3(2889, 2919, 33), + "gate3" -> Vector3(2886, 1235, 32) + ) + zones("i3").locations += ( + "dahaka" -> Vector3(1421, 2216, 30), + "jamshid" -> Vector3(2500, 2543, 30), + "izha" -> Vector3(2569, 1544, 30), + "oasis" -> Vector3(2084, 1935, 40) + ) + zones("i2").gates += ( + "gate1" -> Vector3(1243, 1393, 12), + "gate2" -> Vector3(2510, 2544, 12), + "gate3" -> Vector3(2634, 1477, 12) + ) + zones("i2").locations += ( + "rashnu" -> Vector3(1709, 1802, 91), + "sraosha" -> Vector3(2729, 2349, 91), + "zal" -> Vector3(1888, 2728, 91), + "center" -> Vector3(2082, 2192, 160), + "vpad" -> Vector3(1770, 2686, 92) + ) + zones("i1").gates += ( + "gate1" -> Vector3(1225, 2036, 67), + "gate2" -> Vector3(2548, 2801, 65), + "gate3" -> Vector3(2481, 1194, 89) + ) + zones("i1").locations += ( + "hvar" -> Vector3(1559, 1268, 88), + "mithra" -> Vector3(2855, 2850, 89), + "yazata" -> Vector3(1254, 2583, 88), + "south_of_volcano" -> Vector3(2068, 1686, 88) + ) + zones("i4").gates += ( + "gate1" -> Vector3(2359, 2717, 36), + "gate2" -> Vector3(2732, 1355, 36), + "geowarp" -> Vector3(1424, 1640, 45) + ) + zones("i4").locations += "atar" -> Vector3(1915, 1936, 43) + } +} diff --git a/pslogin/src/main/scala/csr/Traveler.scala b/pslogin/src/main/scala/csr/Traveler.scala new file mode 100644 index 00000000..a60d4188 --- /dev/null +++ b/pslogin/src/main/scala/csr/Traveler.scala @@ -0,0 +1,37 @@ +package csr + +// Copyright (c) 2017 PSForever +import akka.actor.ActorRef +import net.psforever.packet.PlanetSidePacketContainer + +/* +The following is STILL for development and fun. +*/ +/** + * The traveler is synonymous with the player. + * The primary purpose of the object is to keep track of but not expose the player's session so that packets may be relayed back to him. + * csr.Traveler also keeps track of which zone the player currently occupies. + * @param session the player's session + */ +class Traveler(private val session : ActorRef, var zone : String) { + /** + * `sendToSelf` is a call that permits the session to gain access to its internal `rightRef` so that it can dispatch a packet. + * @param msg the byte-code translation of a packet + */ + def sendToSelf(msg : PlanetSidePacketContainer) : Unit = { + // this.session.sendResponse(msg) + } + + def !(msg : Any) : Unit = { + session ! msg + } +} + +object Traveler { + /** + * An abbreviated constructor for creating `csr.Traveler`s without invocation of `new`. + * @param session the player's session + * @return a traveler object for this player + */ + def apply(session : ActorRef, zoneId : String) : Traveler = new Traveler(session, zoneId) +} diff --git a/pslogin/src/main/scala/services/vehicle/VehicleAction.scala b/pslogin/src/main/scala/services/vehicle/VehicleAction.scala index c4505065..c876f340 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleAction.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleAction.scala @@ -16,6 +16,7 @@ object VehicleAction { final case class DeployRequest(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, state : DriveState.Value, unk1 : Int, unk2 : Boolean, pos : Vector3) extends Action final case class DismountVehicle(player_guid : PlanetSideGUID, bailType : BailType.Value, unk2 : Boolean) extends Action final case class InventoryState(player_guid : PlanetSideGUID, obj : PlanetSideGameObject, parent_guid : PlanetSideGUID, start : Int, con_data : ConstructorData) extends Action + final case class InventoryState2(player_guid : PlanetSideGUID, obj_guid : PlanetSideGUID, parent_guid : PlanetSideGUID, value : Int) extends Action final case class KickPassenger(player_guid : PlanetSideGUID, unk1 : Int, unk2 : Boolean, vehicle_guid : PlanetSideGUID) extends Action final case class LoadVehicle(player_guid : PlanetSideGUID, vehicle : Vehicle, vtype : Int, vguid : PlanetSideGUID, vdata : ConstructorData) extends Action final case class MountVehicle(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, seat : Int) extends Action diff --git a/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala b/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala index ce99e8e3..e29437cf 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala @@ -17,6 +17,7 @@ object VehicleResponse { final case class DetachFromRails(vehicle_guid : PlanetSideGUID, rails_guid : PlanetSideGUID, rails_pos : Vector3, rails_rot : Float) extends Response final case class DismountVehicle(bailType : BailType.Value , unk2 : Boolean) extends Response final case class InventoryState(obj : PlanetSideGameObject, parent_guid : PlanetSideGUID, start : Int, con_data : ConstructorData) extends Response + final case class InventoryState2(obj_guid : PlanetSideGUID, parent_guid : PlanetSideGUID, value : Int) extends Response final case class KickPassenger(seat_num : Int, kickedByDriver : Boolean, vehicle_guid : PlanetSideGUID) extends Response final case class LoadVehicle(vehicle : Vehicle, vtype : Int, vguid : PlanetSideGUID, vdata : ConstructorData) extends Response final case class MountVehicle(object_guid : PlanetSideGUID, seat : Int) extends Response diff --git a/pslogin/src/main/scala/services/vehicle/VehicleService.scala b/pslogin/src/main/scala/services/vehicle/VehicleService.scala index 6f335aa5..8fb490cd 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleService.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleService.scala @@ -60,6 +60,10 @@ class VehicleService extends Actor { VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.InventoryState(obj, parent_guid, start, con_data)) ) + case VehicleAction.InventoryState2(player_guid, obj_guid, parent_guid, value) => + VehicleEvents.publish( + VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.InventoryState2(obj_guid, parent_guid, value)) + ) case VehicleAction.KickPassenger(player_guid, seat_num, kickedByDriver, vehicle_guid) => VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.KickPassenger(seat_num, kickedByDriver, vehicle_guid)) diff --git a/pslogin/src/test/scala/VehicleServiceTest.scala b/pslogin/src/test/scala/VehicleServiceTest.scala index 82d0ba6e..261556c7 100644 --- a/pslogin/src/test/scala/VehicleServiceTest.scala +++ b/pslogin/src/test/scala/VehicleServiceTest.scala @@ -136,6 +136,22 @@ class InventoryStateTest extends ActorTest { } } +class InventoryState2Test extends ActorTest { + ServiceManager.boot(system) + val tool = Tool(GlobalDefinitions.beamer) + tool.AmmoSlots.head.Box.GUID = PlanetSideGUID(13) + val cdata = tool.Definition.Packet.ConstructorData(tool).get + + "VehicleService" should { + "pass InventoryState2" in { + val service = system.actorOf(Props[VehicleService], "v-service") + service ! Service.Join("test") + service ! VehicleServiceMessage("test", VehicleAction.InventoryState2(PlanetSideGUID(10), PlanetSideGUID(11), PlanetSideGUID(12), 13)) + expectMsg(VehicleServiceResponse("/test/Vehicle", PlanetSideGUID(10), VehicleResponse.InventoryState2(PlanetSideGUID(11), PlanetSideGUID(12), 13))) + } + } +} + class KickPassengerTest extends ActorTest { ServiceManager.boot(system)