diff --git a/build.sbt b/build.sbt index 04186d28..7e3106c7 100644 --- a/build.sbt +++ b/build.sbt @@ -48,8 +48,8 @@ lazy val commonSettings = Seq( "org.scala-graph" %% "graph-core" % "1.12.5", "io.kamon" %% "kamon-bundle" % "2.1.0", "io.kamon" %% "kamon-apm-reporter" % "2.1.0", - "org.json4s" %% "json4s-native" % "3.6.8" - ) + "org.json4s" %% "json4s-native" % "3.6.8", + ), ) lazy val pscryptoSettings = Seq( diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala index 451d85ff..02c83a26 100644 --- a/common/src/main/scala/net/psforever/objects/Avatar.scala +++ b/common/src/main/scala/net/psforever/objects/Avatar.scala @@ -57,33 +57,6 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet private var lfs : Boolean = false private var vehicleOwned : Option[PlanetSideGUID] = None - /** key - object id
- * value - time last used (ms) - * */ - private var lastUsedEquipmentTimes : mutable.LongMap[Long] = mutable.LongMap[Long]() - /** exo-suit times are sorted by `Enumeration` order, which was determined by packet process
- * key - exo-suit id
- * value - time last used (ms) - * */ - private val lastUsedExoSuitTimes : Array[Long] = Array.fill[Long](ExoSuitType.values.size)(0L) - /** mechanized exo-suit times are sorted by subtype distinction, which was determined by packet process
- * key - subtype id
- * value - time last used (ms) - * */ - private val lastUsedMaxExoSuitTimes : Array[Long] = Array.fill[Long](4)(0L) //invalid, ai, av, aa - /** key - object id
- * value - time last acquired (from a terminal) (ms) - * */ - private var lastPurchaseTimes : mutable.LongMap[Long] = mutable.LongMap[Long]() - /** - * To reload purchase and use timers, a string representing the item must be produced. - * Point directly from the object id to the object definition and get the `Name` from that definition. - * Allocate only when an item is purchased or used. - * The keys match the keys for both `lastUsedEquipmentTimes` and `lastPurchaseTimes`.
- * key - object id
- * value - most basic object definition information - */ - private val objectTypeNameReference : mutable.LongMap[String] = new mutable.LongMap[String]() def CharId : Long = char_id @@ -216,8 +189,7 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet def FifthSlot : EquipmentSlot = { new OffhandEquipmentSlot(EquipmentSize.Inventory) { - val obj = new LockerEquipment(locker) - Equipment = obj + Equipment = locker } } @@ -248,70 +220,6 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet VehicleOwned } - def GetLastUsedTime(code : Int) : Long = { - lastUsedEquipmentTimes.get(code) match { - case Some(time) => time - case None => 0 - } - } - - def GetLastUsedTime(code : ExoSuitType.Value) : Long = { - lastUsedExoSuitTimes(code.id) - } - - def GetLastUsedTime(code : ExoSuitType.Value, subtype : Int) : Long = { - if(code == ExoSuitType.MAX) { - lastUsedMaxExoSuitTimes(subtype) - } - else { - GetLastUsedTime(code) - } - } - - def GetAllLastUsedTimes : Map[Long, Long] = lastUsedEquipmentTimes.toMap - - def SetLastUsedTime(code : Int, time : Long) : Unit = { - lastUsedEquipmentTimes += code.toLong -> time - } - - def SetLastUsedTime(code : ExoSuitType.Value) : Unit = SetLastUsedTime(code, System.currentTimeMillis()) - - def SetLastUsedTime(code : ExoSuitType.Value, time : Long) : Unit = { - lastUsedExoSuitTimes(code.id) = time - } - - def SetLastUsedTime(code : ExoSuitType.Value, subtype : Int, time : Long) : Unit = { - if(code == ExoSuitType.MAX) { - lastUsedMaxExoSuitTimes(subtype) = time - } - SetLastUsedTime(code, time) - } - - def GetLastPurchaseTime(code : Int) : Long = { - lastPurchaseTimes.get(code) match { - case Some(time) => time - case None => 0 - } - } - - def GetAllLastPurchaseTimes : Map[Long, Long] = lastPurchaseTimes.toMap - - def SetLastPurchaseTime(code : Int, time : Long) : Unit = { - lastPurchaseTimes += code.toLong -> time - } - - def ObjectTypeNameReference(id : Long) : String = { - objectTypeNameReference.get(id) match { - case Some(name) => name - case None => "" - } - } - - def ObjectTypeNameReference(id : Long, name : String) : String = { - objectTypeNameReference(id) = name - name - } - def Definition : AvatarDefinition = GlobalDefinitions.avatar /* diff --git a/common/src/main/scala/net/psforever/objects/Deployables.scala b/common/src/main/scala/net/psforever/objects/Deployables.scala index 25b2a69f..a241e978 100644 --- a/common/src/main/scala/net/psforever/objects/Deployables.scala +++ b/common/src/main/scala/net/psforever/objects/Deployables.scala @@ -8,7 +8,7 @@ import net.psforever.objects.ce.{Deployable, DeployedItem} import net.psforever.objects.vehicles.{Utility, UtilityType} import net.psforever.objects.zones.Zone import net.psforever.packet.game.{DeployableInfo, DeploymentAction} -import net.psforever.types.{CertificationType, PlanetSideGUID} +import net.psforever.types.PlanetSideGUID import services.RemoverActor import services.local.{LocalAction, LocalServiceMessage} @@ -123,48 +123,4 @@ object Deployables { case _ => ; } } - - - - /** - * Initialize the deployables backend information. - * @param avatar the player's core - */ - def InitializeDeployableQuantities(avatar : Avatar) : Boolean = { - log.info("Setting up combat engineering ...") - avatar.Deployables.Initialize(avatar.Certifications.toSet) - } - - /** - * Initialize the UI elements for deployables. - * @param avatar the player's core - */ - def InitializeDeployableUIElements(avatar : Avatar) : List[(Int,Int,Int,Int)] = { - log.info("Setting up combat engineering UI ...") - avatar.Deployables.UpdateUI() - } - - /** - * The player learned a new certification. - * Update the deployables user interface elements if it was an "Engineering" certification. - * The certification "Advanced Hacking" also relates to an element. - * @param certification the certification that was added - * @param certificationSet all applicable certifications - */ - def AddToDeployableQuantities(avatar : Avatar, certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : List[(Int,Int,Int,Int)] = { - avatar.Deployables.AddToDeployableQuantities(certification, certificationSet) - avatar.Deployables.UpdateUI(certification) - } - - /** - * The player forgot a certification he previously knew. - * Update the deployables user interface elements if it was an "Engineering" certification. - * The certification "Advanced Hacking" also relates to an element. - * @param certification the certification that was added - * @param certificationSet all applicable certifications - */ - def RemoveFromDeployableQuantities(avatar : Avatar, certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : List[(Int,Int,Int,Int)] = { - avatar.Deployables.RemoveFromDeployableQuantities(certification, certificationSet) - avatar.Deployables.UpdateUI(certification) - } } diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 572b4803..c8541002 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -1703,11 +1703,11 @@ object GlobalDefinitions { plasma_grenade_ammo.Name = "plasma_grenade_ammo" plasma_grenade_ammo.Size = EquipmentSize.Blocked - bullet_9mm.Name = "9mmbullet" + bullet_9mm.Name = "bullet_9mm" bullet_9mm.Capacity = 50 bullet_9mm.Tile = InventoryTile.Tile33 - bullet_9mm_AP.Name="9mmbullet_AP" + bullet_9mm_AP.Name="bullet_9mm_AP" bullet_9mm_AP.Capacity = 50 bullet_9mm_AP.Tile = InventoryTile.Tile33 @@ -1841,7 +1841,7 @@ object GlobalDefinitions { trek_ammo.Name = "trek_ammo" trek_ammo.Size = EquipmentSize.Blocked - bullet_35mm.Name = "35mmbullet" + bullet_35mm.Name = "bullet_35mm" bullet_35mm.Capacity = 100 bullet_35mm.Tile = InventoryTile.Tile44 @@ -1889,11 +1889,11 @@ object GlobalDefinitions { liberator_bomb.Capacity = 20 liberator_bomb.Tile = InventoryTile.Tile44 - bullet_25mm.Name = "25mmbullet" + bullet_25mm.Name = "bullet_25mm" bullet_25mm.Capacity = 150 bullet_25mm.Tile = InventoryTile.Tile44 - bullet_75mm.Name = "75mmbullet" + bullet_75mm.Name = "bullet_75mm" bullet_75mm.Capacity = 100 bullet_75mm.Tile = InventoryTile.Tile44 @@ -1913,11 +1913,11 @@ object GlobalDefinitions { reaver_rocket.Capacity = 12 reaver_rocket.Tile = InventoryTile.Tile44 - bullet_20mm.Name = "20mmbullet" + bullet_20mm.Name = "bullet_20mm" bullet_20mm.Capacity = 200 bullet_20mm.Tile = InventoryTile.Tile44 - bullet_12mm.Name = "12mmbullet" + bullet_12mm.Name = "bullet_12mm" bullet_12mm.Capacity = 300 bullet_12mm.Tile = InventoryTile.Tile44 @@ -1929,7 +1929,7 @@ object GlobalDefinitions { wasp_gun_ammo.Capacity = 150 wasp_gun_ammo.Tile = InventoryTile.Tile44 - bullet_15mm.Name = "15mmbullet" + bullet_15mm.Name = "bullet_15mm" bullet_15mm.Capacity = 360 bullet_15mm.Tile = InventoryTile.Tile44 @@ -1953,7 +1953,7 @@ object GlobalDefinitions { colossus_tank_cannon_ammo.Capacity = 110 colossus_tank_cannon_ammo.Tile = InventoryTile.Tile44 - bullet_105mm.Name = "105mmbullet" + bullet_105mm.Name = "bullet_105mm" bullet_105mm.Capacity = 100 bullet_105mm.Tile = InventoryTile.Tile44 @@ -1981,7 +1981,7 @@ object GlobalDefinitions { peregrine_sparrow_ammo.Capacity = 150 peregrine_sparrow_ammo.Tile = InventoryTile.Tile44 - bullet_150mm.Name = "150mmbullet" + bullet_150mm.Name = "bullet_150mm" bullet_150mm.Capacity = 50 bullet_150mm.Tile = InventoryTile.Tile44 diff --git a/common/src/main/scala/net/psforever/objects/LockerContainer.scala b/common/src/main/scala/net/psforever/objects/LockerContainer.scala index 02ab4471..7793a6a5 100644 --- a/common/src/main/scala/net/psforever/objects/LockerContainer.scala +++ b/common/src/main/scala/net/psforever/objects/LockerContainer.scala @@ -1,17 +1,9 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import akka.actor.Actor import net.psforever.objects.definition.EquipmentDefinition import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.{Container, GridInventory} -import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior} -import net.psforever.packet.game.{ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectDetachMessage} -import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent -import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} -import services.Service -import services.avatar.{AvatarAction, AvatarServiceMessage} /** * The companion of a `Locker` that is carried with a player @@ -19,18 +11,9 @@ import services.avatar.{AvatarAction, AvatarServiceMessage} * The `Player` class refers to it as the "fifth slot" as its permanent slot number is encoded as `0x85`. * The inventory of this object is accessed using a game world `Locker` object (`mb_locker`). */ -class LockerContainer extends PlanetSideServerObject - with Container { - private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL +class LockerContainer extends Equipment with Container { private val inventory = GridInventory(30, 20) - def Faction : PlanetSideEmpire.Value = faction - - override def Faction_=(fact : PlanetSideEmpire.Value) : PlanetSideEmpire.Value = { - faction = fact - Faction - } - def Inventory : GridInventory = inventory def VisibleSlots : Set[Int] = Set.empty[Int] @@ -43,79 +26,3 @@ object LockerContainer { new LockerContainer() } } - -class LockerEquipment(locker : LockerContainer) extends Equipment - with Container { - private val obj = locker - - override def GUID : PlanetSideGUID = obj.GUID - - override def GUID_=(guid : PlanetSideGUID) : PlanetSideGUID = obj.GUID_=(guid) - - override def HasGUID : Boolean = obj.HasGUID - - override def Invalidate() : Unit = obj.Invalidate() - - override def Faction : PlanetSideEmpire.Value = obj.Faction - - def Inventory : GridInventory = obj.Inventory - - def VisibleSlots : Set[Int] = Set.empty[Int] - - def Definition : EquipmentDefinition = obj.Definition -} - -class LockerContainerControl(locker : LockerContainer, toChannel : String) extends Actor - with ContainableBehavior { - def ContainerObject = locker - - def receive : Receive = containerBehavior - .orElse { - case _ => ; - } - - def MessageDeferredCallback(msg : Any) : Unit = { - msg match { - case Containable.MoveItem(_, item, _) => - //momentarily put item back where it was originally - val obj = ContainerObject - obj.Find(item) match { - case Some(slot) => - obj.Zone.AvatarEvents ! AvatarServiceMessage( - toChannel, - AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectAttachMessage(obj.GUID, item.GUID, slot)) - ) - case None => ; - } - case _ => ; - } - } - - def RemoveItemFromSlotCallback(item : Equipment, slot : Int) : Unit = { - val zone = locker.Zone - zone.AvatarEvents ! AvatarServiceMessage(toChannel, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID)) - } - - def PutItemInSlotCallback(item : Equipment, slot : Int) : Unit = { - val zone = locker.Zone - val definition = item.Definition - item.Faction = PlanetSideEmpire.NEUTRAL - zone.AvatarEvents ! AvatarServiceMessage( - toChannel, - AvatarAction.SendResponse( - Service.defaultPlayerGUID, - ObjectCreateDetailedMessage( - definition.ObjectId, - item.GUID, - ObjectCreateMessageParent(locker.GUID, slot), - definition.Packet.DetailedConstructorData(item).get - ) - ) - ) - } - - def SwapItemCallback(item : Equipment) : Unit = { - val zone = locker.Zone - zone.AvatarEvents ! AvatarServiceMessage(toChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectDetachMessage(locker.GUID, item.GUID, Vector3.Zero, 0f))) - } -} diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index ff1b6e14..5ba47533 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -216,8 +216,6 @@ class Player(private val core : Avatar) extends PlanetSideServerObject def Locker : LockerContainer = core.Locker - def FifthSlot : EquipmentSlot = core.FifthSlot - override def Fit(obj : Equipment) : Option[Int] = { recursiveHolsterFit(holsters.iterator, obj.Size) match { case Some(index) => @@ -610,30 +608,6 @@ class Player(private val core : Avatar) extends PlanetSideServerObject def VehicleOwned_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = core.VehicleOwned_=(guid) - def GetLastUsedTime(code : Int) : Long = core.GetLastUsedTime(code) - - def GetLastUsedTime(code : ExoSuitType.Value) : Long = core.GetLastUsedTime(code) - - def GetLastUsedTime(code : ExoSuitType.Value, subtype : Int) : Long = core.GetLastUsedTime(code, subtype) - - def SetLastUsedTime(code : Int, time : Long) : Unit = core.SetLastUsedTime(code, time) - - def SetLastUsedTime(code : ExoSuitType.Value): Unit = core.SetLastUsedTime(code) - - def SetLastUsedTime(code : ExoSuitType.Value, time : Long) : Unit = core.SetLastUsedTime(code, time) - - def SetLastUsedTime(code : ExoSuitType.Value, subtype : Int): Unit = core.SetLastUsedTime(code, subtype) - - def SetLastUsedTime(code : ExoSuitType.Value, subtype : Int, time : Long) : Unit = core.SetLastUsedTime(code, subtype, time) - - def GetLastPurchaseTime(code : Int) : Long = core.GetLastPurchaseTime(code) - - def SetLastPurchaseTime(code : Int, time : Long) : Unit = core.SetLastPurchaseTime(code, time) - - def ObjectTypeNameReference(id : Long) : String = core.ObjectTypeNameReference(id) - - def ObjectTypeNameReference(id : Long, name : String) : String = core.ObjectTypeNameReference(id, name) - def DamageModel = exosuit.asInstanceOf[DamageResistanceModel] def Definition : AvatarDefinition = core.Definition diff --git a/common/src/main/scala/net/psforever/objects/Players.scala b/common/src/main/scala/net/psforever/objects/Players.scala index 9260da45..486265c7 100644 --- a/common/src/main/scala/net/psforever/objects/Players.scala +++ b/common/src/main/scala/net/psforever/objects/Players.scala @@ -1,17 +1,10 @@ // Copyright (c) 2020 PSForever package net.psforever.objects -import net.psforever.objects.definition.ExoSuitDefinition -import net.psforever.objects.equipment.EquipmentSlot -import net.psforever.objects.inventory.InventoryItem -import net.psforever.objects.loadouts.InfantryLoadout import net.psforever.packet.game.{InventoryStateMessage, RepairMessage} -import net.psforever.types.ExoSuitType import services.Service import services.avatar.{AvatarAction, AvatarServiceMessage} -import scala.annotation.tailrec - object Players { private val log = org.log4s.getLogger("Players") @@ -53,71 +46,4 @@ object Players { log.info(s"$medic had revived $name") target.Zone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.Revive(target.GUID)) } - - /** - * Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item. - * Remove any encountered items and add them to an output `List`. - * @param iter the `Iterator` of `EquipmentSlot`s - * @param index a number that equals the "current" holster slot (`EquipmentSlot`) - * @param list a persistent `List` of `Equipment` in the holster slots - * @return a `List` of `Equipment` in the holster slots - */ - @tailrec def clearHolsters(iter : Iterator[EquipmentSlot], index : Int = 0, list : List[InventoryItem] = Nil) : List[InventoryItem] = { - if(!iter.hasNext) { - list - } - else { - val slot = iter.next - slot.Equipment match { - case Some(equipment) => - slot.Equipment = None - clearHolsters(iter, index + 1, InventoryItem(equipment, index) +: list) - case None => - clearHolsters(iter, index + 1, list) - } - } - } - - /** - * Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item. - * For any slots that are not yet occupied by an item, search through the `List` and find an item that fits in that slot. - * Add that item to the slot and remove it from the list. - * @param iter the `Iterator` of `EquipmentSlot`s - * @param list a `List` of all `Equipment` that is not yet assigned to a holster slot or an inventory slot - * @return the `List` of all `Equipment` not yet assigned to a holster slot or an inventory slot - */ - @tailrec def fillEmptyHolsters(iter : Iterator[EquipmentSlot], list : List[InventoryItem]) : List[InventoryItem] = { - if(!iter.hasNext) { - list - } - else { - val slot = iter.next - if(slot.Equipment.isEmpty) { - list.find(item => item.obj.Size == slot.Size) match { - case Some(obj) => - val index = list.indexOf(obj) - slot.Equipment = obj.obj - fillEmptyHolsters(iter, list.take(index) ++ list.drop(index + 1)) - case None => - fillEmptyHolsters(iter, list) - } - } - else { - fillEmptyHolsters(iter, list) - } - } - } - - def CertificationToUseExoSuit(player : Player, exosuit : ExoSuitType.Value, subtype : Int) : Boolean = { - ExoSuitDefinition.Select(exosuit, player.Faction).Permissions match { - case Nil => - true - case permissions if subtype != 0 => - val certs = player.Certifications - certs.intersect(permissions.toSet).nonEmpty && - certs.intersect(InfantryLoadout.DetermineSubtypeC(subtype)).nonEmpty - case permissions => - player.Certifications.intersect(permissions.toSet).nonEmpty - } - } } diff --git a/common/src/main/scala/net/psforever/objects/Vehicle.scala b/common/src/main/scala/net/psforever/objects/Vehicle.scala index 8a872f6a..6db09377 100644 --- a/common/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/common/src/main/scala/net/psforever/objects/Vehicle.scala @@ -4,7 +4,7 @@ package net.psforever.objects import akka.actor.ActorRef import net.psforever.objects.definition.VehicleDefinition import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} -import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile} +import net.psforever.objects.inventory.{Container, GridInventory, InventoryTile} import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity @@ -17,7 +17,6 @@ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} import scala.annotation.tailrec import scala.concurrent.duration.FiniteDuration -import scala.util.{Success, Try} /** * The server-side support object that represents a vehicle.
@@ -453,20 +452,6 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends AmenityOwner } } - override def Collisions(dest : Int, width : Int, height : Int) : Try[List[InventoryItem]] = { - weapons.get(dest) match { - case Some(slot) => - slot.Equipment match { - case Some(item) => - Success(List(InventoryItem(item, dest))) - case None => - Success(List()) - } - case None => - super.Collisions(dest, width, height) - } - } - /** * A reference to the `Vehicle` `Trunk` space. * @return this `Vehicle` `Trunk` diff --git a/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index d283f24f..4588b083 100644 --- a/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -1,28 +1,22 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.avatar -import akka.actor.{Actor, ActorRef, Props} -import net.psforever.objects.{Players, _} -import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile} +import akka.actor.Actor +import net.psforever.objects.{DefaultCancellable, GlobalDefinitions, ImplantSlot, Player, Players, Tool} +import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile, SourceEntry} import net.psforever.objects.definition.ImplantDefinition -import net.psforever.objects.equipment.{Ammo, Equipment, EquipmentSize, JammableBehavior, JammableUnit} -import net.psforever.objects.inventory.{GridInventory, InventoryItem} -import net.psforever.objects.loadouts.Loadout -import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior} +import net.psforever.objects.equipment.{Ammo, JammableBehavior, JammableUnit} import net.psforever.objects.vital.{PlayerSuicide, Vitality} -import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.repair.Repairable -import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.vital._ import net.psforever.objects.zones.Zone import net.psforever.packet.game._ -import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent -import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} -import services.{RemoverActor, Service} +import net.psforever.types.{ExoSuitType, ImplantType, PlanetSideGUID, Vector3} +import services.Service import services.avatar.{AvatarAction, AvatarServiceMessage} -import services.local.{LocalAction, LocalServiceMessage} import scala.concurrent.duration._ import scala.collection.mutable @@ -31,119 +25,112 @@ import scala.concurrent.ExecutionContext.Implicits.global class PlayerControl(player : Player) extends Actor with JammableBehavior - with Damageable - with ContainableBehavior { + with Damageable { def JammableObject = player def DamageableObject = player - def ContainerObject = player - private[this] val log = org.log4s.getLogger(player.Name) - private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel) + private [this] val log = org.log4s.getLogger(player.Name) + private [this] val damageLog = org.log4s.getLogger(Damageable.LogChannel) + // A collection of timers for each slot to trigger stamina drain on an interval val implantSlotStaminaDrainTimers = mutable.HashMap(0 -> DefaultCancellable.obj, 1 -> DefaultCancellable.obj, 2 -> DefaultCancellable.obj) - // control agency for the player's locker container (dedicated inventory slot #5) - val lockerControlAgent : ActorRef = { - val locker = player.Locker - locker.Zone = player.Zone - locker.Actor = context.actorOf(Props(classOf[LockerContainerControl], locker, player.Name), PlanetSideServerObject.UniqueActorName(locker)) - } - - override def postStop() : Unit = { - context.stop(lockerControlAgent) - player.Locker.Actor = ActorRef.noSender - implantSlotStaminaDrainTimers.values.foreach { _.cancel } - } def receive : Receive = jammableBehavior .orElse(takesDamage) - .orElse(containerBehavior) .orElse { - case Player.ImplantActivation(slot : Int, status : Int) => - // todo: disable implants with stamina cost when changing armour type - val implantSlot = player.ImplantSlot(slot) + case Player.ImplantActivation(slot: Int, status : Int) => + // todo: disable implants with stamina cost when changing armour type + val implantSlot = player.ImplantSlot(slot) - // Allow uninitialized implants to be deactivated in case they're stuck in a state where they are no longer active or initialized but still draining stamina (e.g. by EMP) - if (status == 0 && (implantSlot.Active || !implantSlot.Initialized)) { - // Cancel stamina drain timer - implantSlotStaminaDrainTimers(slot).cancel() - implantSlotStaminaDrainTimers(slot) = DefaultCancellable.obj - implantSlot.Active = false - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.DeactivateImplantSlot(player.GUID, slot)) + // Allow uninitialized implants to be deactivated in case they're stuck in a state where they are no longer active or initialized but still draining stamina (e.g. by EMP) + if(status == 0 && (implantSlot.Active || !implantSlot.Initialized)) { + // Cancel stamina drain timer + implantSlotStaminaDrainTimers(slot).cancel() + implantSlotStaminaDrainTimers(slot) = DefaultCancellable.obj + + implantSlot.Active = false + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.DeactivateImplantSlot(player.GUID, slot)) + } else if (status == 1 && implantSlot.Initialized && !player.Fatigued) { + implantSlot.Installed match { + case Some(implant: ImplantDefinition) => + if(implantSlot.Active) { + // Some events such as zoning will reset the implant on the client side without sending a deactivation packet + // But the implant will remain in an active state server side. For now, allow reactivation of the implant. + // todo: Deactivate implants server side when actions like zoning happen. (Other actions?) + log.warn(s"Implant ${slot} is already active, but activating again") + implantSlotStaminaDrainTimers(slot).cancel() + implantSlotStaminaDrainTimers(slot) = DefaultCancellable.obj + } + implantSlot.Active = true + + if (implant.ActivationStaminaCost >= 0) { + player.Stamina -= implant.ActivationStaminaCost // Activation stamina drain + } + + if(implant.StaminaCost > 0 && implant.GetCostIntervalByExoSuit(player.ExoSuit) > 0) { // Ongoing stamina drain, if applicable + implantSlotStaminaDrainTimers(slot) = context.system.scheduler.schedule(0 seconds, implant.GetCostIntervalByExoSuit(player.ExoSuit) milliseconds, self, Player.DrainStamina(implant.StaminaCost)) + } + + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2 + 1)) // Activation sound / effect + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.ActivateImplantSlot(player.GUID, slot)) + case _ => ; } - else if (status == 1 && implantSlot.Initialized && !player.Fatigued) { - implantSlot.Installed match { - case Some(implant : ImplantDefinition) => - if (implantSlot.Active) { - // Some events such as zoning will reset the implant on the client side without sending a deactivation packet - // But the implant will remain in an active state server side. For now, allow reactivation of the implant. - // todo: Deactivate implants server side when actions like zoning happen. (Other actions?) - log.warn(s"Implant $slot is already active, but activating again") - implantSlotStaminaDrainTimers(slot).cancel() - implantSlotStaminaDrainTimers(slot) = DefaultCancellable.obj - } - implantSlot.Active = true - if (implant.ActivationStaminaCost >= 0) { - player.Stamina -= implant.ActivationStaminaCost // Activation stamina drain - } - if (implant.StaminaCost > 0 && implant.GetCostIntervalByExoSuit(player.ExoSuit) > 0) { // Ongoing stamina drain, if applicable - implantSlotStaminaDrainTimers(slot) = context.system.scheduler.schedule(0 seconds, implant.GetCostIntervalByExoSuit(player.ExoSuit) milliseconds, self, Player.DrainStamina(implant.StaminaCost)) - } - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2 + 1)) // Activation sound / effect - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.ActivateImplantSlot(player.GUID, slot)) - case _ => ; - } + } + else { + log.warn(s"Can't handle ImplantActivation: Player GUID: ${player.GUID} Slot ${slot} Status: ${status} Initialized: ${implantSlot.Initialized} Active: ${implantSlot.Active} Fatigued: ${player.Fatigued}") + } + + case Player.UninitializeImplant(slot: Int) => { + PlayerControl.UninitializeImplant(player, slot) + } + + case Player.ImplantInitializationStart(slot: Int) => + val implantSlot = player.ImplantSlot(slot) + if(implantSlot.Installed.isDefined) { + if(implantSlot.Initialized) { + PlayerControl.UninitializeImplant(player, slot) } - case Player.UninitializeImplant(slot : Int) => - PlayerControl.UninitializeImplant(player, slot) + // Start client side initialization timer + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(player.GUID, ActionProgressMessage(slot + 6, 0))) - case Player.ImplantInitializationStart(slot : Int) => - val implantSlot = player.ImplantSlot(slot) - if (implantSlot.Installed.isDefined) { - if (implantSlot.Initialized) { - PlayerControl.UninitializeImplant(player, slot) - } + // Callback after initialization timer to complete initialization + implantSlot.InitializeTimer = context.system.scheduler.scheduleOnce(implantSlot.MaxTimer seconds, self, Player.ImplantInitializationComplete(slot)) + } - // Start client side initialization timer - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(player.GUID, ActionProgressMessage(slot + 6, 0))) - - // Callback after initialization timer to complete initialization - implantSlot.InitializeTimer = context.system.scheduler.scheduleOnce(implantSlot.MaxTimer seconds, self, Player.ImplantInitializationComplete(slot)) + case Player.ImplantInitializationComplete(slot: Int) => + val implantSlot = player.ImplantSlot(slot) + if(implantSlot.Installed.isDefined) { + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.Initialization, slot, 1))) + implantSlot.Initialized = true + if(implantSlot.InitializeTimer != DefaultCancellable.obj) { + implantSlot.InitializeTimer.cancel() + implantSlot.InitializeTimer = DefaultCancellable.obj } + } - case Player.ImplantInitializationComplete(slot : Int) => - val implantSlot = player.ImplantSlot(slot) - if (implantSlot.Installed.isDefined) { - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.Initialization, slot, 1))) - implantSlot.Initialized = true - if (implantSlot.InitializeTimer != DefaultCancellable.obj) { - implantSlot.InitializeTimer.cancel() - implantSlot.InitializeTimer = DefaultCancellable.obj - } - } + case Player.DrainStamina(amount : Int) => + player.Stamina -= amount - case Player.DrainStamina(amount : Int) => - player.Stamina -= amount + case Player.StaminaChanged(currentStamina : Int) => + if(currentStamina == 0) { + player.Fatigued = true + player.skipStaminaRegenForTurns += 4 + for(slot <- 0 to player.Implants.length - 1) { // Disable all implants + self ! Player.ImplantActivation(slot, 0) + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponseTargeted(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 1))) + } + } else if (player.Fatigued && currentStamina >= 20) { + player.Fatigued = false + for(slot <- 0 to player.Implants.length - 1) { // Re-enable all implants + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponseTargeted(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 0))) + } + } - case Player.StaminaChanged(currentStamina : Int) => - if (currentStamina == 0) { - player.Fatigued = true - player.skipStaminaRegenForTurns += 4 - player.Implants.indices.foreach { slot => // Disable all implants - self ! Player.ImplantActivation(slot, 0) - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponseTargeted(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 1))) - } - } - else if (player.Fatigued && currentStamina >= 20) { - player.Fatigued = false - player.Implants.indices.foreach { slot => // Re-enable all implants - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponseTargeted(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 0))) - } - } - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttributeSelf(player.GUID, 2, player.Stamina)) + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttributeSelf(player.GUID, 2, player.Stamina)) case Player.Die() => - if (player.isAlive) { + if(player.isAlive) { PlayerControl.DestructionAwareness(player, None) } @@ -151,7 +138,7 @@ class PlayerControl(player : Player) extends Actor //heal val originalHealth = player.Health val definition = player.Definition - if (player.MaxHealth > 0 && originalHealth < player.MaxHealth && + if(player.MaxHealth > 0 && originalHealth < player.MaxHealth && user.Faction == player.Faction && item.Magazine > 0 && Vector3.Distance(user.Position, player.Position) < definition.RepairDistance) { @@ -159,14 +146,14 @@ class PlayerControl(player : Player) extends Actor val events = zone.AvatarEvents val uname = user.Name val guid = player.GUID - if (!(player.isMoving || user.isMoving)) { //only allow stationary heals + if(!(player.isMoving || user.isMoving)) { //only allow stationary heals val newHealth = player.Health = originalHealth + 10 val magazine = item.Discharge events ! AvatarServiceMessage(uname, AvatarAction.SendResponse(Service.defaultPlayerGUID, InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong))) events ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttributeToAll(guid, 0, newHealth)) player.History(HealFromEquipment(PlayerSource(player), PlayerSource(user), newHealth - originalHealth, GlobalDefinitions.medicalapplicator)) } - if (player != user) { + if(player != user) { //"Someone is trying to heal you" events ! AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(guid, 55, 1)) //progress bar remains visible for all heal attempts @@ -176,7 +163,7 @@ class PlayerControl(player : Player) extends Actor case CommonMessages.Use(user, Some(item : Tool)) if item.Definition == GlobalDefinitions.medicalapplicator => //revive - if (user != player && + if(user != player && user.Faction == player.Faction && user.isAlive && !user.isMoving && !player.isAlive && !player.isBackpack && @@ -191,7 +178,7 @@ class PlayerControl(player : Player) extends Actor case CommonMessages.Use(user, Some(item : Tool)) if item.Definition == GlobalDefinitions.bank => val originalArmor = player.Armor val definition = player.Definition - if (player.MaxArmor > 0 && originalArmor < player.MaxArmor && + if(player.MaxArmor > 0 && originalArmor < player.MaxArmor && user.Faction == player.Faction && item.AmmoType == Ammo.armor_canister && item.Magazine > 0 && Vector3.Distance(user.Position, player.Position) < definition.RepairDistance) { @@ -199,15 +186,15 @@ class PlayerControl(player : Player) extends Actor val events = zone.AvatarEvents val uname = user.Name val guid = player.GUID - if (!(player.isMoving || user.isMoving)) { //only allow stationary repairs + if(!(player.isMoving || user.isMoving)) { //only allow stationary repairs val newArmor = player.Armor = originalArmor + Repairable.Quality + RepairValue(item) + definition.RepairMod val magazine = item.Discharge events ! AvatarServiceMessage(uname, AvatarAction.SendResponse(Service.defaultPlayerGUID, InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong))) events ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttributeToAll(guid, 4, player.Armor)) player.History(RepairFromEquipment(PlayerSource(player), PlayerSource(user), newArmor - originalArmor, GlobalDefinitions.bank)) } - if (player != user) { - if (player.isAlive) { + if(player != user) { + if(player.isAlive) { //"Someone is trying to repair you" gets strobed twice for visibility val msg = AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(guid, 56, 1)) events ! msg @@ -219,263 +206,6 @@ class PlayerControl(player : Player) extends Actor } } - case Terminal.TerminalMessage(_, msg, order) => - order match { - case Terminal.BuyExosuit(exosuit, subtype) => - val time = System.currentTimeMillis - var toDelete : List[InventoryItem] = Nil - val originalSuit = player.ExoSuit - val originalSubtype = Loadout.DetermineSubtype(player) - val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype - val allowedToChangeArmor = Players.CertificationToUseExoSuit(player, exosuit, subtype) && - (if (exosuit == ExoSuitType.MAX) { - if (time - player.GetLastUsedTime(exosuit, subtype) < 30000L) { - false - } - else { - player.SetLastUsedTime(exosuit, subtype, time) - true - } - } - else { - player.SetLastUsedTime(exosuit, subtype, time) - true - }) - val result = if (requestToChangeArmor && allowedToChangeArmor) { - log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit") - player.SetLastUsedTime(exosuit, subtype, System.currentTimeMillis()) - val beforeHolsters = Players.clearHolsters(player.Holsters().iterator) - val beforeInventory = player.Inventory.Clear() - //change suit - val originalArmor = player.Armor - player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit - val toMaxArmor = player.MaxArmor - if (originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) { - player.History(HealFromExoSuitChange(PlayerSource(player), exosuit)) - player.Armor = toMaxArmor - } - else { - player.Armor = originalArmor - } - //ensure arm is down, even if it needs to go back up - if (player.DrawnSlot != Player.HandsDownSlot) { - player.DrawnSlot = Player.HandsDownSlot - } - val normalHolsters = if (originalSuit == ExoSuitType.MAX) { - val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max) - toDelete ++= maxWeapons - normalWeapons - } - else { - beforeHolsters - } - //populate holsters - val (afterHolsters, finalInventory) = if (exosuit == ExoSuitType.MAX) { - (normalHolsters, Players.fillEmptyHolsters(List(player.Slot(4)).iterator, normalHolsters) ++ beforeInventory) - } - else if (originalSuit == exosuit) { //note - this will rarely be the situation - (normalHolsters, Players.fillEmptyHolsters(player.Holsters().iterator, normalHolsters)) - } - else { - val (afterHolsters, toInventory) = normalHolsters.partition(elem => elem.obj.Size == player.Slot(elem.start).Size) - afterHolsters.foreach({ elem => player.Slot(elem.start).Equipment = elem.obj }) - val remainder = Players.fillEmptyHolsters(player.Holsters().iterator, toInventory ++ beforeInventory) - ( - player.Holsters() - .zipWithIndex - .map { case (slot, i) => (slot.Equipment, i) } - .collect { case (Some(obj), index) => InventoryItem(obj, index) } - .toList, - remainder - ) - } - //put items back into inventory - val (stow, drop) = if (originalSuit == exosuit) { - (finalInventory, Nil) - } - else { - val (a, b) = GridInventory.recoverInventory(finalInventory, player.Inventory) - (a, b.map { - InventoryItem(_, -1) - }) - } - stow.foreach { elem => - player.Inventory.InsertQuickly(elem.start, elem.obj) - } - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, - AvatarAction.ChangeExosuit(player.GUID, exosuit, subtype, player.LastDrawnSlot, exosuit == ExoSuitType.MAX && requestToChangeArmor, - beforeHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterHolsters, - beforeInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, stow, drop, - toDelete.map { case InventoryItem(obj, _) => (obj, obj.GUID) } - ) - ) - true - } - else { - false - } - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result)) - - case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) => - log.info(s"wants to change equipment loadout to their option #${msg.unk1 + 1}") - val fallbackSubtype = 0 - val fallbackSuit = ExoSuitType.Standard - val originalSuit = player.ExoSuit - val originalSubtype = Loadout.DetermineSubtype(player) - //sanitize exo-suit for change - val dropPred = ContainableBehavior.DropPredicate(player) - val oldHolsters = Players.clearHolsters(player.Holsters().iterator) - val dropHolsters = oldHolsters.filter(dropPred) - val oldInventory = player.Inventory.Clear() - val dropInventory = oldInventory.filter(dropPred) - val toDeleteOrDrop : List[InventoryItem] = (player.FreeHand.Equipment match { - case Some(obj) => - val out = InventoryItem(obj, -1) - player.FreeHand.Equipment = None - if (dropPred(out)) { - List(out) - } - else { - Nil - } - case _ => - Nil - }) ++ dropHolsters ++ dropInventory - //a loadout with a prohibited exo-suit type will result in the fallback exo-suit type - //imposed 5min delay on mechanized exo-suit switches - val time = System.currentTimeMillis() - val (nextSuit, nextSubtype) = if (Players.CertificationToUseExoSuit(player, exosuit, subtype) && - (if (exosuit == ExoSuitType.MAX) { - if (time - player.GetLastUsedTime(exosuit, subtype) < 30000L) { - false - } - else { - player.SetLastUsedTime(exosuit, subtype, time) - true - } - } - else { - player.SetLastUsedTime(exosuit, subtype, time) - true - })) { - (exosuit, subtype) - } - else { - log.warn(s"no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead") - player.SetLastUsedTime(fallbackSuit, fallbackSubtype, time) - (fallbackSuit, fallbackSubtype) - } - //sanitize (incoming) inventory - //TODO equipment permissions; these loops may be expanded upon in future - val curatedHolsters = for { - item <- holsters - //id = item.obj.Definition.ObjectId - //lastTime = player.GetLastUsedTime(id) - if true - } yield item - val curatedInventory = for { - item <- inventory - //id = item.obj.Definition.ObjectId - //lastTime = player.GetLastUsedTime(id) - if true - } yield item - //update suit internally - val originalArmor = player.Armor - player.ExoSuit = nextSuit - val toMaxArmor = player.MaxArmor - if (originalSuit != nextSuit || originalSubtype != nextSubtype || originalArmor > toMaxArmor) { - player.History(HealFromExoSuitChange(PlayerSource(player), nextSuit)) - player.Armor = toMaxArmor - } - else { - player.Armor = originalArmor - } - //ensure arm is down, even if it needs to go back up - if (player.DrawnSlot != Player.HandsDownSlot) { - player.DrawnSlot = Player.HandsDownSlot - } - //a change due to exo-suit permissions mismatch will result in (more) items being re-arranged and/or dropped - //dropped items are not registered and can just be forgotten - val (afterHolsters, afterInventory) = if (nextSuit == exosuit) { - ( - //melee slot preservation for MAX - if (nextSuit == ExoSuitType.MAX) { - holsters.filter(_.start == 4) - } - else { - curatedHolsters.filterNot(dropPred) - }, - curatedInventory.filterNot(dropPred) - ) - } - else { - //our exo-suit type was hijacked by changing permissions; we shouldn't even be able to use that loadout(!) - //holsters - val leftoversForInventory = Players.fillEmptyHolsters( - player.Holsters().iterator, - (curatedHolsters ++ curatedInventory).filterNot(dropPred) - ) - val finalHolsters = player.Holsters() - .zipWithIndex - .collect { case (slot, index) if slot.Equipment.nonEmpty => InventoryItem(slot.Equipment.get, index) } - .toList - //inventory - val (finalInventory, _) = GridInventory.recoverInventory(leftoversForInventory, player.Inventory) - (finalHolsters, finalInventory) - } - (afterHolsters ++ afterInventory).foreach { entry => entry.obj.Faction = player.Faction } - toDeleteOrDrop.foreach { entry => entry.obj.Faction = PlanetSideEmpire.NEUTRAL } - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, - AvatarAction.ChangeLoadout(player.GUID, nextSuit, nextSubtype, player.LastDrawnSlot, exosuit == ExoSuitType.MAX, - oldHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterHolsters, - oldInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterInventory, toDeleteOrDrop) - ) - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true)) - - case _ => ; //terminal messages not handled here - } - - case Zone.Ground.ItemOnGround(item, _, _) => ; - val name = player.Name - val zone = player.Zone - val avatarEvents = zone.AvatarEvents - val localEvents = zone.LocalEvents - item match { - case trigger : BoomerTrigger => - //dropped the trigger, no longer own the boomer; make certain whole faction is aware of that - (zone.GUID(trigger.Companion), zone.Players.find { _.name == name}) match { - case (Some(boomer : BoomerDeployable), Some(avatar)) => - val guid = boomer.GUID - val factionChannel = boomer.Faction.toString - if(avatar.Deployables.Remove(boomer)) { - boomer.Faction = PlanetSideEmpire.NEUTRAL - boomer.AssignOwnership(None) - avatar.Deployables.UpdateUIElement(boomer.Definition.Item).foreach { case (currElem, curr, maxElem, max) => - avatarEvents ! AvatarServiceMessage(name, AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, maxElem, max)) - avatarEvents ! AvatarServiceMessage(name, AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, currElem, curr)) - } - localEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(boomer, zone)) - localEvents ! LocalServiceMessage(factionChannel, - LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, - DeployableInfo(guid, DeployableIcon.Boomer, boomer.Position, PlanetSideGUID(0)) - ) - ) - avatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, PlanetSideEmpire.NEUTRAL)) - } - case _ => ; //pointless trigger? or a trigger being deleted? - } - case _ => ; - } - - - case Zone.Ground.CanNotDropItem(_, item, reason) => - log.warn(s"${player.Name} tried to drop a ${item.Definition.Name} on the ground, but it $reason") - - case Zone.Ground.ItemInHand(_) => ; - - case Zone.Ground.CanNotPickupItem(_, item_guid, reason) => - log.warn(s"${player.Name} failed to pick up an item ($item_guid) from the ground because $reason") - case _ => ; } @@ -524,22 +254,27 @@ class PlayerControl(player : Player) extends Actor * @param target an object that can be affected by the jammered status * @param dur the duration of the timer, in milliseconds */ - override def StartJammeredStatus(target : Any, dur : Int) : Unit = { - //TODO these features - val zone = player.Zone + override def StartJammeredStatus(target : Any, dur : Int) : Unit = target match { + case obj : Player => + //TODO these features + val guid = obj.GUID + val zone = obj.Zone + val zoneId = zone.Id + val events = zone.AvatarEvents - player.Implants.indices.foreach { slot => // Deactivate & uninitialize all implants - zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect - self ! Player.ImplantActivation(slot, 0) - PlayerControl.UninitializeImplant(player, slot) - } + for(slot <- 0 to player.Implants.length - 1) { // Deactivate & uninitialize all implants + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect + self ! Player.ImplantActivation(slot, 0) + PlayerControl.UninitializeImplant(player, slot) + } - player.skipStaminaRegenForTurns = math.max(player.skipStaminaRegenForTurns, 10) - super.StartJammeredStatus(target, dur) + obj.skipStaminaRegenForTurns = math.max(obj.skipStaminaRegenForTurns, 10) + super.StartJammeredStatus(target, dur) + case _ => ; } override def CancelJammeredStatus(target: Any): Unit = { - player.Implants.indices.foreach { slot => // Start reinitializing all implants + for(slot <- 0 to player.Implants.length - 1) { // Start reinitializing all implants self ! Player.ImplantInitializationStart(slot) } @@ -564,101 +299,6 @@ class PlayerControl(player : Player) extends Actor else { item.FireMode.Modifiers.Damage3 } - - def MessageDeferredCallback(msg : Any) : Unit = { - msg match { - case Containable.MoveItem(_, item, _) => - //momentarily put item back where it was originally - val obj = ContainerObject - obj.Find(item) match { - case Some(slot) => - obj.Zone.AvatarEvents ! AvatarServiceMessage( - player.Name, - AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectAttachMessage(obj.GUID, item.GUID, slot)) - ) - case None => ; - } - case _ => ; - } - } - - def RemoveItemFromSlotCallback(item : Equipment, slot : Int) : Unit = { - val obj = ContainerObject - val zone = obj.Zone - val name = player.Name - val toChannel = if(obj.VisibleSlots.contains(slot) || obj.isBackpack) zone.Id else name - val events = zone.AvatarEvents - item.Faction = PlanetSideEmpire.NEUTRAL - if(slot == obj.DrawnSlot) { - obj.DrawnSlot = Player.HandsDownSlot - } - events ! AvatarServiceMessage(toChannel, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID)) - } - - def PutItemInSlotCallback(item : Equipment, slot : Int) : Unit = { - val obj = ContainerObject - val guid = obj.GUID - val zone = obj.Zone - val events = zone.AvatarEvents - val name = player.Name - val definition = item.Definition - val msg = AvatarAction.SendResponse( - Service.defaultPlayerGUID, - ObjectCreateDetailedMessage( - definition.ObjectId, - item.GUID, - ObjectCreateMessageParent(guid, slot), - definition.Packet.DetailedConstructorData(item).get - ) - ) - if(obj.isBackpack) { - item.Faction = PlanetSideEmpire.NEUTRAL - events ! AvatarServiceMessage(zone.Id, msg) - } - else { - val faction = obj.Faction - item.Faction = faction - events ! AvatarServiceMessage(name, msg) - if(obj.VisibleSlots.contains(slot)) { - events ! AvatarServiceMessage(zone.Id, AvatarAction.EquipmentInHand(guid, guid, slot, item)) - } - //handle specific types of items - item match { - case trigger : BoomerTrigger => - //pick up the trigger, own the boomer; make certain whole faction is aware of that - (zone.GUID(trigger.Companion), zone.Players.find { _.name == name }) match { - case (Some(boomer : BoomerDeployable), Some(avatar)) - if !boomer.OwnerName.contains(name) || boomer.Faction != faction => - val bguid = boomer.GUID - val faction = player.Faction - val factionChannel = faction.toString - if(avatar.Deployables.Add(boomer)) { - boomer.Faction = faction - boomer.AssignOwnership(player) - avatar.Deployables.UpdateUIElement(boomer.Definition.Item).foreach { case (currElem, curr, maxElem, max) => - events ! AvatarServiceMessage(name, AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, maxElem, max)) - events ! AvatarServiceMessage(name, AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, currElem, curr)) - } - zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(boomer), zone)) - events ! AvatarServiceMessage(factionChannel, AvatarAction.SetEmpire(Service.defaultPlayerGUID, bguid, faction)) - zone.LocalEvents ! LocalServiceMessage(factionChannel, - LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, - DeployableInfo(bguid, DeployableIcon.Boomer, boomer.Position, boomer.Owner.getOrElse(PlanetSideGUID(0))) - ) - ) - } - case _ => ; //pointless trigger? - } - case _ => ; - } - } - } - - def SwapItemCallback(item : Equipment) : Unit = { - val obj = ContainerObject - val zone = obj.Zone - zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectDetachMessage(obj.GUID, item.GUID, Vector3.Zero, 0f))) - } } object PlayerControl { diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala index ba24b1d8..38fe6984 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.definition.converter -import net.psforever.objects.LockerEquipment +import net.psforever.objects.LockerContainer import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.GridInventory import net.psforever.packet.game.objectcreate._ @@ -9,8 +9,8 @@ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} import scala.util.{Success, Try} -class LockerContainerConverter extends ObjectCreateConverter[LockerEquipment]() { - override def ConstructorData(obj : LockerEquipment) : Try[LockerContainerData] = { +class LockerContainerConverter extends ObjectCreateConverter[LockerContainer]() { + override def ConstructorData(obj : LockerContainer) : Try[LockerContainerData] = { MakeInventory(obj.Inventory) match { case Nil => Success(LockerContainerData(None)) @@ -19,7 +19,7 @@ class LockerContainerConverter extends ObjectCreateConverter[LockerEquipment]() } } - override def DetailedConstructorData(obj : LockerEquipment) : Try[DetailedLockerContainerData] = { + override def DetailedConstructorData(obj : LockerContainer) : Try[DetailedLockerContainerData] = { if(obj.Inventory.Size > 0) { Success(DetailedLockerContainerData( CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, None, None, PlanetSideGUID(0)), diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala index 9f9b5c2e..f62d5ec3 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -86,11 +86,12 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { } private def MakeMountings(obj : Vehicle) : List[InventoryItemData.InventoryItem] = { - obj.Weapons.collect { case (index, slot) if slot.Equipment.nonEmpty => - val equip : Equipment = slot.Equipment.get - val equipDef = equip.Definition - InventoryItemData(equipDef.ObjectId, equip.GUID, index, equipDef.Packet.ConstructorData(equip).get) - }.toList + obj.Weapons.map({ + case(index, slot) => + val equip : Equipment = slot.Equipment.get + val equipDef = equip.Definition + InventoryItemData(equipDef.ObjectId, equip.GUID, index, equipDef.Packet.ConstructorData(equip).get) + }).toList } protected def MakeUtilities(obj : Vehicle) : List[InventoryItemData.InventoryItem] = { diff --git a/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala b/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala index 9107d2a9..3443ad96 100644 --- a/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala +++ b/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala @@ -43,8 +43,6 @@ object GUIDTask { private val localObject = obj private val localAccessor = guid - override def Description : String = s"register $localObject" - override def isComplete : Task.Resolution.Value = if(localObject.HasGUID) { Task.Resolution.Success } @@ -92,9 +90,6 @@ object GUIDTask { def RegisterLocker(obj : LockerContainer)(implicit guid : ActorRef) : TaskResolver.GiveTask = { TaskResolver.GiveTask(RegisterObjectTask(obj).task, RegisterInventory(obj)) } - def RegisterLocker(obj : LockerEquipment)(implicit guid : ActorRef) : TaskResolver.GiveTask = { - TaskResolver.GiveTask(RegisterObjectTask(obj).task, RegisterInventory(obj)) - } /** * Construct tasking that registers the objects that are within the given container's inventory @@ -129,6 +124,8 @@ object GUIDTask { obj match { case tool : Tool => RegisterTool(tool) + case locker : LockerContainer => + RegisterLocker(locker) case _ => RegisterObjectTask(obj) } @@ -218,8 +215,6 @@ object GUIDTask { private val localObject = obj private val localAccessor = guid - override def Description : String = s"unregister $localObject" - override def isComplete : Task.Resolution.Value = if(!localObject.HasGUID) { Task.Resolution.Success } @@ -259,9 +254,6 @@ object GUIDTask { def UnregisterLocker(obj : LockerContainer)(implicit guid : ActorRef) : TaskResolver.GiveTask = { TaskResolver.GiveTask(UnregisterObjectTask(obj).task, UnregisterInventory(obj)) } - def UnregisterLocker(obj : LockerEquipment)(implicit guid : ActorRef) : TaskResolver.GiveTask = { - TaskResolver.GiveTask(RegisterObjectTask(obj).task, RegisterInventory(obj)) - } /** * Construct tasking that unregisters the objects that are within the given container's inventory @@ -290,6 +282,8 @@ object GUIDTask { obj match { case tool : Tool => UnregisterTool(tool) + case locker : LockerContainer => + UnregisterLocker(locker) case _ => UnregisterObjectTask(obj) } diff --git a/common/src/main/scala/net/psforever/objects/guid/Task.scala b/common/src/main/scala/net/psforever/objects/guid/Task.scala index 9b4e83e3..625adc77 100644 --- a/common/src/main/scala/net/psforever/objects/guid/Task.scala +++ b/common/src/main/scala/net/psforever/objects/guid/Task.scala @@ -4,7 +4,6 @@ package net.psforever.objects.guid import akka.actor.ActorRef trait Task { - def Description : String = "write_descriptive_task_message" def Execute(resolver : ActorRef) : Unit def isComplete : Task.Resolution.Value = Task.Resolution.Incomplete def Timeout : Long = 200L //milliseconds diff --git a/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala b/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala index 9b41834f..037aacf7 100644 --- a/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala +++ b/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala @@ -82,7 +82,7 @@ class TaskResolver() extends Actor { private def GiveTask(aTask : Task) : Unit = { val entry : TaskResolver.TaskEntry = TaskResolver.TaskEntry(aTask) tasks += entry - trace(s"enqueue and start task ${aTask.Description}") + trace(s"enqueue and start task $aTask") entry.Execute(self) StartTimeoutCheck() } @@ -112,13 +112,12 @@ class TaskResolver() extends Actor { private def QueueSubtasks(task : Task, subtasks : List[TaskResolver.GiveTask], resolver : ActorRef = Actor.noSender) : Unit = { val entry : TaskResolver.TaskEntry = TaskResolver.TaskEntry(task, subtasks.map(task => task.task), resolver) tasks += entry - trace(s"enqueue task ${task.Description}") + trace(s"enqueue task $task") if(subtasks.isEmpty) { //a leaf in terms of task dependency; so, not dependent on any other work - trace(s"start task ${task.Description}") + trace(s"start task $task") entry.Execute(self) } else { - trace(s"enqueuing ${subtasks.length} substask(s) belonging to ${task.Description}") subtasks.foreach({subtask => context.parent ! TaskResolver.GiveSubtask(subtask.task, subtask.subs, self) //route back to submit subtask to pool }) @@ -161,7 +160,7 @@ class TaskResolver() extends Actor { private def GeneralOnSuccess(index : Int) : Unit = { val entry = tasks(index) entry.task.onSuccess() - trace(s"success with task ${entry.task.Description}") + trace(s"success with this task ${entry.task}") if(entry.supertaskRef != ActorRef.noSender) { entry.supertaskRef ! TaskResolver.CompletedSubtask(entry.task) //alert our dependent task's resolver that we have completed } @@ -178,7 +177,7 @@ class TaskResolver() extends Actor { case Some(index) => val entry = tasks(index) if(TaskResolver.filterCompletionMatch(entry.subtasks.iterator, Task.Resolution.Success)) { - trace(s"start new task ${entry.task.Description}") + trace(s"start new task ${entry.task}") entry.Execute(self) StartTimeoutCheck() } @@ -225,7 +224,7 @@ class TaskResolver() extends Actor { private def GeneralOnFailure(index : Int, ex : Throwable) : Unit = { val entry = tasks(index) val task = entry.task - trace(s"failure with task ${task.Description}") + trace(s"failure with this task $task") task.onAbort(ex) task.onFailure(ex) if(entry.supertaskRef != ActorRef.noSender) { @@ -268,7 +267,7 @@ class TaskResolver() extends Actor { private def PropagateAbort(index : Int, ex : Throwable) : Unit = { tasks(index).subtasks.foreach({subtask => if(subtask.isComplete == Task.Resolution.Success) { - trace(s"aborting task ${subtask.Description}") + trace(s"aborting task $subtask") subtask.onAbort(ex) } context.parent ! Broadcast(TaskResolver.AbortTask(subtask, ex)) diff --git a/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala b/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala index 2038d5aa..bc08949a 100644 --- a/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala +++ b/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala @@ -110,7 +110,7 @@ class GridInventory extends Container { * Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.
*
* A "collision" is considered a situation where the stowed placards of two items would overlap in some way. - * The grid keeps track of the location of items by storing the primitive of their GUID in one or more cells. + * The gridkeeps track of the location of items by storing the primitive of their GUID in one or more cells. * Two primitives can not be stored in the same cell. * If placing two items into the same inventory leads to a situation where two primitive values might be in the same cell, * that is a collision. @@ -188,19 +188,17 @@ class GridInventory extends Container { } else { val collisions : mutable.Set[InventoryItem] = mutable.Set[InventoryItem]() - items - .map { case (_, item : InventoryItem) => item } - .foreach { item : InventoryItem => - val actualItemStart : Int = item.start - offset - val itemx : Int = actualItemStart % width - val itemy : Int = actualItemStart / width - val tile = item.obj.Tile - val clipsOnX : Boolean = if(itemx < startx) { itemx + tile.Width > startx } else { itemx <= startw } - val clipsOnY : Boolean = if(itemy < starty) { itemy + tile.Height > starty } else { itemy <= starth } - if(clipsOnX && clipsOnY) { - collisions += item - } + items.values.foreach({ item : InventoryItem => + val actualItemStart : Int = item.start - offset + val itemx : Int = actualItemStart % width + val itemy : Int = actualItemStart / width + val tile = item.obj.Tile + val clipsOnX : Boolean = if(itemx < startx) { itemx + tile.Width > startx } else { itemx <= startw } + val clipsOnY : Boolean = if(itemy < starty) { itemy + tile.Height > starty } else { itemy <= starth } + if(clipsOnX && clipsOnY) { + collisions += item } + }) Success(collisions.toList) } } @@ -580,7 +578,7 @@ class GridInventory extends Container { def Clear() : List[InventoryItem] = { val list = items.values.toList items.clear - entryIndex.set(0) + //entryIndex.set(0) grid = SetCellsOnlyNoOffset(0, width, height) list } @@ -781,17 +779,4 @@ object GridInventory { node.down.get(node.x, node.y + height, node.width, node.height - height) node.right.get(node.x + width, node.y, node.width - width, height) } - - def toPrintedList(inv : GridInventory) : String = { - val list = new StringBuilder - list.append("\n") - inv.Items.zipWithIndex.foreach { case (InventoryItem(obj, start), index) => - list.append(s"${index+1}: ${obj.Definition.Name}@${obj.GUID} -> $start\n") - } - list.toString - } - - def toPrintedGrid(inv : GridInventory) : String = { - new StringBuilder().append("\n").append(inv.grid.toSeq.grouped(inv.width).mkString("\n")).toString - } } diff --git a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala index 0f2ad13f..64668c4e 100644 --- a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala +++ b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala @@ -53,9 +53,7 @@ object Loadout { def Create(vehicle : Vehicle, label : String) : Loadout = { VehicleLoadout( label, - packageSimplifications(vehicle.Weapons.collect { case (index, slot) if slot.Equipment.nonEmpty => - InventoryItem(slot.Equipment.get, index) }.toList - ), + packageSimplifications(vehicle.Weapons.map({ case (index, weapon) => InventoryItem(weapon.Equipment.get, index) }).toList), packageSimplifications(vehicle.Trunk.Items), vehicle.Definition ) diff --git a/common/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala b/common/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala deleted file mode 100644 index 1d3bada4..00000000 --- a/common/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala +++ /dev/null @@ -1,631 +0,0 @@ -// Copyright (c) 2020 PSForever -package net.psforever.objects.serverobject.containable - -import akka.actor.{Actor, ActorRef} -import akka.pattern.{AskTimeoutException, ask} -import akka.util.Timeout -import net.psforever.objects.equipment.Equipment -import net.psforever.objects.inventory.{Container, InventoryItem} -import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.zones.Zone -import net.psforever.objects.{BoomerTrigger, GlobalDefinitions, Player} -import net.psforever.types.{PlanetSideEmpire, Vector3} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ -import scala.util.Success - -/** Parent of all standard (input) messages handled by a `ContainableBehavior` object for the purposes of item transfer */ -sealed trait ContainableMsg -/** `ContainableBehavior` messages that are allowed to be temporarily blocked in event of a complicated item transfer */ -sealed trait DeferrableMsg extends ContainableMsg - -/** - * A mixin for handling synchronized movement of `Equipment` items into or out from `Container` entities. - * The most important feature of this synchronization is the movmement of equipment - * out from one container into another container - * without causing representation overlap, overwriting, or unintended stacking of other equipment - * including equipment that has nort yet been inserted. - */ -trait ContainableBehavior { - _ : Actor => - def ContainerObject : PlanetSideServerObject with Container - - /** - * A flag for handling deferred messages during an attempt at complicated item movement (`MoveItem`) procedures. - * Complicated item movement procedures generally occur when the source and the destination are not the same container. - * The flag is set to `1` on the destination and is designed to block interference other normal insertion messages - * by taking those messages and pushing them back into the mailbox after a short delay. - * If two attempts on the same destination occur due to extremely coincidental item movement messages, - * the flag is set to `2` and most all messages involving item movement and item insertion are deferred. - * The destination is set back to normal - flag to `0` - when both of the attempts short-circuit due to timeout. - */ - private var waitOnMoveItemOps : Int = 0 - - final val containerBehavior : Receive = { - /* messages that modify delivery order */ - case ContainableBehavior.Wait() => Wait() - - case ContainableBehavior.Resume() => Resume() - - case repeatMsg @ ContainableBehavior.Defer(msg, sentBy) => - //received a previously blocked message; is it still blocked? - msg match { - case _ : ContainableMsg if waitOnMoveItemOps == 2 => RepeatMessageLater(repeatMsg) - case _ : DeferrableMsg if waitOnMoveItemOps == 1 => RepeatMessageLater(repeatMsg) - case _ => self.tell(msg, sentBy) - } - - case msg : ContainableMsg if waitOnMoveItemOps == 2 => - //all standard messages are blocked - RepeatMessageLater(ContainableBehavior.Defer(msg, sender)) - MessageDeferredCallback(msg) - - case msg : DeferrableMsg if waitOnMoveItemOps == 1 => - //insertion messages not related to an item move attempt are blocked - RepeatMessageLater(ContainableBehavior.Defer(msg, sender)) - MessageDeferredCallback(msg) - - /* normal messages */ - case Containable.RemoveItemFromSlot(None, Some(slot)) => - sender ! LocalRemoveItemFromSlot(slot) - - case Containable.RemoveItemFromSlot(Some(item), _) => - sender ! LocalRemoveItemFromSlot(item) - - case Containable.PutItemInSlot(item, dest) => /* can be deferred */ - sender ! LocalPutItemInSlot(item, dest) - - case Containable.PutItemInSlotOnly(item, dest) => /* can be deferred */ - sender ! LocalPutItemInSlotOnly(item, dest) - - case Containable.PutItemAway(item) => /* can be deferred */ - sender ! LocalPutItemAway(item) - - case Containable.PutItemInSlotOrAway(item, dest) => /* can be deferred */ - sender ! LocalPutItemInSlotOrAway(item, dest) - - case msg @ Containable.MoveItem(destination, equipment, destSlot) => /* can be deferred */ - if(ContainableBehavior.TestPutItemInSlot(destination, equipment, destSlot).nonEmpty) { //test early, before we try to move the item - val source = ContainerObject - val item = equipment - val dest = destSlot - LocalRemoveItemFromSlot(item) match { - case Containable.ItemFromSlot(_, Some(_), slot @ Some(originalSlot)) => - if(source eq destination) { - //when source and destination are the same, moving the item can be performed in one pass - LocalPutItemInSlot(item, dest) match { - case Containable.ItemPutInSlot(_, _, _, None) => ; //success - case Containable.ItemPutInSlot(_, _, _, Some(swapItem)) => //success, but with swap item - LocalPutItemInSlotOnlyOrAway(swapItem, slot) match { - case Containable.ItemPutInSlot(_, _, _, None) => ; - case _ => - source.Zone.Ground.tell(Zone.Ground.DropItem(swapItem, source.Position, Vector3.z(source.Orientation.z)), source.Actor) //drop it - } - case _ : Containable.CanNotPutItemInSlot => //failure case ; try restore original item placement - LocalPutItemInSlot(item, originalSlot) - } - } - else { - //destination sync - destination.Actor ! ContainableBehavior.Wait() - implicit val timeout = new Timeout(1000 milliseconds) - val moveItemOver = ask(destination.Actor, ContainableBehavior.MoveItemPutItemInSlot(item, dest)) - moveItemOver.onSuccess { - case Containable.ItemPutInSlot(_, _, _, None) => ; //successful - - case Containable.ItemPutInSlot(_, _, _, Some(swapItem)) => //successful, but with swap item - PutItBackOrDropIt(source, swapItem, slot, destination.Actor) - - case _ : Containable.CanNotPutItemInSlot => //failure case ; try restore original item placement - PutItBackOrDropIt(source, item, slot, source.Actor) - } - moveItemOver.onFailure { - case _ => //failure case ; try restore original item placement - PutItBackOrDropIt(source, item, slot, source.Actor) - } - //always do this - moveItemOver - .recover { case _ : AskTimeoutException => destination.Actor ! ContainableBehavior.Resume() } - .onComplete { _ => destination.Actor ! ContainableBehavior.Resume() } - } - case _ => ; - //we could not find the item to be moved in the source location; trying to act on old data? - } - } - else { - MessageDeferredCallback(msg) - } - - case ContainableBehavior.MoveItemPutItemInSlot(item, dest) => - sender ! LocalPutItemInSlot(item, dest) - - case ContainableBehavior.MoveItemPutItemInSlotOrAway(item, dest) => - sender ! LocalPutItemInSlotOrAway(item, dest) - } - - /* Functions (message control) */ - - /** - * Defer a message until later. - * @see `ContainableBehavior.Defer` - * @see `DeferrableMsg` - * @param msg the message to defer - */ - def RepeatMessageLater(msg : Any) : Unit = { - import scala.concurrent.ExecutionContext.Implicits.global - context.system.scheduler.scheduleOnce(100 milliseconds, self, msg) - } - - /** - * Increment the flag for blocking messages. - */ - def Wait() : Unit = { - waitOnMoveItemOps = math.min(waitOnMoveItemOps + 1, 2) - } - - /** - * Decrement the flag for blocking messages. - */ - def Resume() : Unit = { - waitOnMoveItemOps = math.max(0, waitOnMoveItemOps - 1) - } - - /** - * Stop blocking messages. - */ - def Reset() : Unit = { - waitOnMoveItemOps = 0 - } - - /* Functions (item transfer) */ - - private def LocalRemoveItemFromSlot(slot : Int) : Any = { - val source = ContainerObject - val (outSlot, item) = ContainableBehavior.TryRemoveItemFromSlot(source, slot) - item match { - case Some(thing) => RemoveItemFromSlotCallback(thing, outSlot.get) - case None => ; - } - Containable.ItemFromSlot(source, item, outSlot) - } - - private def LocalRemoveItemFromSlot(item : Equipment) : Any = { - val source = ContainerObject - val(slot, retItem) = ContainableBehavior.TryRemoveItemFromSlot(source, item) - retItem match { - case Some(thing) => RemoveItemFromSlotCallback(thing, slot.get) - case None => ; - } - Containable.ItemFromSlot(source, Some(item), slot) - } - - private def LocalPutItemInSlot(item : Equipment, dest : Int) : Any = { - val destination = ContainerObject - ContainableBehavior.TryPutItemInSlot(destination, item, dest) match { - case (true, swapItem) => - swapItem match { - case Some(thing) => SwapItemCallback(thing) - case None => ; - } - PutItemInSlotCallback(item, dest) - Containable.ItemPutInSlot(destination, item, dest, swapItem) - case (false, _) => - Containable.CanNotPutItemInSlot(destination, item, dest) - } - } - - private def LocalPutItemInSlotOnly(item : Equipment, dest : Int) : Any = { - val destination = ContainerObject - if(ContainableBehavior.TryPutItemInSlotOnly(destination, item, dest)) { - PutItemInSlotCallback(item, dest) - Containable.ItemPutInSlot(destination, item, dest, None) - } - else { - Containable.CanNotPutItemInSlot(destination, item, dest) - } - } - - private def LocalPutItemAway(item : Equipment) : Any = { - val destination = ContainerObject - ContainableBehavior.TryPutItemAway(destination, item) match { - case Some(dest) => - PutItemInSlotCallback(item, dest) - Containable.ItemPutInSlot(destination, item, dest, None) - case _ => - Containable.CanNotPutItemInSlot(destination, item, -1) - } - } - - private def LocalPutItemInSlotOrAway(item : Equipment, dest : Option[Int]) : Any = { - val destination = ContainerObject - ContainableBehavior.TryPutItemInSlotOrAway(destination, item, dest) match { - case (Some(slot), swapItem) => - swapItem match { - case Some(thing) => SwapItemCallback(thing) - case None => ; - } - PutItemInSlotCallback(item, slot) - Containable.ItemPutInSlot(destination, item, slot, swapItem) - case (None, _) => - Containable.CanNotPutItemInSlot(destination, item, dest.getOrElse(-1)) - } - } - - private def LocalPutItemInSlotOnlyOrAway(item : Equipment, dest : Option[Int]) : Any = { - val destination = ContainerObject - ContainableBehavior.TryPutItemInSlotOnlyOrAway(destination, item, dest) match { - case (Some(slot), None) => - PutItemInSlotCallback(item, slot) - Containable.ItemPutInSlot(destination, item, slot, None) - case _ => - Containable.CanNotPutItemInSlot(destination, item, dest.getOrElse(-1)) - } - } - - /** - * A controlled response where, in certain situations, - * it is appropriate to attempt to place an item into a specific container, - * first testing a specific slot, - * and attempting anywhere available in the container if not that slot, - * and, if nowhere is available, then it gets dropped on the ground. - * The inserted item is not permitted to swap places with another item in this case. - * @param container the container - * @param item the item to be inserted - * @param slot in which slot the insertion is prioritized (upper left corner of item) - * @param to a recipient to redirect the response message - * @param timeout how long the request has to complete before expiring - */ - private def PutItBackOrDropIt(container : PlanetSideServerObject with Container, item : Equipment, slot : Option[Int], to : ActorRef)(implicit timeout : Timeout) : Unit = { - val restore = ask(container.Actor, ContainableBehavior.MoveItemPutItemInSlotOrAway(item, slot)) - restore.onSuccess { - case _ : Containable.CanNotPutItemInSlot => - container.Zone.Ground.tell(Zone.Ground.DropItem(item, container.Position, Vector3.z(container.Orientation.z)), to) - case _ => - } - restore.onFailure { - case _ => - container.Zone.Ground.tell(Zone.Ground.DropItem(item, container.Position, Vector3.z(container.Orientation.z)), to) - } - } - - /** - * Reaction to the initial deferrence of a message that should handle the visual aspects of not immediately addressing the message. - * To be implemented. - * @param msg the deferred message - */ - def MessageDeferredCallback(msg : Any) : Unit - - /** - * Reaction to an item being removed a container. - * To be implemented. - * @param item the item that was removed - * @param slot the slot from which is was removed - */ - def RemoveItemFromSlotCallback(item : Equipment, slot : Int) : Unit - - /** - * Reaction to an item being placed into a container. - * To be implemented. - * @param item the item that was removed - * @param slot the slot from which is was removed - */ - def PutItemInSlotCallback(item : Equipment, slot : Int) : Unit - - /** - * Reaction to the existence of a swap item being produced from a container into the environment. - * To be implemented. - * @param item the item that was removed - */ - def SwapItemCallback(item : Equipment) : Unit -} - -object ContainableBehavior { - /** Control message for temporarily blocking some messages to maintain integrity of underlying `Container` object */ - private case class Wait() - /** Control message for unblocking all messages */ - private case class Resume() - /** Internal message for the purpose of refreshing a blocked message in the mailbox */ - private case class Defer(msg : Any, from : ActorRef) - - /* The same as `PutItemInSlot`, but is not a `DeferrableMsg` for the purposes of completing a `MoveItem` */ - private case class MoveItemPutItemInSlot(item : Equipment, slot : Int) extends ContainableMsg - /* The same as `PutItemInSlotOrAway`, but is not a `DeferrableMsg` for the purposes of completing a `MoveItem` */ - private case class MoveItemPutItemInSlotOrAway(item : Equipment, slot : Option[Int]) extends ContainableMsg - - /* Functions */ - - /** - * If the target item can be found in a container, remove the item from the container. - * This process can fail if the item can not be found or if it can not be removed for some reason. - * @see `Container.Find` - * @see `EquipmentSlot.Equipment` - * @param source the container in which the `item` is currently located - * @param item the item to be removed - * @return a `Tuple` of two optional values; - * the first is from what index in the container the `item` was removed, if it was removed; - * the second is the item again, if it has been removed; - * will use `(None, None)` to report failure - */ - def TryRemoveItemFromSlot(source : PlanetSideServerObject with Container, item : Equipment) : (Option[Int], Option[Equipment]) = { - source.Find(item) match { - case slot @ Some(index) => - source.Slot(index).Equipment = None - if(source.Slot(index).Equipment.isEmpty) { - (slot, Some(item)) - } - else { - (None, None) - } - case None => - (None, None) - } - } - - /** - * If the target slot of a container contains an item, remove that item from the container - * fromthe upper left corner position of the item as found in the container. - * This process can fail if no item can be found or if it can not be removed for some reason. - * @see `Container.Find` - * @see `EquipmentSlot.Equipment` - * @param source the container in which the `slot` is to be searched - * @param slot where the container will be searched - * @return a `Tuple` of two values; - * the first is from what `slot` in the container an `item` was removed, if any item removed; - * the second is the item, if it has been removed; - * will use `(None, None)` to report failure - */ - def TryRemoveItemFromSlot(source : PlanetSideServerObject with Container, slot : Int) : (Option[Int], Option[Equipment]) = { - val (item, outSlot) = source.Slot(slot).Equipment match { - case Some(thing) => (Some(thing), source.Find(thing)) - case None => (None, None) - } - source.Slot(slot).Equipment = None - item match { - case Some(_) if item.nonEmpty && source.Slot(slot).Equipment.isEmpty => - (outSlot, item) - case None => - (None, None) - } - } - - /** - * Are the conditions for an item insertion acceptable? - * If another item occupies the expected region of insertion (collision of bounding regions), - * the insertion can still be permitted with the assumption that - * the displaced item ("swap item") will have to be put somewhere else. - * @see `ContainableBehavior.PermitEquipmentStow` - * @see `Container.Collisions` - * @see `InventoryTile` - * @param destination the container - * @param item the item to be tested for insertion - * @param dest the upper left corner of the insertion position - * @return the results of the insertion test, if an insertion can be permitted; - * `None`, otherwise, and the insertion is not permitted - */ - def TestPutItemInSlot(destination : PlanetSideServerObject with Container, item : Equipment, dest : Int) : Option[List[InventoryItem]] = { - if(ContainableBehavior.PermitEquipmentStow(destination, item)) { - val tile = item.Definition.Tile - val destinationCollisionTest = destination.Collisions(dest, tile.Width, tile.Height) - destinationCollisionTest match { - case Success(Nil) => Some(Nil) //no item to swap - case Success(out @ List(_)) => Some(out) //one item to swap - case _ => None //abort when too many items at destination or other failure case - } - } - else { - None //blocked insertion (object type not permitted in container) - } - } - - /** - * Put an item in a container at the given position. - * The inserted item may swap places with another item. - * If the new item can not be inserted, the swap item is kept in its original position. - * @param destination the container - * @param item the item to be inserted - * @param dest in which slot the insertion is expected to occur (upper left corner of item) - * @return a `Tuple` of two values; - * the first is `true` if the insertion occurred; and, `false`, otherwise - * the second is an optional item that was removed from a coincidental position in the container ("swap item") - */ - def TryPutItemInSlot(destination : PlanetSideServerObject with Container, item : Equipment, dest : Int) : (Boolean, Option[Equipment]) = { - ContainableBehavior.TestPutItemInSlot(destination, item, dest) match { - case Some(results) => - //insert and swap, if applicable - val (swapItem, swapSlot) = results match { - case List(InventoryItem(obj, start)) => (Some(obj), start) - case _ => (None, dest) - } - destination.Slot(swapSlot).Equipment = None - if((destination.Slot(dest).Equipment = item).contains(item)) { - (true, swapItem) - } - else { - //put the swapItem back - destination.Slot(swapSlot).Equipment = swapItem - (false, None) - } - case None => - (false, None) - } - } - - /** - * Put an item in a container at the given position. - * The inserted item is not permitted to swap places with another item in this case. - * @param destination the container - * @param item the item to be inserted - * @param dest in which slot the insertion is expected to occur (upper left corner of item) - * @return `true` if the insertion occurred; - * `false`, otherwise - */ - def TryPutItemInSlotOnly(destination : PlanetSideServerObject with Container, item : Equipment, dest : Int) : Boolean = { - ContainableBehavior.TestPutItemInSlot(destination, item, dest).contains(Nil) && (destination.Slot(dest).Equipment = item).contains(item) - } - - /** - * Put an item in a container in the whatever position it cleanly fits. - * The inserted item will not swap places with another item in this case. - * @param destination the container - * @param item the item to be inserted - * @return the slot index of the insertion point; - * `None`, if a clean insertion is not possible - */ - def TryPutItemAway(destination : PlanetSideServerObject with Container, item : Equipment) : Option[Int] = { - destination.Fit(item) match { - case out @ Some(dest) - if ContainableBehavior.PermitEquipmentStow(destination, item) && (destination.Slot(dest).Equipment = item).contains(item) => - out - case _ => - None - } - } - - /** - * Attempt to put an item in a container at the given position. - * The inserted item may swap places with another item at this time. - * If the targeted insertion at this position fails, - * attempt to put the item in the container in the whatever position it cleanly fits. - * @param destination the container - * @param item the item to be inserted - * @param dest in which specific slot the insertion is first tested (upper left corner of item) - * @return na - */ - def TryPutItemInSlotOrAway(destination : PlanetSideServerObject with Container, item : Equipment, dest : Option[Int]) : (Option[Int], Option[Equipment]) = { - (dest match { - case Some(slot) => ContainableBehavior.TryPutItemInSlot(destination, item, slot) - case None => (false, None) - }) match { - case (true, swapItem) => - (dest, swapItem) - case _ => - ContainableBehavior.TryPutItemAway(destination, item) match { - case out @ Some(_) => (out, None) - case None => (None, None) - } - } - } - - /** - * Attempt to put an item in a container at the given position. - * The inserted item may not swap places with another item at this time. - * If the targeted insertion at this position fails, - * attempt to put the item in the container in the whatever position it cleanly fits. - * @param destination the container - * @param item the item to be inserted - * @param dest in which specific slot the insertion is first tested (upper left corner of item) - * @return na - */ - def TryPutItemInSlotOnlyOrAway(destination : PlanetSideServerObject with Container, item : Equipment, dest : Option[Int]) : (Option[Int], Option[Equipment]) = { - (dest match { - case Some(slot) if ContainableBehavior.TestPutItemInSlot(destination, item, slot).contains(Nil) => ContainableBehavior.TryPutItemInSlot(destination, item, slot) - case None => (false, None) - }) match { - case (true, swapItem) => - (dest, swapItem) - case _ => - ContainableBehavior.TryPutItemAway(destination, item) match { - case out @ Some(_) => (out, None) - case None => (None, None) - } - } - } - - /** - * Apply incontestable, arbitrary limitations - * whereby certain items are denied insertion into certain containers - * for vaguely documented but assuredly fantastic excuses on the part of the developer. - * @param destination the container - * @param equipment the item to be inserted - * @return `true`, if the object is allowed to contain the type of equipment object; - * `false`, otherwise - */ - def PermitEquipmentStow(destination : PlanetSideServerObject with Container, equipment : Equipment) : Boolean = { - import net.psforever.objects.{BoomerTrigger, Player} - equipment match { - case _ : BoomerTrigger => - //a BoomerTrigger can only be stowed in a player's holsters or inventory - //this is only a requirement until they, and their Boomer explosive complement, are cleaned-up properly - destination.isInstanceOf[Player] - case _ => - true - } - } - - /** - * A predicate used to determine if an `InventoryItem` object contains `Equipment` that should be dropped. - * Used to filter through lists of object data before it is placed into a player's inventory. - * Drop the item if:
- * - the item is cavern equipment
- * - the item is a `BoomerTrigger` type object
- * - the item is a `router_telepad` type object
- * - the item is another faction's exclusive equipment - * @param tplayer the player - * @return true if the item is to be dropped; false, otherwise - */ - def DropPredicate(tplayer : Player) : InventoryItem => Boolean = entry => { - val objDef = entry.obj.Definition - val faction = GlobalDefinitions.isFactionEquipment(objDef) - GlobalDefinitions.isCavernEquipment(objDef) || - objDef == GlobalDefinitions.router_telepad || - entry.obj.isInstanceOf[BoomerTrigger] || - (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL) - } -} - -object Containable { - final case class RemoveItemFromSlot(item : Option[Equipment], slot : Option[Int]) extends ContainableMsg - - object RemoveItemFromSlot { - def apply(slot : Int) : RemoveItemFromSlot = RemoveItemFromSlot(None, Some(slot)) - - def apply(item : Equipment) : RemoveItemFromSlot = RemoveItemFromSlot(Some(item), None) - } - - /** - * A response for the `RemoveItemFromSlot` message. - * It serves the dual purpose of reporting a missing item (by not reporting any slot information) - * and reporting no item ata given position (by not reporting any item information). - * @param obj the container - * @param item the equipment that was removed - * @param slot the index position from which any item was removed - */ - final case class ItemFromSlot(obj : PlanetSideServerObject with Container, item : Option[Equipment], slot : Option[Int]) - - final case class PutItemInSlot(item : Equipment, slot : Int) extends DeferrableMsg - - final case class PutItemInSlotOnly(item : Equipment, slot : Int) extends DeferrableMsg - - final case class PutItemAway(item : Equipment) extends DeferrableMsg - - final case class PutItemInSlotOrAway(item : Equipment, slot : Option[Int]) extends DeferrableMsg - - /** - * A "successful insertion" response for the variety message of messages that attempt to insert an item into a container. - * @param obj the container - * @param item the equipment that was inserted - * @param slot the slot position into which the item was inserted - * @param swapped_item any other item, previously in the container, that was displaced to make room for this insertion - */ - final case class ItemPutInSlot(obj : PlanetSideServerObject with Container, item : Equipment, slot : Int, swapped_item : Option[Equipment]) - - /** - * A "failed insertion" response for the variety message of messages that attempt to insert an item into a container. - * @param obj the container - * @param item the equipment that was not inserted - * @param slot the slot position into which the item should have been inserted; - * `-1` if no insertion slot was reported in the original message or discovered in the process of inserting - */ - final case class CanNotPutItemInSlot(obj : PlanetSideServerObject with Container, item : Equipment, slot : Int) - - /** - * The item should already be contained by us. - * The item is being removed from our containment and placed into a fixed slot position in another container. - * `MoveItem` is a process that may be complicated and is one reason why `DeferrableMsg`s are employed. - * @param destination the container into which the item is being placed - * @param item the item - * @param destination_slot where in the destination container the item is being placed - */ - final case class MoveItem(destination : PlanetSideServerObject with Container, item : Equipment, destination_slot : Int) extends DeferrableMsg -} 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 aca3ffe6..5ab86e44 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 @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals -import akka.actor.{ActorContext, ActorRef} +import akka.actor.ActorContext import net.psforever.objects.definition.ImplantDefinition import net.psforever.objects.{Player, Vehicle} import net.psforever.objects.equipment.Equipment @@ -92,14 +92,6 @@ class OrderTerminalDefinition(objId : Int) extends TerminalDefinition(objId) { } } } - - override def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { - tabs.get(msg.msg.item_page) match { - case Some(page) => - page.Dispatch(sender, terminal, msg) - case _ => ; - } - } } object OrderTerminalDefinition { @@ -108,9 +100,8 @@ object OrderTerminalDefinition { * @see `ItemTransactionMessage` */ sealed trait Tab { - def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange + def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal() def Sell(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal() - def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit } /** @@ -128,10 +119,6 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } - - def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { - msg.player.Actor ! msg - } } /** @@ -157,13 +144,6 @@ object OrderTerminalDefinition { } } } - - def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { - msg.response match { - case _ : Terminal.BuyExosuit => msg.player.Actor ! msg - case _ => sender ! msg - } - } } /** @@ -191,10 +171,6 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } - - def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { - sender ! msg - } } /** @@ -211,10 +187,6 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } - - def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { - sender ! msg - } } /** @@ -243,10 +215,6 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } - - def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { - sender ! msg - } } /** @@ -304,10 +272,6 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } - - def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { - msg.player.Actor ! msg - } } /** @@ -336,14 +300,6 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } - - def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { - val player = msg.player - player.Zone.GUID(player.VehicleOwned) match { - case Some(vehicle : Vehicle) => vehicle.Actor ! msg - case _ => sender ! Terminal.TerminalMessage(player, msg.msg, Terminal.NoDeal()) - } - } } /** @@ -375,10 +331,6 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } - - def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { - sender ! msg - } } /** 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 525ed0ae..b493477a 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 @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals -import akka.actor.{Actor, ActorRef} +import akka.actor.Actor import net.psforever.objects.{GlobalDefinitions, SimpleItem} import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior @@ -30,10 +30,7 @@ class TerminalControl(term : Terminal) extends Actor .orElse(canBeRepairedByNanoDispenser) .orElse { case Terminal.Request(player, msg) => - TerminalControl.Dispatch( - sender, - term, - Terminal.TerminalMessage(player, msg, term.Request(player, msg))) + sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg)) case CommonMessages.Use(player, Some(item : SimpleItem)) if item.Definition == GlobalDefinitions.remote_electronics_kit => //TODO setup certifications check @@ -49,16 +46,5 @@ class TerminalControl(term : Terminal) extends Actor case _ => ; } - override def toString : String = term.Definition.Name } - -object TerminalControl { - def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { - msg.response match { - case Terminal.NoDeal() => sender ! msg - case _ => - terminal.Definition.Dispatch(sender, terminal, msg) - } - } -} 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 cb2a2885..10d01760 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 @@ -1,7 +1,6 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals -import akka.actor.ActorRef import net.psforever.objects.Player import net.psforever.objects.definition.converter.TerminalConverter import net.psforever.objects.serverobject.structures.AmenityDefinition @@ -23,6 +22,4 @@ abstract class TerminalDefinition(objectId : Int) extends AmenityDefinition(obje * @return a message that resolves the transaction */ def Request(player : Player, msg : Any) : Terminal.Exchange - - def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { } } diff --git a/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala index ef6de382..6a62b822 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -1,28 +1,21 @@ -// Copyright (c) 2017-2020 PSForever +// Copyright (c) 2017 PSForever package net.psforever.objects.vehicles import akka.actor.{Actor, ActorRef, Cancellable} import net.psforever.objects._ import net.psforever.objects.ballistics.{ResolvedProjectile, VehicleSource} -import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons} -import net.psforever.objects.inventory.{GridInventory, InventoryItem} +import net.psforever.objects.equipment.JammableMountedWeapons import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} -import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior} import net.psforever.objects.serverobject.damage.DamageableVehicle import net.psforever.objects.serverobject.deploy.DeploymentBehavior import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.repair.RepairableVehicle -import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.vital.VehicleShieldCharge import net.psforever.objects.zones.Zone -import net.psforever.types._ -import services.RemoverActor -import net.psforever.packet.game._ -import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent -import services.Service -import services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.types.{DriveState, ExoSuitType, PlanetSideGUID, Vector3} +import services.{RemoverActor, Service} import services.vehicle.{VehicleAction, VehicleServiceMessage} import scala.concurrent.ExecutionContext.Implicits.global @@ -43,8 +36,7 @@ class VehicleControl(vehicle : Vehicle) extends Actor with CargoBehavior with DamageableVehicle with RepairableVehicle - with JammableMountedWeapons - with ContainableBehavior { + with JammableMountedWeapons { //make control actors belonging to utilities when making control actor belonging to vehicle vehicle.Utilities.foreach({case (_, util) => util.Setup }) @@ -56,7 +48,6 @@ class VehicleControl(vehicle : Vehicle) extends Actor def DeploymentObject = vehicle def DamageableObject = vehicle def RepairableObject = vehicle - def ContainerObject = vehicle /** cheap flag for whether the vehicle is decaying */ var decaying : Boolean = false @@ -81,7 +72,6 @@ class VehicleControl(vehicle : Vehicle) extends Actor .orElse(jammableBehavior) .orElse(takesDamage) .orElse(canBeRepairedByNanoDispenser) - .orElse(containerBehavior) .orElse { case Vehicle.Ownership(None) => LoseOwnership() @@ -153,52 +143,6 @@ class VehicleControl(vehicle : Vehicle) extends Actor ) } - case Terminal.TerminalMessage(player, msg, reply) => - reply match { - case Terminal.VehicleLoadout(definition, weapons, inventory) => - org.log4s.getLogger(vehicle.Definition.Name).info(s"changing vehicle equipment loadout to ${player.Name}'s option #${msg.unk1 + 1}") - //remove old inventory - val oldInventory = vehicle.Inventory.Clear().map { case InventoryItem(obj, _) => (obj, obj.GUID) } - //"dropped" items are lost; if it doesn't go in the trunk, it vanishes into the nanite cloud - val (_, afterInventory) = inventory.partition(ContainableBehavior.DropPredicate(player)) - val (oldWeapons, newWeapons, finalInventory) = if(vehicle.Definition == definition) { - //vehicles are the same type - //TODO want to completely swap weapons, but holster icon vanishes temporarily after swap - //TODO BFR arms must be swapped properly -// //remove old weapons -// val oldWeapons = vehicle.Weapons.values.collect { case slot if slot.Equipment.nonEmpty => -// val obj = slot.Equipment.get -// slot.Equipment = None -// (obj, obj.GUID) -// }.toList -// (oldWeapons, weapons, afterInventory) - //TODO for now, just refill ammo; assume weapons stay the same - vehicle.Weapons - .collect { case (_, slot) if slot.Equipment.nonEmpty => slot.Equipment.get } - .collect { case weapon : Tool => - weapon.AmmoSlots.foreach { ammo => ammo.Box.Capacity = ammo.Box.Definition.Capacity } - } - (Nil, Nil, afterInventory) - } - else { - //vehicle loadout is not for this vehicle - //do not transfer over weapon ammo - if(vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset) { - (Nil, Nil, afterInventory) //trunk is the same dimensions, however - } - else { - //accommodate as much of inventory as possible - val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) - (Nil, Nil, stow) - } - } - finalInventory.foreach { _.obj.Faction = vehicle.Faction } - player.Zone.VehicleEvents ! VehicleServiceMessage(player.Zone.Id, VehicleAction.ChangeLoadout(vehicle.GUID, oldWeapons, newWeapons, oldInventory, finalInventory)) - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true)) - - case _ => ; - } - case Vehicle.Deconstruct(time) => time match { case Some(delay) => @@ -300,73 +244,6 @@ class VehicleControl(vehicle : Vehicle) extends Actor } } - def MessageDeferredCallback(msg : Any) : Unit = { - msg match { - case Containable.MoveItem(_, item, _) => - //momentarily put item back where it was originally - val obj = ContainerObject - obj.Find(item) match { - case Some(slot) => - obj.Zone.AvatarEvents ! AvatarServiceMessage( - self.toString, - AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectAttachMessage(obj.GUID, item.GUID, slot)) - ) - case None => ; - } - case _ => ; - } - } - - def RemoveItemFromSlotCallback(item : Equipment, slot : Int) : Unit = { - val zone = ContainerObject.Zone - zone.VehicleEvents ! VehicleServiceMessage(self.toString, VehicleAction.UnstowEquipment(Service.defaultPlayerGUID, item.GUID)) - } - - def PutItemInSlotCallback(item : Equipment, slot : Int) : Unit = { - val obj = ContainerObject - val oguid = obj.GUID - val zone = obj.Zone - val channel = self.toString - val events = zone.VehicleEvents - val iguid = item.GUID - val definition = item.Definition - item.Faction = obj.Faction - events ! VehicleServiceMessage( - //TODO when a new weapon, the equipment slot ui goes blank, but the weapon functions; remount vehicle to correct it - if(obj.VisibleSlots.contains(slot)) zone.Id else channel, - VehicleAction.SendResponse( - Service.defaultPlayerGUID, - ObjectCreateMessage( - definition.ObjectId, - iguid, - ObjectCreateMessageParent(oguid, slot), - definition.Packet.ConstructorData(item).get - ) - ) - ) - item match { - case box : AmmoBox => - events ! VehicleServiceMessage( - channel, - VehicleAction.InventoryState2(Service.defaultPlayerGUID, iguid, oguid, box.Capacity) - ) - case weapon : Tool => - weapon.AmmoSlots.map { slot => slot.Box }.foreach { box => - events ! VehicleServiceMessage( - channel, - VehicleAction.InventoryState2(Service.defaultPlayerGUID, iguid, weapon.GUID, box.Capacity) - ) - } - case _ => ; - } - } - - def SwapItemCallback(item : Equipment) : Unit = { - val obj = ContainerObject - val zone = obj.Zone - zone.VehicleEvents ! VehicleServiceMessage(self.toString, VehicleAction.SendResponse(Service.defaultPlayerGUID, ObjectDetachMessage(obj.GUID, item.GUID, Vector3.Zero, 0f))) - } - def LoseOwnership() : Unit = { val obj = MountableObject Vehicles.Disown(obj.GUID, obj) diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala index 41d375c9..7d41516d 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala @@ -4,8 +4,6 @@ package net.psforever.objects.zones import akka.actor.Actor import net.psforever.objects.equipment.Equipment import net.psforever.types.PlanetSideGUID -import services.Service -import services.avatar.{AvatarAction, AvatarServiceMessage} import scala.annotation.tailrec import scala.collection.mutable.ListBuffer @@ -30,28 +28,19 @@ class ZoneGroundActor(zone : Zone, equipmentOnGround : ListBuffer[Equipment]) ex } else { equipmentOnGround += item - item.Position = pos - item.Orientation = orient - zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.DropItem(Service.defaultPlayerGUID, item)) Zone.Ground.ItemOnGround(item, pos, orient) }) case Zone.Ground.PickupItem(item_guid) => sender ! (FindItemOnGround(item_guid) match { case Some(item) => - zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PickupItem(Service.defaultPlayerGUID, item, 0)) Zone.Ground.ItemInHand(item) case None => Zone.Ground.CanNotPickupItem(zone, item_guid, "can not find") }) case Zone.Ground.RemoveItem(item_guid) => - //intentionally no callback - FindItemOnGround(item_guid) match { - case Some(item) => - zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PickupItem(Service.defaultPlayerGUID, item, 0)) - case None => ; - } + FindItemOnGround(item_guid) //intentionally no callback case _ => ; } diff --git a/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala index 889d9a21..1f7bdf0a 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala @@ -63,26 +63,13 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player } case Zone.Corpse.Add(player) => - val (playerIsCorpse, playerInZone) = CorpseAdd(player, playerMap, corpseList) - if(playerIsCorpse) { - if(!playerInZone && player.Actor == ActorRef.noSender) { - player.Zone = zone - player.Actor = context.actorOf(Props(classOf[PlayerControl], player), s"corpse_of_${player.CharId}_${player.GUID.guid}_${System.currentTimeMillis}") - } - } + CorpseAdd(player, corpseList) case Zone.Corpse.Remove(player) => - if(CorpseRemove(player, corpseList)) { - PlayerLeave(player) - } + CorpseRemove(player, corpseList) case _ => ; } - - def PlayerLeave(player : Player) : Unit = { - context.stop(player.Actor) - player.Actor = ActorRef.noSender - } } object ZonePopulationActor { @@ -161,33 +148,18 @@ object ZonePopulationActor { /** * If the given `player` passes a condition check, add it to the list. - * Also, ensure that "this player" is not currently counted among the living. * @param player a `Player` object - * @param playerMap the mapping of `Avatar` objects to `Player` objects * @param corpseList a list of `Player` objects - * @return a `Tuple` of two flags; - * the first is whether the player was turned into a corpse or not; - * the second is whether the player was found in the zone before being turned into a corpse + * @return true, if the `player` was added to the list; + * false, otherwise */ - def CorpseAdd(player : Player, playerMap : TrieMap[Avatar, Option[Player]], corpseList : ListBuffer[Player]) : (Boolean, Boolean) = { + def CorpseAdd(player : Player, corpseList : ListBuffer[Player]) : Boolean = { if(player.isBackpack) { - val playerFoundInZone = playerMap.find { - case (_, Some(p)) => p.CharId == player.CharId - case (_, None) => false - } match { - case Some((a, _)) => PopulationRelease(a, playerMap).nonEmpty - case _ => false - } - corpseList.find { _ eq player } match { - case None => ; - corpseList += player - (true, playerFoundInZone) - case _ => - (false, false) - } + corpseList += player + true } else { - (false, false) + false } } @@ -195,19 +167,20 @@ object ZonePopulationActor { * Remove the given `player` from the list. * @param player a `Player` object * @param corpseList a list of `Player` objects - * @return `true`, if the corpse was found and removed; - * `false`, otherwise */ - def CorpseRemove(player : Player, corpseList : ListBuffer[Player]) : Boolean = { + def CorpseRemove(player : Player, corpseList : ListBuffer[Player]) : Unit = { recursiveFindCorpse(corpseList.iterator, player) match { - case None => - false + case None => ; case Some(index) => corpseList.remove(index) - true } } + def PlayerLeave(player : Player) : Unit = { + player.Actor ! akka.actor.PoisonPill + player.Actor = ActorRef.noSender + } + /** * A recursive function that finds and removes a specific player from a list of players. * @param iter an `Iterator` of `Player` objects diff --git a/common/src/main/scala/services/avatar/AvatarService.scala b/common/src/main/scala/services/avatar/AvatarService.scala index eae49fb7..93faada0 100644 --- a/common/src/main/scala/services/avatar/AvatarService.scala +++ b/common/src/main/scala/services/avatar/AvatarService.scala @@ -94,7 +94,7 @@ class AvatarService(zone : Zone) extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.DestroyDisplay(killer, victim, method, unk)) ) - case AvatarAction.DropItem(player_guid, item) => + case AvatarAction.DropItem(player_guid, item, _) => val definition = item.Definition val objectData = DroppedItemData( PlacementData(item.Position, item.Orientation), @@ -179,10 +179,21 @@ class AvatarService(zone : Zone) extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ProjectileState(projectile_guid, shot_pos, shot_vel, shot_orient, sequence, end, target)) ) - case AvatarAction.PickupItem(player_guid, item, unk) => + case AvatarAction.PickupItem(player_guid, _, target, slot, item, unk) => janitor forward RemoverActor.ClearSpecific(List(item), zone) AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ObjectDelete(item.GUID, unk)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, { + val itemGUID = item.GUID + if(target.VisibleSlots.contains(slot)) { + val definition = item.Definition + val containerData = ObjectCreateMessageParent(target.GUID, slot) + val objectData = definition.Packet.ConstructorData(item).get + AvatarResponse.EquipmentInHand(ObjectCreateMessage(definition.ObjectId, itemGUID, containerData, objectData)) + } + else { + AvatarResponse.ObjectDelete(itemGUID, unk) + } + }) ) case AvatarAction.PutDownFDU(player_guid) => AvatarEvents.publish( @@ -227,19 +238,6 @@ class AvatarService(zone : Zone) extends Actor { AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.TeardownConnection()) ) - case AvatarAction.TerminalOrderResult(terminal, term_action, result) => - AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.TerminalOrderResult(terminal, term_action, result)) - ) - case AvatarAction.ChangeExosuit(target, exosuit, subtype, slot, maxhand, old_holsters, holsters, old_inventory, inventory, drop, delete) => - AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.ChangeExosuit(target, exosuit, subtype, slot, maxhand, old_holsters, holsters, old_inventory, inventory, drop, delete)) - ) - case AvatarAction.ChangeLoadout(target, exosuit, subtype, slot, maxhand, old_holsters, holsters, old_inventory, inventory, drop) => - AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.ChangeLoadout(target, exosuit, subtype, slot, maxhand, old_holsters, holsters, old_inventory, inventory, drop)) - ) - case _ => ; } @@ -261,7 +259,6 @@ class AvatarService(zone : Zone) extends Actor { )) } */ - case msg => log.warn(s"Unhandled message $msg from $sender") } diff --git a/common/src/main/scala/services/avatar/AvatarServiceMessage.scala b/common/src/main/scala/services/avatar/AvatarServiceMessage.scala index 234dffef..e7fc9dab 100644 --- a/common/src/main/scala/services/avatar/AvatarServiceMessage.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceMessage.scala @@ -5,11 +5,12 @@ import net.psforever.objects.{PlanetSideGameObject, Player} import net.psforever.objects.ballistics.{Projectile, SourceEntry} import net.psforever.objects.ce.Deployable import net.psforever.objects.equipment.Equipment -import net.psforever.objects.inventory.{Container, InventoryItem} +import net.psforever.objects.inventory.Container import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket +import net.psforever.packet.game.ImplantAction import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent} -import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3} +import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} import scala.concurrent.duration.FiniteDuration @@ -35,7 +36,7 @@ object AvatarAction { final case class ActivateImplantSlot(player_guid : PlanetSideGUID, slot : Int) extends Action final case class Destroy(victim : PlanetSideGUID, killer : PlanetSideGUID, weapon : PlanetSideGUID, pos : Vector3) extends Action final case class DestroyDisplay(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int = 121) extends Action - final case class DropItem(player_guid : PlanetSideGUID, item : Equipment) extends Action + final case class DropItem(player_guid : PlanetSideGUID, item : Equipment, zone : Zone) extends Action final case class EquipmentInHand(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action final case class GenericObjectAction(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, action_code : Int) extends Action final case class HitHint(source_guid : PlanetSideGUID, player_guid : PlanetSideGUID) extends Action @@ -48,7 +49,7 @@ object AvatarAction { final case class PlanetsideAttributeToAll(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action final case class PlanetsideAttributeSelf(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action final case class PlayerState(player_guid : PlanetSideGUID, pos : Vector3, vel : Option[Vector3], facingYaw : Float, facingPitch : Float, facingYawUpper : Float, timestamp : Int, is_crouching : Boolean, is_jumping : Boolean, jump_thrust : Boolean, is_cloaked : Boolean, spectator : Boolean, weaponInHand : Boolean) extends Action - final case class PickupItem(player_guid : PlanetSideGUID, item : Equipment, unk : Int = 0) extends Action + final case class PickupItem(player_guid : PlanetSideGUID, zone : Zone, target : PlanetSideGameObject with Container, slot : Int, item : Equipment, unk : Int = 0) extends Action final case class ProjectileAutoLockAwareness(mode : Int) extends Action final case class ProjectileExplodes(player_guid : PlanetSideGUID, projectile_guid : PlanetSideGUID, projectile : Projectile) extends Action final case class ProjectileState(player_guid : PlanetSideGUID, projectile_guid : PlanetSideGUID, shot_pos : Vector3, shot_vel : Vector3, shot_orient : Vector3, sequence : Int, end : Boolean, hit_target : PlanetSideGUID) extends Action @@ -63,10 +64,6 @@ object AvatarAction { final case class SendResponse(player_guid: PlanetSideGUID, msg: PlanetSideGamePacket) extends Action final case class SendResponseTargeted(target_guid: PlanetSideGUID, msg: PlanetSideGamePacket) extends Action - final case class TerminalOrderResult(terminal_guid : PlanetSideGUID, action : TransactionType.Value, result : Boolean) extends Action - final case class ChangeExosuit(target_guid : PlanetSideGUID, exosuit : ExoSuitType.Value, subtype : Int, last_drawn_slot : Int, new_max_hand : Boolean, old_holsters : List[(Equipment, PlanetSideGUID)], holsters : List[InventoryItem], old_inventory : List[(Equipment, PlanetSideGUID)], inventory : List[InventoryItem], drop : List[InventoryItem], delete : List[(Equipment, PlanetSideGUID)]) extends Action - final case class ChangeLoadout(target_guid : PlanetSideGUID, exosuit : ExoSuitType.Value, subtype : Int, last_drawn_slot : Int, new_max_hand : Boolean, old_holsters : List[(Equipment, PlanetSideGUID)], holsters : List[InventoryItem], old_inventory : List[(Equipment, PlanetSideGUID)], inventory : List[InventoryItem], drop : List[InventoryItem]) extends Action - final case class TeardownConnection() extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action diff --git a/common/src/main/scala/services/avatar/AvatarServiceResponse.scala b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala index da811964..7920703f 100644 --- a/common/src/main/scala/services/avatar/AvatarServiceResponse.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala @@ -4,11 +4,10 @@ package services.avatar import net.psforever.objects.Player import net.psforever.objects.ballistics.{Projectile, SourceEntry} import net.psforever.objects.equipment.Equipment -import net.psforever.objects.inventory.InventoryItem import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.objectcreate.ConstructorData -import net.psforever.packet.game.ObjectCreateMessage -import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3} +import net.psforever.packet.game.{ImplantAction, ObjectCreateMessage} +import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} import services.GenericEventBusMsg final case class AvatarServiceResponse(toChannel : String, @@ -57,10 +56,6 @@ object AvatarResponse { final case class SendResponse(msg: PlanetSideGamePacket) extends Response final case class SendResponseTargeted(target_guid : PlanetSideGUID, msg: PlanetSideGamePacket) extends Response - final case class TerminalOrderResult(terminal_guid : PlanetSideGUID, action : TransactionType.Value, result : Boolean) extends Response - final case class ChangeExosuit(target_guid : PlanetSideGUID, exosuit : ExoSuitType.Value, subtype : Int, last_drawn_slot : Int, new_max_hand : Boolean, old_holsters : List[(Equipment, PlanetSideGUID)], holsters : List[InventoryItem], old_inventory : List[(Equipment, PlanetSideGUID)], inventory : List[InventoryItem], drop : List[InventoryItem], delete : List[(Equipment, PlanetSideGUID)]) extends Response - final case class ChangeLoadout(target_guid : PlanetSideGUID, exosuit : ExoSuitType.Value, subtype : Int, last_drawn_slot : Int, new_max_hand : Boolean, old_holsters : List[(Equipment, PlanetSideGUID)], holsters : List[InventoryItem], old_inventory : List[(Equipment, PlanetSideGUID)], inventory : List[InventoryItem], drop : List[InventoryItem]) extends Response - final case class TeardownConnection() extends Response // final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response } diff --git a/common/src/main/scala/services/vehicle/VehicleService.scala b/common/src/main/scala/services/vehicle/VehicleService.scala index ff664e18..d535056e 100644 --- a/common/src/main/scala/services/vehicle/VehicleService.scala +++ b/common/src/main/scala/services/vehicle/VehicleService.scala @@ -141,11 +141,6 @@ class VehicleService(zone : Zone) extends Actor { VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.KickCargo(cargo, speed, delay)) ) - - case VehicleAction.ChangeLoadout(target_guid, removed_weapons, new_weapons, old_inventory, new_inventory) => - VehicleEvents.publish( - VehicleServiceResponse(s"/$forChannel/Vehicle", Service.defaultPlayerGUID, VehicleResponse.ChangeLoadout(target_guid, removed_weapons, new_weapons, old_inventory, new_inventory)) - ) case _ => ; } diff --git a/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala b/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala index 5fa5d672..8d101911 100644 --- a/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala +++ b/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala @@ -3,7 +3,6 @@ package services.vehicle import net.psforever.objects.{PlanetSideGameObject, Vehicle} import net.psforever.objects.equipment.Equipment -import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.objectcreate.ConstructorData @@ -48,6 +47,4 @@ object VehicleAction { final case class TransferPassengerChannel(player_guid : PlanetSideGUID, temp_channel : String, new_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Action final case class KickCargo(player_guid : PlanetSideGUID, cargo : Vehicle, speed : Int, delay : Long) extends Action - - final case class ChangeLoadout(target_guid : PlanetSideGUID, removed_weapons : List[(Equipment, PlanetSideGUID)], new_weapons : List[InventoryItem], old_inventory : List[(Equipment, PlanetSideGUID)], new_inventory : List[InventoryItem]) extends Action } diff --git a/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala b/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala index 93c16f5a..68834cc4 100644 --- a/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala +++ b/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala @@ -1,8 +1,6 @@ // Copyright (c) 2017 PSForever package services.vehicle -import net.psforever.objects.equipment.Equipment -import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.pad.VehicleSpawnPad.Reminders import net.psforever.objects.{PlanetSideGameObject, Vehicle} @@ -55,6 +53,4 @@ object VehicleResponse { final case class TransferPassengerChannel(old_channel : String, temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Response final case class KickCargo(cargo : Vehicle, speed : Int, delay : Long) extends Response - - final case class ChangeLoadout(target_guid : PlanetSideGUID, removed_weapons : List[(Equipment, PlanetSideGUID)], new_weapons : List[InventoryItem], old_inventory : List[(Equipment, PlanetSideGUID)], new_inventory : List[InventoryItem]) extends Response } diff --git a/common/src/test/scala/objects/AvatarTest.scala b/common/src/test/scala/objects/AvatarTest.scala index 6c0ff545..6e1502b1 100644 --- a/common/src/test/scala/objects/AvatarTest.scala +++ b/common/src/test/scala/objects/AvatarTest.scala @@ -389,10 +389,7 @@ class AvatarTest extends Specification { "the fifth slot is the locker wrapped in an EquipmentSlot" in { val (_, avatar) = CreatePlayer() - avatar.FifthSlot.Equipment match { - case Some(slot : LockerEquipment) => slot.Inventory mustEqual avatar.Locker.Inventory - case _ => ko - } + avatar.FifthSlot.Equipment.contains(avatar.Locker) } "toString" in { diff --git a/common/src/test/scala/objects/ConverterTest.scala b/common/src/test/scala/objects/ConverterTest.scala index bd1eaaa5..04a58b5f 100644 --- a/common/src/test/scala/objects/ConverterTest.scala +++ b/common/src/test/scala/objects/ConverterTest.scala @@ -631,7 +631,7 @@ class ConverterTest extends Specification { "LockerContainer" should { "convert to packet (empty)" in { - val obj = new LockerEquipment(LockerContainer()) + val obj = LockerContainer() obj.Definition.Packet.DetailedConstructorData(obj) match { case Success(pkt) => pkt mustEqual DetailedLockerContainerData(CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, None, None, PlanetSideGUID(0)), None) @@ -648,7 +648,7 @@ class ConverterTest extends Specification { "convert to packet (occupied)" in { import GlobalDefinitions._ - val obj = new LockerEquipment(LockerContainer()) + val obj = LockerContainer() val rek = SimpleItem(remote_electronics_kit) rek.GUID = PlanetSideGUID(1) obj.Inventory += 0 -> rek diff --git a/common/src/test/scala/objects/InventoryTest.scala b/common/src/test/scala/objects/InventoryTest.scala index b5b1b4d6..828f5a78 100644 --- a/common/src/test/scala/objects/InventoryTest.scala +++ b/common/src/test/scala/objects/InventoryTest.scala @@ -525,136 +525,4 @@ class InventoryTest extends Specification { ok } } - - "InventoryEquiupmentSlot" should { - "insert, collide, insert" in { - val obj : GridInventory = GridInventory(7, 7) - obj.Slot(16).Equipment = bullet9mmBox1 - //confirm all squares - obj.Slot( 8).Equipment.nonEmpty mustEqual false - obj.Slot( 9).Equipment.nonEmpty mustEqual false - obj.Slot( 10).Equipment.nonEmpty mustEqual false - obj.Slot( 11).Equipment.nonEmpty mustEqual false - obj.Slot( 12).Equipment.nonEmpty mustEqual false - // - obj.Slot(15).Equipment.nonEmpty mustEqual false - obj.Slot(16).Equipment.nonEmpty mustEqual true - obj.Slot(17).Equipment.nonEmpty mustEqual true - obj.Slot(18).Equipment.nonEmpty mustEqual true - obj.Slot(19).Equipment.nonEmpty mustEqual false - // - obj.Slot(22).Equipment.nonEmpty mustEqual false - obj.Slot(23).Equipment.nonEmpty mustEqual true - obj.Slot(24).Equipment.nonEmpty mustEqual true - obj.Slot(25).Equipment.nonEmpty mustEqual true - obj.Slot(26).Equipment.nonEmpty mustEqual false - // - obj.Slot(29).Equipment.nonEmpty mustEqual false - obj.Slot(30).Equipment.nonEmpty mustEqual true - obj.Slot(31).Equipment.nonEmpty mustEqual true - obj.Slot(32).Equipment.nonEmpty mustEqual true - obj.Slot(33).Equipment.nonEmpty mustEqual false - // - obj.Slot(36).Equipment.nonEmpty mustEqual false - obj.Slot(37).Equipment.nonEmpty mustEqual false - obj.Slot(38).Equipment.nonEmpty mustEqual false - obj.Slot(39).Equipment.nonEmpty mustEqual false - obj.Slot(40).Equipment.nonEmpty mustEqual false - // - //remove - obj.Slot(16).Equipment = None - obj.Slot( 8).Equipment.nonEmpty mustEqual false - obj.Slot( 9).Equipment.nonEmpty mustEqual false - obj.Slot( 10).Equipment.nonEmpty mustEqual false - obj.Slot( 11).Equipment.nonEmpty mustEqual false - obj.Slot( 12).Equipment.nonEmpty mustEqual false - // - obj.Slot(15).Equipment.nonEmpty mustEqual false - obj.Slot(16).Equipment.nonEmpty mustEqual false - obj.Slot(17).Equipment.nonEmpty mustEqual false - obj.Slot(18).Equipment.nonEmpty mustEqual false - obj.Slot(19).Equipment.nonEmpty mustEqual false - // - obj.Slot(22).Equipment.nonEmpty mustEqual false - obj.Slot(23).Equipment.nonEmpty mustEqual false - obj.Slot(24).Equipment.nonEmpty mustEqual false - obj.Slot(25).Equipment.nonEmpty mustEqual false - obj.Slot(26).Equipment.nonEmpty mustEqual false - // - obj.Slot(29).Equipment.nonEmpty mustEqual false - obj.Slot(30).Equipment.nonEmpty mustEqual false - obj.Slot(31).Equipment.nonEmpty mustEqual false - obj.Slot(32).Equipment.nonEmpty mustEqual false - obj.Slot(33).Equipment.nonEmpty mustEqual false - // - obj.Slot(36).Equipment.nonEmpty mustEqual false - obj.Slot(37).Equipment.nonEmpty mustEqual false - obj.Slot(38).Equipment.nonEmpty mustEqual false - obj.Slot(39).Equipment.nonEmpty mustEqual false - obj.Slot(40).Equipment.nonEmpty mustEqual false - //insert again - obj.Slot(16).Equipment = bullet9mmBox2 - obj.Slot( 8).Equipment.nonEmpty mustEqual false - obj.Slot( 9).Equipment.nonEmpty mustEqual false - obj.Slot( 10).Equipment.nonEmpty mustEqual false - obj.Slot( 11).Equipment.nonEmpty mustEqual false - obj.Slot( 12).Equipment.nonEmpty mustEqual false - // - obj.Slot(15).Equipment.nonEmpty mustEqual false - obj.Slot(16).Equipment.nonEmpty mustEqual true - obj.Slot(17).Equipment.nonEmpty mustEqual true - obj.Slot(18).Equipment.nonEmpty mustEqual true - obj.Slot(19).Equipment.nonEmpty mustEqual false - // - obj.Slot(22).Equipment.nonEmpty mustEqual false - obj.Slot(23).Equipment.nonEmpty mustEqual true - obj.Slot(24).Equipment.nonEmpty mustEqual true - obj.Slot(25).Equipment.nonEmpty mustEqual true - obj.Slot(26).Equipment.nonEmpty mustEqual false - // - obj.Slot(29).Equipment.nonEmpty mustEqual false - obj.Slot(30).Equipment.nonEmpty mustEqual true - obj.Slot(31).Equipment.nonEmpty mustEqual true - obj.Slot(32).Equipment.nonEmpty mustEqual true - obj.Slot(33).Equipment.nonEmpty mustEqual false - // - obj.Slot(36).Equipment.nonEmpty mustEqual false - obj.Slot(37).Equipment.nonEmpty mustEqual false - obj.Slot(38).Equipment.nonEmpty mustEqual false - obj.Slot(39).Equipment.nonEmpty mustEqual false - obj.Slot(40).Equipment.nonEmpty mustEqual false - // - //remove - obj.Slot(16).Equipment = None - obj.Slot( 8).Equipment.nonEmpty mustEqual false - obj.Slot( 9).Equipment.nonEmpty mustEqual false - obj.Slot( 10).Equipment.nonEmpty mustEqual false - obj.Slot( 11).Equipment.nonEmpty mustEqual false - obj.Slot( 12).Equipment.nonEmpty mustEqual false - // - obj.Slot(15).Equipment.nonEmpty mustEqual false - obj.Slot(16).Equipment.nonEmpty mustEqual false - obj.Slot(17).Equipment.nonEmpty mustEqual false - obj.Slot(18).Equipment.nonEmpty mustEqual false - obj.Slot(19).Equipment.nonEmpty mustEqual false - // - obj.Slot(22).Equipment.nonEmpty mustEqual false - obj.Slot(23).Equipment.nonEmpty mustEqual false - obj.Slot(24).Equipment.nonEmpty mustEqual false - obj.Slot(25).Equipment.nonEmpty mustEqual false - obj.Slot(26).Equipment.nonEmpty mustEqual false - // - obj.Slot(29).Equipment.nonEmpty mustEqual false - obj.Slot(30).Equipment.nonEmpty mustEqual false - obj.Slot(31).Equipment.nonEmpty mustEqual false - obj.Slot(32).Equipment.nonEmpty mustEqual false - obj.Slot(33).Equipment.nonEmpty mustEqual false - // - obj.Slot(36).Equipment.nonEmpty mustEqual false - obj.Slot(37).Equipment.nonEmpty mustEqual false - obj.Slot(38).Equipment.nonEmpty mustEqual false - obj.Slot(39).Equipment.nonEmpty mustEqual false - obj.Slot(40).Equipment.nonEmpty mustEqual false - } - } } diff --git a/common/src/test/scala/objects/PlayerControlTest.scala b/common/src/test/scala/objects/PlayerControlTest.scala index a1d81b60..2fa7419a 100644 --- a/common/src/test/scala/objects/PlayerControlTest.scala +++ b/common/src/test/scala/objects/PlayerControlTest.scala @@ -32,12 +32,10 @@ class PlayerControlHealTest extends ActorTest { player1.Zone = zone player1.Spawn player1.Position = Vector3(2, 0, 0) - guid.register(player1.Locker, 5) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 player2.Zone = zone player2.Spawn - guid.register(player2.Locker, 6) player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") val tool = Tool(GlobalDefinitions.medicalapplicator) //guid=3 & 4 @@ -104,7 +102,6 @@ class PlayerControlHealSelfTest extends ActorTest { player1.Zone = zone player1.Spawn player1.Position = Vector3(2, 0, 0) - guid.register(player1.Locker, 5) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") val tool = Tool(GlobalDefinitions.medicalapplicator) //guid=3 & 4 @@ -170,12 +167,10 @@ class PlayerControlRepairTest extends ActorTest { player1.Zone = zone player1.Spawn player1.Position = Vector3(2, 0, 0) - guid.register(player1.Locker, 5) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 player2.Zone = zone player2.Spawn - guid.register(player2.Locker, 6) player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") val tool = Tool(GlobalDefinitions.bank) //guid=3 & 4 @@ -248,7 +243,6 @@ class PlayerControlRepairSelfTest extends ActorTest { player1.Zone = zone player1.Spawn player1.Position = Vector3(2, 0, 0) - guid.register(player1.Locker, 5) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") val tool = Tool(GlobalDefinitions.bank) //guid=3 & 4 @@ -315,12 +309,10 @@ class PlayerControlDamageTest extends ActorTest { player1.Zone = zone player1.Spawn player1.Position = Vector3(2, 0, 0) - guid.register(player1.Locker, 5) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 player2.Zone = zone player2.Spawn - guid.register(player2.Locker, 6) player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4 val projectile = tool.Projectile @@ -393,12 +385,10 @@ class PlayerControlDeathStandingTest extends ActorTest { player1.Zone = zone player1.Spawn player1.Position = Vector3(2,0,0) - guid.register(player1.Locker, 5) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 player2.Zone = zone player2.Spawn - guid.register(player2.Locker, 6) player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4 @@ -503,12 +493,10 @@ class PlayerControlDeathSeatedTest extends ActorTest { player1.Zone = zone player1.Spawn player1.Position = Vector3(2,0,0) - guid.register(player1.Locker, 6) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 player2.Zone = zone player2.Spawn - guid.register(player2.Locker, 7) player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") val vehicle = Vehicle(GlobalDefinitions.quadstealth) //guid=5 @@ -607,4 +595,5 @@ class PlayerControlDeathSeatedTest extends ActorTest { } } + object PlayerControlTest { } diff --git a/common/src/test/scala/objects/PlayerTest.scala b/common/src/test/scala/objects/PlayerTest.scala index ee9e43c6..69507fec 100644 --- a/common/src/test/scala/objects/PlayerTest.scala +++ b/common/src/test/scala/objects/PlayerTest.scala @@ -279,7 +279,7 @@ class PlayerTest extends Specification { "can access the player's locker-space" in { val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) - obj.Slot(5).Equipment.get.isInstanceOf[LockerEquipment] mustEqual true + obj.Slot(5).Equipment.get.isInstanceOf[LockerContainer] mustEqual true } "can find equipment" in { diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegisterAvatarTest.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterAvatarTest.scala index 632b827e..4fdd378f 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskRegisterAvatarTest.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterAvatarTest.scala @@ -18,7 +18,7 @@ class GUIDTaskRegisterAvatarTest extends ActorTest { obj.Slot(6).Equipment = obj_inv_ammo val obj_locker = obj.Slot(5).Equipment.get val obj_locker_ammo = AmmoBox(GlobalDefinitions.energy_cell) - obj_locker.asInstanceOf[LockerEquipment].Inventory += 0 -> obj_locker_ammo + obj_locker.asInstanceOf[LockerContainer].Inventory += 0 -> obj_locker_ammo assert(!obj.HasGUID) assert(!obj_wep.HasGUID) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegisterPlayerTest.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterPlayerTest.scala index e60349b3..0171d8d6 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskRegisterPlayerTest.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegisterPlayerTest.scala @@ -18,7 +18,7 @@ class GUIDTaskRegisterPlayerTest extends ActorTest { obj.Slot(6).Equipment = obj_inv_ammo val obj_locker = obj.Slot(5).Equipment.get val obj_locker_ammo = AmmoBox(GlobalDefinitions.energy_cell) - obj_locker.asInstanceOf[LockerEquipment].Inventory += 0 -> obj_locker_ammo + obj_locker.asInstanceOf[LockerContainer].Inventory += 0 -> obj_locker_ammo assert(!obj.HasGUID) assert(!obj_wep.HasGUID) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterAvatarTest.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterAvatarTest.scala index 09d8f8e3..6bddc5be 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterAvatarTest.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterAvatarTest.scala @@ -18,7 +18,7 @@ class GUIDTaskUnregisterAvatarTest extends ActorTest { obj.Slot(6).Equipment = obj_inv_ammo val obj_locker = obj.Slot(5).Equipment.get val obj_locker_ammo = AmmoBox(GlobalDefinitions.energy_cell) - obj_locker.asInstanceOf[LockerEquipment].Inventory += 0 -> obj_locker_ammo + obj_locker.asInstanceOf[LockerContainer].Inventory += 0 -> obj_locker_ammo guid.register(obj, "dynamic") guid.register(obj_wep, "dynamic") guid.register(obj_wep_ammo, "dynamic") diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterPlayerTest.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterPlayerTest.scala index 03ef7b34..3a4bf21a 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterPlayerTest.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregisterPlayerTest.scala @@ -18,7 +18,7 @@ class GUIDTaskUnregisterPlayerTest extends ActorTest { obj.Slot(6).Equipment = obj_inv_ammo val obj_locker = obj.Slot(5).Equipment.get val obj_locker_ammo = AmmoBox(GlobalDefinitions.energy_cell) - obj_locker.asInstanceOf[LockerEquipment].Inventory += 0 -> obj_locker_ammo + obj_locker.asInstanceOf[LockerContainer].Inventory += 0 -> obj_locker_ammo guid.register(obj, "dynamic") guid.register(obj_wep, "dynamic") guid.register(obj_wep_ammo, "dynamic") diff --git a/pslogin/src/main/scala/WorldSession.scala b/pslogin/src/main/scala/WorldSession.scala deleted file mode 100644 index a16f0c33..00000000 --- a/pslogin/src/main/scala/WorldSession.scala +++ /dev/null @@ -1,603 +0,0 @@ -// Copyright (c) 2020 PSForever -import akka.actor.ActorRef -import akka.pattern.{AskTimeoutException, ask} -import akka.util.Timeout -import net.psforever.objects.{AmmoBox, GlobalDefinitions, Player, Tool} -import net.psforever.objects.equipment.{Ammo, Equipment} -import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} -import net.psforever.objects.inventory.{Container, InventoryItem} -import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.serverobject.containable.Containable -import net.psforever.objects.zones.Zone -import net.psforever.packet.game.ObjectHeldMessage -import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3} -import services.Service -import services.avatar.{AvatarAction, AvatarServiceMessage} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future -import scala.concurrent.duration._ -import scala.language.implicitConversions - -object WorldSession { - /** - * Convert a boolean value into an integer value. - * Use: `true:Int` or `false:Int` - * @param b `true` or `false` (or `null`) - * @return 1 for `true`; 0 for `false` - */ - implicit def boolToInt(b : Boolean) : Int = if(b) 1 else 0 - private implicit val timeout = new Timeout(5000 milliseconds) - - /** - * Use this for placing equipment that has yet to be registered into a container, - * such as in support of changing ammunition types in `Tool` objects (weapons). - * If the object can not be placed into the container, it will be dropped onto the ground. - * It will also be dropped if it takes too long to be placed. - * Item swapping during the placement is not allowed. - * @see `ask` - * @see `ChangeAmmoMessage` - * @see `Containable.CanNotPutItemInSlot` - * @see `Containable.PutItemAway` - * @see `Future.onComplete` - * @see `Future.recover` - * @see `tell` - * @see `Zone.Ground.DropItem` - * @param obj the container - * @param item the item being manipulated - * @return a `Future` that anticipates the resolution to this manipulation - */ - def PutEquipmentInInventoryOrDrop(obj : PlanetSideServerObject with Container)(item : Equipment) : Future[Any] = { - val localContainer = obj - val localItem = item - val result = ask(localContainer.Actor, Containable.PutItemAway(localItem)) - result.onComplete { - case scala.util.Failure(_) | scala.util.Success(_ : Containable.CanNotPutItemInSlot) => - localContainer.Zone.Ground.tell(Zone.Ground.DropItem(localItem, localContainer.Position, Vector3.z(localContainer.Orientation.z)), localContainer.Actor) - case _ => ; - } - result - } - - /** - * Use this for placing equipment that has yet to be registered into a container, - * such as in support of changing ammunition types in `Tool` objects (weapons). - * Equipment will go wherever it fits in containing object, or be dropped if it fits nowhere. - * Item swapping during the placement is not allowed. - * @see `ChangeAmmoMessage` - * @see `GUIDTask.RegisterEquipment` - * @see `PutEquipmentInInventoryOrDrop` - * @see `Task` - * @see `TaskResolver.GiveTask` - * @param obj the container - * @param item the item being manipulated - * @return a `TaskResolver` object - */ - def PutNewEquipmentInInventoryOrDrop(obj : PlanetSideServerObject with Container)(item : Equipment) : TaskResolver.GiveTask = { - val localZone = obj.Zone - TaskResolver.GiveTask( - new Task() { - private val localContainer = obj - private val localItem = item - - override def isComplete : Task.Resolution.Value = Task.Resolution.Success - - def Execute(resolver : ActorRef) : Unit = { - PutEquipmentInInventoryOrDrop(localContainer)(localItem) - resolver ! scala.util.Success(this) - } - }, - List(GUIDTask.RegisterEquipment(item)(localZone.GUID)) - ) - } - - /** - * Use this for obtaining new equipment from a loadout specification. - * The loadout specification contains a specific slot position for placing the item. - * This request will (probably) be coincidental with a number of other such requests based on that loadout - * so items must be rigidly placed else cascade into a chaostic order. - * Item swapping during the placement is not allowed. - * @see `ask` - * @see `AvatarAction.ObjectDelete` - * @see `ChangeAmmoMessage` - * @see `Containable.CanNotPutItemInSlot` - * @see `Containable.PutItemAway` - * @see `Future.onComplete` - * @see `Future.recover` - * @see `GUIDTask.UnregisterEquipment` - * @see `tell` - * @see `Zone.AvatarEvents` - * @param obj the container - * @param taskResolver na - * @param item the item being manipulated - * @param slot na - * @return a `Future` that anticipates the resolution to this manipulation - */ - def PutEquipmentInInventorySlot(obj : PlanetSideServerObject with Container, taskResolver : ActorRef)(item : Equipment, slot : Int) : Future[Any] = { - val localContainer = obj - val localItem = item - val localResolver = taskResolver - val result = ask(localContainer.Actor, Containable.PutItemInSlotOnly(localItem, slot)) - result.onComplete { - case scala.util.Failure(_) | scala.util.Success(_ : Containable.CanNotPutItemInSlot) => - localResolver ! GUIDTask.UnregisterEquipment(localItem)(localContainer.Zone.GUID) - case _ => ; - } - result - } - - /** - * Use this for obtaining new equipment from a loadout specification. - * The loadout specification contains a specific slot position for placing the item. - * This request will (probably) be coincidental with a number of other such requests based on that loadout - * so items must be rigidly placed else cascade into a chaostic order. - * Item swapping during the placement is not allowed. - * @see `GUIDTask.RegisterEquipment` - * @see `PutEquipmentInInventorySlot` - * @see `Task` - * @see `TaskResolver.GiveTask` - * @param obj the container - * @param taskResolver na - * @param item the item being manipulated - * @param slot where the item will be placed in the container - * @return a `TaskResolver` object - */ - def PutLoadoutEquipmentInInventory(obj : PlanetSideServerObject with Container, taskResolver : ActorRef)(item : Equipment, slot : Int) : TaskResolver.GiveTask = { - val localZone = obj.Zone - TaskResolver.GiveTask( - new Task() { - private val localContainer = obj - private val localItem = item - private val localSlot = slot - private val localFunc : (Equipment,Int)=>Future[Any] = PutEquipmentInInventorySlot(obj, taskResolver) - - override def Timeout : Long = 1000 - - override def isComplete : Task.Resolution.Value = { - if(localItem.HasGUID && localContainer.Find(localItem).nonEmpty) - Task.Resolution.Success - else - Task.Resolution.Incomplete - } - - override def Description : String = s"PutEquipmentInInventorySlot - ${localItem.Definition.Name}" - - def Execute(resolver : ActorRef) : Unit = { - localFunc(localItem, localSlot) - resolver ! scala.util.Success(this) - } - }, - List(GUIDTask.RegisterEquipment(item)(localZone.GUID)) - ) - } - - /** - * Used for purchasing new equipment from a terminal and placing it somewhere in a player's loadout. - * Two levels of query are performed here based on the behavior expected of the item. - * First, an attempt is made to place the item anywhere in the target container as long as it does not cause swap items to be generated. - * Second, if it fails admission to the target container, an attempt is made to place it into the target player's free hand. - * If the container and the suggested player are the same, it will skip the second attempt. - * As a terminal operation, the player must receive a report regarding whether the transaction was successful. - * @see `ask` - * @see `Containable.CanNotPutItemInSlot` - * @see `Containable.PutItemInSlotOnly` - * @see `GUIDTask.RegisterEquipment` - * @see `GUIDTask.UnregisterEquipment` - * @see `Future.onComplete` - * @see `PutEquipmentInInventorySlot` - * @see `TerminalMessageOnTimeout` - * @param obj the container - * @param taskResolver na - * @param player na - * @param term na - * @param item the item being manipulated - * @return a `TaskResolver` object - */ - def BuyNewEquipmentPutInInventory(obj : PlanetSideServerObject with Container, taskResolver : ActorRef, player : Player, term : PlanetSideGUID)(item : Equipment) : TaskResolver.GiveTask = { - val localZone = obj.Zone - TaskResolver.GiveTask( - new Task() { - private val localContainer = obj - private val localItem = item - private val localPlayer = player - private val localResolver = taskResolver - private val localTermMsg : Boolean=>Unit = TerminalResult(term, localPlayer, TransactionType.Buy) - - override def Timeout : Long = 1000 - - override def isComplete : Task.Resolution.Value = { - if(localItem.HasGUID && localContainer.Find(localItem).nonEmpty) - Task.Resolution.Success - else - Task.Resolution.Incomplete - } - - def Execute(resolver : ActorRef) : Unit = { - TerminalMessageOnTimeout( - ask(localContainer.Actor, Containable.PutItemAway(localItem)), - localTermMsg - ) - .onComplete { - case scala.util.Failure(_) | scala.util.Success(_ : Containable.CanNotPutItemInSlot) => - if(localContainer != localPlayer) { - TerminalMessageOnTimeout( - PutEquipmentInInventorySlot(localPlayer, localResolver)(localItem, Player.FreeHandSlot), - localTermMsg - ) - .onComplete { - case scala.util.Failure(_) | scala.util.Success(_ : Containable.CanNotPutItemInSlot) => - localTermMsg(false) - case _ => - localTermMsg(true) - } - } - else { - localResolver ! GUIDTask.UnregisterEquipment(localItem)(localContainer.Zone.GUID) - localTermMsg(false) - } - case _ => - localTermMsg(true) - } - resolver ! scala.util.Success(this) - } - }, - List(GUIDTask.RegisterEquipment(item)(localZone.GUID)) - ) - } - - /** - * The primary use is to register new mechanized assault exo-suit armaments, - * place the newly registered weapon in hand, - * and then raise that hand (draw that slot) so that the weapon is active. - * (Players in MAX suits can not manipulate their drawn slot manually.) - * In general, this can be used for any equipment that is to be equipped to a player's hand then immediately drawn. - * Do not allow the item to be (mis)placed in any available slot. - * Item swapping during the placement is not allowed and the possibility should be proactively avoided. - * @throws `RuntimeException` if slot is not a player visible slot (holsters) - * @see `ask` - * @see `AvatarAction.ObjectDelete` - * @see `AvatarAction.SendResponse` - * @see `Containable.CanNotPutItemInSlot` - * @see `Containable.PutItemInSlotOnly` - * @see `GUIDTask.RegisterEquipment` - * @see `GUIDTask.UnregisterEquipment` - * @see `Future.onComplete` - * @see `ObjectHeldMessage` - * @see `Player.DrawnSlot` - * @see `Player.LastDrawnSlot` - * @see `Service.defaultPlayerGUID` - * @see `TaskResolver.GiveTask` - * @see `Zone.AvatarEvents` - * @param player the player whose visible slot will be equipped and drawn - * @param taskResolver na - * @param item the item to equip - * @param slot the slot in which the item will be equipped - * @return a `TaskResolver` object - */ - def HoldNewEquipmentUp(player : Player, taskResolver : ActorRef)(item : Equipment, slot : Int) : TaskResolver.GiveTask = { - if(player.VisibleSlots.contains(slot)) { - val localZone = player.Zone - TaskResolver.GiveTask( - new Task() { - private val localPlayer = player - private val localGUID = player.GUID - private val localItem = item - private val localSlot = slot - private val localResolver = taskResolver - - override def Timeout : Long = 1000 - - override def isComplete : Task.Resolution.Value = { - if(localPlayer.DrawnSlot == localSlot) - Task.Resolution.Success - else - Task.Resolution.Incomplete - } - - def Execute(resolver : ActorRef) : Unit = { - ask(localPlayer.Actor, Containable.PutItemInSlotOnly(localItem, localSlot)) - .onComplete { - case scala.util.Failure(_) | scala.util.Success(_ : Containable.CanNotPutItemInSlot) => - localResolver ! GUIDTask.UnregisterEquipment(localItem)(localZone.GUID) - case _ => - if(localPlayer.DrawnSlot != Player.HandsDownSlot) { - localPlayer.DrawnSlot = Player.HandsDownSlot - localZone.AvatarEvents ! AvatarServiceMessage(localPlayer.Name, - AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectHeldMessage(localGUID, Player.HandsDownSlot, false)) - ) - localZone.AvatarEvents ! AvatarServiceMessage(localZone.Id, AvatarAction.ObjectHeld(localGUID, localPlayer.LastDrawnSlot)) - } - localPlayer.DrawnSlot = localSlot - localZone.AvatarEvents ! AvatarServiceMessage(localZone.Id, - AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectHeldMessage(localGUID, localSlot, false)) - ) - } - resolver ! scala.util.Success(this) - } - }, - List(GUIDTask.RegisterEquipment(item)(localZone.GUID)) - ) - } - else { - //TODO log.error - throw new RuntimeException(s"provided slot $slot is not a player visible slot (holsters)") - } - } - - /** - * Get an item from the ground and put it into the given container. - * The zone in which the item is found is expected to be the same in which the container object is located. - * If the object can not be placed into the container, it is put back on the ground. - * The item that was collected off the ground, if it is placed back on the ground, - * will be positioned with respect to the container object rather than its original location. - * @see `ask` - * @see `AvatarAction.ObjectDelete` - * @see `Future.onComplete` - * @see `Zone.AvatarEvents` - * @see `Zone.Ground.CanNotPickUpItem` - * @see `Zone.Ground.ItemInHand` - * @see `Zone.Ground.PickUpItem` - * @see `PutEquipmentInInventoryOrDrop` - * @param obj the container into which the item will be placed - * @param item the item being collected from off the ground of the container's zone - * @return a `Future` that anticipates the resolution to this manipulation - */ - def PickUpEquipmentFromGround(obj : PlanetSideServerObject with Container)(item : Equipment) : Future[Any] = { - val localZone = obj.Zone - val localContainer = obj - val localItem = item - val future = ask(localZone.Ground, Zone.Ground.PickupItem(item.GUID)) - future.onComplete { - case scala.util.Success(Zone.Ground.ItemInHand(_)) => - PutEquipmentInInventoryOrDrop(localContainer)(localItem) - case scala.util.Success(Zone.Ground.CanNotPickupItem(_, item_guid, _)) => - localZone.GUID(item_guid) match { - case Some(_) => ; - case None => //acting on old data? - localZone.AvatarEvents ! AvatarServiceMessage(localZone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item_guid)) - } - case _ => ; - } - future - } - - /** - * Remove an item from a container and drop it on the ground. - * @see `ask` - * @see `AvatarAction.ObjectDelete` - * @see `Containable.ItemFromSlot` - * @see `Containable.RemoveItemFromSlot` - * @see `Future.onComplete` - * @see `Future.recover` - * @see `tell` - * @see `Zone.AvatarEvents` - * @see `Zone.Ground.DropItem` - * @param obj the container to search - * @param item the item to find and remove from the container - * @param pos an optional position where to drop the item on the ground; - * expected override from original container's position - * @return a `Future` that anticipates the resolution to this manipulation - */ - def DropEquipmentFromInventory(obj : PlanetSideServerObject with Container)(item : Equipment, pos : Option[Vector3] = None) : Future[Any] = { - val localContainer = obj - val localItem = item - val localPos = pos - val result = ask(localContainer.Actor, Containable.RemoveItemFromSlot(localItem)) - result.onComplete { - case scala.util.Success(Containable.ItemFromSlot(_, Some(_), Some(_))) => - localContainer.Zone.Ground.tell(Zone.Ground.DropItem(localItem, localPos.getOrElse(localContainer.Position), Vector3.z(localContainer.Orientation.z)), localContainer.Actor) - case _ => ; - } - result - } - - /** - * Remove an item from a container and delete it. - * @see `ask` - * @see `AvatarAction.ObjectDelete` - * @see `Containable.ItemFromSlot` - * @see `Containable.RemoveItemFromSlot` - * @see `Future.onComplete` - * @see `Future.recover` - * @see `GUIDTask.UnregisterEquipment` - * @see `Zone.AvatarEvents` - * @param obj the container to search - * @param taskResolver na - * @param item the item to find and remove from the container - * @return a `Future` that anticipates the resolution to this manipulation - */ - def RemoveOldEquipmentFromInventory(obj : PlanetSideServerObject with Container, taskResolver : ActorRef)(item : Equipment) : Future[Any] = { - val localContainer = obj - val localItem = item - val localResolver = taskResolver - val result = ask(localContainer.Actor, Containable.RemoveItemFromSlot(localItem)) - result.onComplete { - case scala.util.Success(Containable.ItemFromSlot(_, Some(_), Some(_))) => - localResolver ! GUIDTask.UnregisterEquipment(localItem)(localContainer.Zone.GUID) - case _ => - } - result - } - - /** - * Primarily, remove an item from a container and delete it. - * As a terminal operation, the player must receive a report regarding whether the transaction was successful. - * At the end of a successful transaction, and only a successful transaction, - * the item that was removed is no longer considered a valid game object. - * Contrasting `RemoveOldEquipmentFromInventory` which identifies the actual item to be eliminated, - * this function uses the slot where the item is (should be) located. - * @see `ask` - * @see `Containable.ItemFromSlot` - * @see `Containable.RemoveItemFromSlot` - * @see `Future.onComplete` - * @see `Future.recover` - * @see `GUIDTask.UnregisterEquipment` - * @see `RemoveOldEquipmentFromInventory` - * @see `TerminalMessageOnTimeout` - * @see `TerminalResult` - * @param obj the container to search - * @param taskResolver na - * @param player the player who used the terminal - * @param term the unique identifier number of the terminal - * @param slot from which slot the equipment is to be removed - * @return a `Future` that anticipates the resolution to this manipulation - */ - def SellEquipmentFromInventory(obj : PlanetSideServerObject with Container, taskResolver : ActorRef, player : Player, term : PlanetSideGUID)(slot : Int) : Future[Any] = { - val localContainer = obj - val localPlayer = player - val localSlot = slot - val localResolver = taskResolver - val localTermMsg : Boolean=>Unit = TerminalResult(term, localPlayer, TransactionType.Sell) - val result = TerminalMessageOnTimeout( - ask(localContainer.Actor, Containable.RemoveItemFromSlot(localSlot)), - localTermMsg - ) - result.onComplete { - case scala.util.Success(Containable.ItemFromSlot(_, Some(item), Some(_))) => - localResolver ! GUIDTask.UnregisterEquipment(item)(localContainer.Zone.GUID) - localTermMsg(true) - case _ => - localTermMsg(false) - } - result - } - - /** - * If a timeout occurs on the manipulation, declare a terminal transaction failure. - * @see `AskTimeoutException` - * @see `recover` - * @param future the item manipulation's `Future` object - * @param terminalMessage how to call the terminal message - * @return a `Future` that anticipates the resolution to this manipulation - */ - def TerminalMessageOnTimeout(future : Future[Any], terminalMessage : Boolean=>Unit) : Future[Any] = { - future.recover { - case _ : AskTimeoutException => - terminalMessage(false) - } - } - - /** - * Announced the result of this player's terminal use, to the player that used the terminal. - * This is a necessary step for regaining terminal use which is naturally blocked by the client after a transaction request. - * @see `AvatarAction.TerminalOrderResult` - * @see `ItemTransactionResultMessage` - * @see `TransactionType` - * @param guid the terminal's unique identifier - * @param player the player who used the terminal - * @param transaction what kind of transaction was involved in terminal use - * @param result the result of that transaction - */ - def TerminalResult(guid : PlanetSideGUID, player : Player, transaction : TransactionType.Value)(result : Boolean) : Unit = { - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.TerminalOrderResult(guid, transaction, result)) - } - - /** - * Drop some items on the ground is a given location. - * The location corresponds to the previous container for those items. - * @see `Zone.Ground.DropItem` - * @param container the original object that contained the items - * @param drops the items to be dropped on the ground - */ - def DropLeftovers(container : PlanetSideServerObject with Container)(drops : List[InventoryItem]) : Unit = { - //drop or retire - val zone = container.Zone - val pos = container.Position - val orient = Vector3.z(container.Orientation.z) - //TODO make a sound when dropping stuff? - drops.foreach { entry => zone.Ground.tell(Zone.Ground.DropItem(entry.obj, pos, orient), container.Actor) } - } - - /** - * Within a specified `Container`, find the smallest number of `Equipment` objects of a certain qualifying type - * whose sum count is greater than, or equal to, a `desiredAmount` based on an accumulator method.
- *
- * In an occupied `List` of returned `Inventory` entries, all but the last entry is typically considered "emptied." - * For objects with contained quantities, the last entry may require having that quantity be set to a non-zero number. - * @param obj the `Container` to search - * @param filterTest test used to determine inclusivity of `Equipment` collection - * @param desiredAmount how much is requested - * @param counting test used to determine value of found `Equipment`; - * defaults to one per entry - * @return a `List` of all discovered entries totaling approximately the amount requested - */ - def FindEquipmentStock(obj : Container, - filterTest : Equipment=>Boolean, - desiredAmount : Int, - counting : Equipment=>Int = DefaultCount) : List[InventoryItem] = { - var currentAmount : Int = 0 - obj.Inventory.Items - .filter(item => filterTest(item.obj)) - .sortBy(_.start) - .takeWhile(entry => { - val previousAmount = currentAmount - currentAmount += counting(entry.obj) - previousAmount < desiredAmount - }) - } - - - /** - * The default counting function for an item. - * Counts the number of item(s). - * @param e the `Equipment` object - * @return the quantity; - * always one - */ - def DefaultCount(e : Equipment) : Int = 1 - - /** - * The counting function for an item of `AmmoBox`. - * Counts the `Capacity` of the ammunition. - * @param e the `Equipment` object - * @return the quantity - */ - def CountAmmunition(e : Equipment) : Int = { - e match { - case a : AmmoBox => a.Capacity - case _ => 0 - } - } - - /** - * The counting function for an item of `Tool` where the item is also a grenade. - * Counts the number of grenades. - * @see `GlobalDefinitions.isGrenade` - * @param e the `Equipment` object - * @return the quantity - */ - def CountGrenades(e : Equipment) : Int = { - e match { - case t : Tool => (GlobalDefinitions.isGrenade(t.Definition):Int) * t.Magazine - case _ => 0 - } - } - - /** - * Flag an `AmmoBox` object that matches for the given ammunition type. - * @param ammo the type of `Ammo` to check - * @param e the `Equipment` object - * @return `true`, if the object is an `AmmoBox` of the correct ammunition type; `false`, otherwise - */ - def FindAmmoBoxThatUses(ammo : Ammo.Value)(e : Equipment) : Boolean = { - e match { - case t : AmmoBox => t.AmmoType == ammo - case _ => false - } - } - - /** - * Flag a `Tool` object that matches for loading the given ammunition type. - * @param ammo the type of `Ammo` to check - * @param e the `Equipment` object - * @return `true`, if the object is a `Tool` that loads the correct ammunition type; `false`, otherwise - */ - def FindToolThatUses(ammo : Ammo.Value)(e : Equipment) : Boolean = { - e match { - case t : Tool => - t.Definition.AmmoTypes.map { _.AmmoType }.contains(ammo) - case _ => - false - } - } -} diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index b999ce2e..00700123 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1,8 +1,6 @@ // Copyright (c) 2017-2020 PSForever //language imports import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} -import akka.pattern.ask -import akka.util.Timeout import com.github.mauricio.async.db.general.ArrayRowData import com.github.mauricio.async.db.{Connection, QueryResult} import java.util.concurrent.TimeUnit @@ -33,7 +31,6 @@ import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, SquadLoadout, V import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.damage.Damageable -import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior} import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.generator.Generator @@ -78,7 +75,6 @@ class WorldSessionActor extends Actor with MDCContextAware { import WorldSessionActor._ - import WorldSession._ private[this] val log = org.log4s.getLogger private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel) @@ -112,7 +108,17 @@ class WorldSessionActor extends Actor //keep track of avatar's ServerVehicleOverride state var traveler : Traveler = null var deadState : DeadState.Value = DeadState.Dead + var whenUsedLastAAMAX : Long = 0 + var whenUsedLastAIMAX : Long = 0 + var whenUsedLastAVMAX : Long = 0 + var whenUsedLastMAX : Array[Long] = Array.fill[Long](4)(0L) var whenUsedLastMAXName : Array[String] = Array.fill[String](4)("") + var whenUsedLastItem : Array[Long] = Array.fill[Long](1020)(0L) + var whenUsedLastItemName : Array[String] = Array.fill[String](1020)("") + var whenUsedLastKit : Long = 0 + var whenUsedLastSMKit : Long = 0 + var whenUsedLastSAKit : Long = 0 + var whenUsedLastSSKit : Long = 0 val projectiles : Array[Option[Projectile]] = Array.fill[Option[Projectile]](Projectile.RangeUID - Projectile.BaseUID)(None) val projectilesToCleanUp : Array[Boolean] = Array.fill[Boolean](Projectile.RangeUID - Projectile.BaseUID)(false) var drawDeloyableIcon : PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons @@ -143,7 +149,7 @@ class WorldSessionActor extends Actor var squad_supplement_id : Int = 0 /** * When joining or creating a squad, the original state of the avatar's internal LFS variable is blanked. - * This `WorldSessionActor`-local variable is then used to indicate the ongoing state of the LFS UI component, + * This `WSA`-local variable is then used to indicate the ongoing state of the LFS UI component, * now called "Looking for Squad Member." * Only the squad leader may toggle the LFSM marquee. * Upon leaving or disbanding a squad, this value is made false. @@ -176,6 +182,17 @@ class WorldSessionActor extends Actor var antDischargingTick : Cancellable = DefaultCancellable.obj var zoningTimer : Cancellable = DefaultCancellable.obj var zoningReset : Cancellable = DefaultCancellable.obj + /** + * Convert a boolean value into an integer value. + * Use: `true:Int` or `false:Int` + * @param b `true` or `false` (or `null`) + * @return 1 for `true`; 0 for `false` + */ + + import scala.language.implicitConversions + + implicit def boolToInt(b : Boolean) : Int = if(b) 1 + else 0 override def postStop() : Unit = { //normally, the player avatar persists a minute or so after disconnect; we are subject to the SessionReaper @@ -244,7 +261,7 @@ class WorldSessionActor extends Actor case None if id.nonEmpty && id.get != PlanetSideGUID(0) => //delete stale entity reference from client log.warn(s"Player ${player.Name} has an invalid reference to GUID ${id.get} in zone ${continent.Id}.") - sendResponse(ObjectDeleteMessage(id.get, 0)) + //sendResponse(ObjectDeleteMessage(id.get, 0)) None case _ => None @@ -861,6 +878,82 @@ class WorldSessionActor extends Actor case msg@Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) => log.warn(s"$msg") + case Zone.Ground.ItemOnGround(item : BoomerTrigger, pos, orient) => + //dropped the trigger, no longer own the boomer; make certain whole faction is aware of that + val playerGUID = player.GUID + continent.GUID(item.Companion) match { + case Some(obj : BoomerDeployable) => + val guid = obj.GUID + val factionChannel = s"${player.Faction}" + obj.AssignOwnership(None) + avatar.Deployables.Remove(obj) + UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(obj.Definition.Item)) + continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent)) + obj.Faction = PlanetSideEmpire.NEUTRAL + sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL)) + continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.SetEmpire(playerGUID, guid, PlanetSideEmpire.NEUTRAL)) + val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, PlanetSideGUID(0)) + sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Dismiss, info)) + continent.LocalEvents ! LocalServiceMessage(factionChannel, LocalAction.DeployableMapIcon(playerGUID, DeploymentAction.Dismiss, info)) + PutItemOnGround(item, pos, orient) + case Some(_) | None => + //pointless trigger + val guid = item.GUID + continent.Ground ! Zone.Ground.RemoveItem(guid) //undo; no callback + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), guid)) + taskResolver ! GUIDTask.UnregisterObjectTask(item)(continent.GUID) + } + + case Zone.Ground.ItemOnGround(item : ConstructionItem, pos, orient) => + //defensively, reset CItem configuration + item.FireModeIndex = 0 + item.AmmoTypeIndex = 0 + PutItemOnGround(item, pos, orient) + + case Zone.Ground.ItemOnGround(item : PlanetSideGameObject, pos, orient) => + PutItemOnGround(item, pos, orient) + + case Zone.Ground.CanNotDropItem(zone, item, reason) => + log.warn(s"DropItem: $player tried to drop a $item on the ground, but $reason") + if(!item.HasGUID) { + log.warn(s"DropItem: zone ${continent.Id} contents may be in disarray") + } + + case Zone.Ground.ItemInHand(item : BoomerTrigger) => + if(PutItemInHand(item)) { + //pick up the trigger, own the boomer; make certain whole faction is aware of that + continent.GUID(item.Companion) match { + case Some(obj : BoomerDeployable) => + val guid = obj.GUID + val playerGUID = player.GUID + val faction = player.Faction + val factionChannel = s"$faction" + obj.AssignOwnership(player) + obj.Faction = faction + avatar.Deployables.Add(obj) + UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(obj.Definition.Item)) + continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(obj), continent)) + sendResponse(SetEmpireMessage(guid, faction)) + continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.SetEmpire(playerGUID, guid, faction)) + val info = DeployableInfo(obj.GUID, DeployableIcon.Boomer, obj.Position, obj.Owner.getOrElse(PlanetSideGUID(0))) + sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, info)) + continent.LocalEvents ! LocalServiceMessage(factionChannel, LocalAction.DeployableMapIcon(playerGUID, DeploymentAction.Build, info)) + case Some(_) | None => ; //pointless trigger; see Zone.Ground.ItemOnGround(BoomerTrigger, ...) + } + } + + case Zone.Ground.ItemInHand(item : Equipment) => + PutItemInHand(item) + + case Zone.Ground.CanNotPickupItem(zone, item_guid, _) => + zone.GUID(item_guid) match { + case Some(item) => + log.warn(s"DropItem: finding a $item on the ground was suggested, but $player can not reach it") + case None => + log.warn(s"DropItem: finding an item ($item_guid) on the ground was suggested, but $player can not see it") + sendResponse(ObjectDeleteMessage(item_guid, 0)) + } + case Zone.Deployable.DeployableIsBuilt(obj, tool) => val index = player.Find(tool) match { case Some(x) => @@ -887,7 +980,7 @@ class WorldSessionActor extends Actor ) } else { - TryDropFDU(tool, index, obj.Position) + TryDropConstructionTool(tool, index, obj.Position) sendResponse(ObjectDeployedMessage.Failure(obj.Definition.Name)) obj.Position = Vector3.Zero obj.AssignOwnership(None) @@ -917,11 +1010,11 @@ class WorldSessionActor extends Actor val holster = player.Slot(index) if(holster.Equipment.contains(tool)) { holster.Equipment = None - taskResolver ! HoldNewEquipmentUp(player, taskResolver)(trigger, index) + taskResolver ! DelayedObjectHeld(player, index, List(PutEquipmentInSlot(player, trigger, index))) } else { //don't know where boomer trigger should go; drop it on the ground - taskResolver ! NewItemDrop(player, continent)(trigger) + taskResolver ! NewItemDrop(player, continent, continent.AvatarEvents)(trigger) } StopBundlingPackets() @@ -960,7 +1053,7 @@ class WorldSessionActor extends Actor log.info(s"FinalizeDeployable: setup for telepad #${guid.guid} in zone ${continent.Id}") obj.Router = routerGUID //necessary; forwards link to the router DeployableBuildActivity(obj) - RemoveOldEquipmentFromInventory(player, taskResolver)(tool) + CommonDestroyConstructionItem(tool, index) StopBundlingPackets() //it takes 60s for the telepad to become properly active continent.LocalEvents ! LocalServiceMessage.Telepads(RouterTelepadActivation.AddTask(obj, continent)) @@ -982,16 +1075,16 @@ class WorldSessionActor extends Actor sendResponse(ObjectDeployedMessage.Failure(definition.Name)) log.warn(s"FinalizeDeployable: deployable ${definition.asInstanceOf[BaseDeployableDefinition].Item}@$guid not handled by specific case") log.warn(s"FinalizeDeployable: deployable will be cleaned up, but may not get unregistered properly") - TryDropFDU(tool, index, obj.Position) + TryDropConstructionTool(tool, index, obj.Position) obj.Position = Vector3.Zero continent.Deployables ! Zone.Deployable.Dismiss(obj) StopBundlingPackets() - //!!only dispatch Zone.Deployable.Dismiss from WorldSessionActor as cleanup if the target deployable was never fully introduced + //!!only dispatch Zone.Deployable.Dismiss from WSA as cleanup if the target deployable was never fully introduced case Zone.Deployable.DeployableIsDismissed(obj : TurretDeployable) => taskResolver ! GUIDTask.UnregisterDeployableTurret(obj)(continent.GUID) - //!!only dispatch Zone.Deployable.Dismiss from WorldSessionActor as cleanup if the target deployable was never fully introduced + //!!only dispatch Zone.Deployable.Dismiss from WSA as cleanup if the target deployable was never fully introduced case Zone.Deployable.DeployableIsDismissed(obj) => taskResolver ! GUIDTask.UnregisterObjectTask(obj)(continent.GUID) @@ -1554,7 +1647,7 @@ class WorldSessionActor extends Actor // avatar.Certifications += BattleFrameRobotics // avatar.Certifications += BFRAntiInfantry // avatar.Certifications += BFRAntiAircraft - Deployables.InitializeDeployableQuantities(avatar) //set deployables ui elements + InitializeDeployableQuantities(avatar) //set deployables ui elements AwardBattleExperiencePoints(avatar, 20000000L) avatar.CEP = 600000 avatar.Implants(0).Unlocked = true @@ -1581,7 +1674,7 @@ class WorldSessionActor extends Actor case PlayerToken.LoginInfo(playerName, inZone, pos) => log.info(s"LoginInfo: player $playerName is already logged in zone ${inZone.Id}; rejoining that character") persist = UpdatePersistence(sender) - //tell the old WorldSessionActor to kill itself by using its own subscriptions against itself + //tell the old WSA to kill itself by using its own subscriptions against itself inZone.AvatarEvents ! AvatarServiceMessage(playerName, AvatarAction.TeardownConnection()) //find and reload previous player (inZone.Players.find(p => p.name.equals(playerName)), inZone.LivePlayers.find(p => p.Name.equals(playerName))) match { @@ -1601,11 +1694,12 @@ class WorldSessionActor extends Actor player.Zone = inZone setupAvatarFunc = AvatarDeploymentPassOver beginZoningSetCurrentAvatarFunc = SetCurrentAvatarUponDeployment + p.Release + inZone.Population ! Zone.Population.Release(avatar) if(p.VehicleSeated.isEmpty) { PrepareToTurnPlayerIntoCorpse(p, inZone) } else { - inZone.Population ! Zone.Population.Release(avatar) inZone.GUID(p.VehicleSeated) match { case Some(v : Vehicle) if v.Destroyed => v.Actor ! Vehicle.Deconstruct( @@ -1644,12 +1738,6 @@ class WorldSessionActor extends Actor self ! PlayerToken.LoginInfo(playerName, Zone.Nowhere, pos) } - case msg @ Containable.ItemPutInSlot(_ : PlanetSideServerObject with Container, _ : Equipment, _ : Int, _ : Option[Equipment]) => - log.info(s"$msg") - - case msg @ Containable.CanNotPutItemInSlot(_ : PlanetSideServerObject with Container, _ : Equipment, _ : Int) => - log.info(s"$msg") - case default => log.warn(s"Invalid packet class received: $default from $sender") } @@ -2186,155 +2274,10 @@ class WorldSessionActor extends Actor sendResponse(WeaponDryFireMessage(weapon_guid)) } - case AvatarResponse.TerminalOrderResult(terminal_guid, action, result) => - sendResponse(ItemTransactionResultMessage(terminal_guid, action, result)) - lastTerminalOrderFulfillment = true - - case AvatarResponse.ChangeExosuit(target, exosuit, subtype, slot, maxhand, old_holsters, holsters, old_inventory, inventory, drop, delete) => - StartBundlingPackets() - sendResponse(ArmorChangedMessage(target, exosuit, subtype)) - sendResponse(PlanetsideAttributeMessage(target, 4, player.Armor)) - if(tplayer_guid == target) { - //happening to this player - if(exosuit == ExoSuitType.MAX) { - sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastMAXName(subtype), 300, true)) - } - //cleanup - sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, false)) - (old_holsters ++ old_inventory ++ delete).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 0)) } - //functionally delete - delete.foreach { case (obj, _) => taskResolver ! GUIDTask.UnregisterEquipment(obj)(continent.GUID) } - //redraw - if(maxhand) { - taskResolver ! HoldNewEquipmentUp(player, taskResolver)(Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)), 0) - } - //draw free hand - player.FreeHand.Equipment match { - case Some(obj) => - val definition = obj.Definition - sendResponse( - ObjectCreateDetailedMessage( - definition.ObjectId, - obj.GUID, - ObjectCreateMessageParent(target, Player.FreeHandSlot), - definition.Packet.DetailedConstructorData(obj).get - ) - ) - case None => ; - } - //draw holsters and inventory - (holsters ++ inventory).foreach { case InventoryItem(obj, index) => - val definition = obj.Definition - sendResponse( - ObjectCreateDetailedMessage( - definition.ObjectId, - obj.GUID, - ObjectCreateMessageParent(target, index), - definition.Packet.DetailedConstructorData(obj).get - ) - ) - } - DropLeftovers(player)(drop) - } - else { - //happening to some other player - sendResponse(ObjectHeldMessage(target, slot, false)) - //cleanup - (old_holsters ++ delete).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 0)) } - //draw holsters - holsters.foreach { case InventoryItem(obj, index) => - val definition = obj.Definition - sendResponse( - ObjectCreateMessage( - definition.ObjectId, - obj.GUID, - ObjectCreateMessageParent(target, index), - definition.Packet.ConstructorData(obj).get - ) - ) - } - } - StopBundlingPackets() - - case AvatarResponse.ChangeLoadout(target, exosuit, subtype, slot, maxhand, old_holsters, holsters, old_inventory, inventory, drops) => - StartBundlingPackets() - sendResponse(ArmorChangedMessage(target, exosuit, subtype)) - sendResponse(PlanetsideAttributeMessage(target, 4, player.Armor)) - if(tplayer_guid == target) { - //happening to this player - if(exosuit == ExoSuitType.MAX) { - sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastMAXName(subtype), 300, true)) - } - sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, false)) - //cleanup - (old_holsters ++ old_inventory).foreach { case (obj, guid) => - sendResponse(ObjectDeleteMessage(guid, 0)) - taskResolver ! GUIDTask.UnregisterEquipment(obj)(continent.GUID) - } - //redraw - if(maxhand) { - taskResolver ! HoldNewEquipmentUp(player, taskResolver)(Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)), 0) - } - ApplyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory) - DropLeftovers(player)(drops) - } - else { - //happening to some other player - sendResponse(ObjectHeldMessage(target, slot, false)) - //cleanup - old_holsters.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 0)) } - //redraw handled by callback - } - StopBundlingPackets() - case _ => ; } } - /** - * Enforce constraints on bulk purchases as determined by a given player's previous purchase times and hard acquisition delays. - * Intended to assist in sanitizing loadout information from the perspectvie of the player, or target owner. - * The equipment is expected to be unregistered and already fitted to their ultimate slot in the target container. - * @see `AvatarVehicleTimerMessage` - * @see `Container` - * @see `delayedPurchaseEntries` - * @see `InventoryItem` - * @see `Player.GetLastUsedTime` - * @see `Player.SetLastUsedTime` - * @see `TaskResolver.GiveTask` - * @see `WorldSession.PutLoadoutEquipmentInInventory` - * @param player the player whose purchasing constraints are to be tested - * @param target the location in which the equipment will be stowed - * @param slots the equipment, in the standard object-slot format container - */ - def ApplyPurchaseTimersBeforePackingLoadout(player : Player, target : PlanetSideServerObject with Container, slots : List[InventoryItem]) : Unit = { - //depiction of packed equipment is handled through callbacks - val loadoutEquipmentFunc : (Equipment, Int)=>TaskResolver.GiveTask = PutLoadoutEquipmentInInventory(target, taskResolver) - val time = System.currentTimeMillis - slots.collect { case _obj@InventoryItem(obj, slot) - if { - val id = obj.Definition.ObjectId - delayedPurchaseEntries.get(id) match { - case Some(delay) => - val lastUse = player.GetLastPurchaseTime(id) - time - lastUse > delay - case None => - true - } - } => - val definition = obj.Definition - val id = definition.ObjectId - player.SetLastPurchaseTime(id, time) - player.ObjectTypeNameReference(id.toLong, definition.Name) - delayedPurchaseEntries.get(id) match { - case Some(delay) => - sendResponse(AvatarVehicleTimerMessage(player.GUID, definition.Name, delay / 1000, true)) - case _ => ; - } - taskResolver ! loadoutEquipmentFunc(obj, slot) - } - } - /** * na * @param tplayer na @@ -2733,35 +2676,388 @@ class WorldSessionActor extends Actor */ def HandleTerminalMessage(tplayer : Player, msg : ItemTransactionMessage, order : Terminal.Exchange) : Unit = { order match { - case Terminal.BuyEquipment(item) => - val definition = item.Definition - val itemid = definition.ObjectId - val time = System.currentTimeMillis - if(delayedPurchaseEntries.get(itemid) match { - case Some(delay) if time - tplayer.GetLastPurchaseTime(itemid) > delay => - player.SetLastPurchaseTime(itemid, time) - player.ObjectTypeNameReference(itemid.toLong, definition.Name) - sendResponse(AvatarVehicleTimerMessage(tplayer.GUID, definition.Name, delay / 1000, true)) - true - case Some(_) => - false - case _ => ; - true - }) { - taskResolver ! BuyNewEquipmentPutInInventory( - continent.GUID(tplayer.VehicleSeated) match { case Some(v : Vehicle) => v; case _ => player }, - taskResolver, - tplayer, - msg.terminal_guid - )(item) + case Terminal.BuyExosuit(exosuit, subtype) => + //TODO check exo-suit permissions + val originalSuit = tplayer.ExoSuit + val originalSubtype = Loadout.DetermineSubtype(tplayer) + val lTime = System.currentTimeMillis + var changeArmor : Boolean = true + if(lTime - whenUsedLastMAX(subtype) < 300000) { + changeArmor = false + } + if(changeArmor && exosuit.id == 2) { + for(i <- 1 to 3) { + sendResponse(AvatarVehicleTimerMessage(tplayer.GUID, whenUsedLastMAXName(i), 300, true)) + whenUsedLastMAX(i) = lTime + } + } + if(originalSuit != exosuit || originalSubtype != subtype && changeArmor) { + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + //prepare lists of valid objects + val beforeInventory = tplayer.Inventory.Clear() + val beforeHolsters = clearHolsters(tplayer.Holsters().iterator) + //change suit (clear inventory and change holster sizes; holsters must be empty before this point) + val originalArmor = tplayer.Armor + tplayer.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit + val toMaxArmor = tplayer.MaxArmor + if(originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) { + tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), exosuit)) + tplayer.Armor = toMaxArmor + sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 4, toMaxArmor)) + continent.AvatarEvents ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, toMaxArmor)) + } + else { + tplayer.Armor = originalArmor + } + //ensure arm is down, even if it needs to go back up + if(tplayer.DrawnSlot != Player.HandsDownSlot) { + tplayer.DrawnSlot = Player.HandsDownSlot + sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true)) + continent.AvatarEvents ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectHeld(tplayer.GUID, tplayer.LastDrawnSlot)) + } + //delete everything not dropped + (beforeHolsters ++ beforeInventory).foreach({ elem => + sendResponse(ObjectDeleteMessage(elem.obj.GUID, 0)) + }) + beforeHolsters.foreach({ elem => + continent.AvatarEvents ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID)) + }) + //report change + sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, subtype)) + continent.AvatarEvents ! AvatarServiceMessage(player.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype)) + //sterilize holsters + val normalHolsters = if(originalSuit == ExoSuitType.MAX) { + val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max) + maxWeapons.foreach(entry => { + taskResolver ! GUIDTask.UnregisterEquipment(entry.obj)(continent.GUID) + }) + normalWeapons + } + else { + beforeHolsters + } + //populate holsters + val finalInventory = if(exosuit == ExoSuitType.MAX) { + taskResolver ! DelayedObjectHeld(tplayer, 0, List(PutEquipmentInSlot(tplayer, Tool(GlobalDefinitions.MAXArms(subtype, tplayer.Faction)), 0))) + fillEmptyHolsters(List(tplayer.Slot(4)).iterator, normalHolsters) ++ beforeInventory + } + else if(originalSuit == exosuit) { //note - this will rarely be the situation + fillEmptyHolsters(tplayer.Holsters().iterator, normalHolsters) + } + else { + val (afterHolsters, toInventory) = normalHolsters.partition(elem => elem.obj.Size == tplayer.Slot(elem.start).Size) + afterHolsters.foreach({ elem => tplayer.Slot(elem.start).Equipment = elem.obj }) + fillEmptyHolsters(tplayer.Holsters().iterator, toInventory ++ beforeInventory) + } + //draw holsters + tplayer.VisibleSlots.foreach({ index => + tplayer.Slot(index).Equipment match { + case Some(obj) => + val definition = obj.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + obj.GUID, + ObjectCreateMessageParent(tplayer.GUID, index), + definition.Packet.DetailedConstructorData(obj).get + ) + ) + continent.AvatarEvents ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, player.GUID, index, obj)) + case None => ; + } + }) + //re-draw equipment held in free hand + tplayer.FreeHand.Equipment match { + case Some(item) => + val definition = item.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + item.GUID, + ObjectCreateMessageParent(tplayer.GUID, Player.FreeHandSlot), + definition.Packet.DetailedConstructorData(item).get + ) + ) + case None => ; + } + //put items back into inventory + val (stow, drop) = if(originalSuit == exosuit) { + (finalInventory, Nil) + } + else { + GridInventory.recoverInventory(finalInventory, tplayer.Inventory) + } + stow.foreach(elem => { + tplayer.Inventory.Insert(elem.start, elem.obj) + val obj = elem.obj + val definition = obj.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + obj.GUID, + ObjectCreateMessageParent(tplayer.GUID, elem.start), + definition.Packet.DetailedConstructorData(obj).get + ) + ) + }) + val (finalDroppedItems, retiredItems) = drop.map(item => InventoryItem(item, -1)).partition(DropPredicate(tplayer)) + //drop special items on ground + val pos = tplayer.Position + val orient = Vector3.z(tplayer.Orientation.z) + finalDroppedItems.foreach(entry => { + //TODO make a sound when dropping stuff + continent.Ground ! Zone.Ground.DropItem(entry.obj, pos, orient) + }) + //deconstruct normal items + retiredItems.foreach({ entry => + taskResolver ! GUIDTask.UnregisterEquipment(entry.obj)(continent.GUID) + }) } else { - lastTerminalOrderFulfillment = true sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false)) } + lastTerminalOrderFulfillment = true + + case Terminal.BuyEquipment(item) => + continent.GUID(tplayer.VehicleSeated) match { + //vehicle trunk + case Some(vehicle : Vehicle) => + vehicle.Fit(item) match { + case Some(index) => + item.Faction = tplayer.Faction + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + taskResolver ! StowNewEquipmentInVehicle(vehicle)(index, item) + case None => //player free hand? + tplayer.FreeHand.Equipment match { + case None => + item.Faction = tplayer.Faction + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + taskResolver ! PutEquipmentInSlot(tplayer, item, Player.FreeHandSlot) + case Some(_) => + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false)) + } + } + //player backpack or free hand + case _ => + tplayer.Fit(item) match { + case Some(index) => + item.Faction = tplayer.Faction + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + taskResolver ! PutEquipmentInSlot(tplayer, item, index) + case None => + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false)) + } + } + lastTerminalOrderFulfillment = true case Terminal.SellEquipment() => - SellEquipmentFromInventory(tplayer, taskResolver, tplayer, msg.terminal_guid)(Player.FreeHandSlot) + tplayer.FreeHand.Equipment match { + case Some(item) => + if(item.GUID == msg.item_guid) { + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true)) + taskResolver ! RemoveEquipmentFromSlot(tplayer, item, Player.FreeHandSlot) + } + case None => + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, false)) + } + lastTerminalOrderFulfillment = true + + case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) => + log.info(s"$tplayer wants to change equipment loadout to their option #${msg.unk1 + 1}") + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, true)) + //sanitize exo-suit for change + val originalSuit = player.ExoSuit + val originalSubtype = Loadout.DetermineSubtype(tplayer) + //prepare lists of valid objects + val beforeFreeHand = tplayer.FreeHand.Equipment + val dropPred = DropPredicate(tplayer) + val (dropHolsters, beforeHolsters) = clearHolsters(tplayer.Holsters().iterator).partition(dropPred) + val (dropInventory, beforeInventory) = tplayer.Inventory.Clear().partition(dropPred) + tplayer.FreeHand.Equipment = None //terminal and inventory will close, so prematurely dropping should be fine + val fallbackSuit = ExoSuitType.Standard + val fallbackSubtype = 0 + //a loadout with a prohibited exo-suit type will result in a fallback exo-suit type + val (nextSuit : ExoSuitType.Value, nextSubtype : Int) = + if(ExoSuitDefinition.Select(exosuit, player.Faction).Permissions match { + case Nil => + true + case permissions if subtype != 0 => + val certs = tplayer.Certifications + certs.intersect(permissions.toSet).nonEmpty && + certs.intersect(InfantryLoadout.DetermineSubtypeC(subtype)).nonEmpty + case permissions => + tplayer.Certifications.intersect(permissions.toSet).nonEmpty + }) { + val lTime = System.currentTimeMillis + if(lTime - whenUsedLastMAX(subtype) < 300000) { // PTS v3 hack + (originalSuit, subtype) + } + else { + if(lTime - whenUsedLastMAX(subtype) > 300000 && subtype != 0) { + for(i <- 1 to 3) { + sendResponse(AvatarVehicleTimerMessage(tplayer.GUID, whenUsedLastMAXName(i), 300, true)) + whenUsedLastMAX(i) = lTime + } + } + (exosuit, subtype) + } + } + else { + log.warn(s"$tplayer no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead") + (fallbackSuit, fallbackSubtype) + } + //update suit interally (holsters must be empty before this point) + val originalArmor = player.Armor + tplayer.ExoSuit = nextSuit + val toMaxArmor = tplayer.MaxArmor + if(originalSuit != nextSuit || originalSubtype != nextSubtype || originalArmor > toMaxArmor) { + tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), nextSuit)) + tplayer.Armor = toMaxArmor + sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 4, toMaxArmor)) + continent.AvatarEvents ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, toMaxArmor)) + } + else { + tplayer.Armor = originalArmor + } + //ensure arm is down, even if it needs to go back up + if(tplayer.DrawnSlot != Player.HandsDownSlot) { + tplayer.DrawnSlot = Player.HandsDownSlot + sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true)) + continent.AvatarEvents ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectHeld(tplayer.GUID, tplayer.LastDrawnSlot)) + } + //a change due to exo-suit permissions mismatch will result in (more) items being re-arranged and/or dropped + //dropped items can be forgotten safely + val (afterHolsters, afterInventory) = if(nextSuit == exosuit) { + ( + holsters.filterNot(dropPred), + inventory.filterNot(dropPred) + ) + } + else { + val newSuitDef = ExoSuitDefinition.Select(nextSuit, player.Faction) + val (afterInventory, extra) = GridInventory.recoverInventory( + inventory.filterNot(dropPred), + tplayer.Inventory + ) + val afterHolsters = { + val preservedHolsters = if(exosuit == ExoSuitType.MAX) { + holsters.filter(_.start == 4) //melee slot perservation + } + else { + holsters + .filterNot(dropPred) + .collect { + case item@InventoryItem(obj, index) if newSuitDef.Holster(index) == obj.Size => item + } + } + val size = newSuitDef.Holsters.size + val indexMap = preservedHolsters.map { entry => entry.start } + preservedHolsters ++ (extra.map { obj => + tplayer.Fit(obj) match { + case Some(index : Int) if index < size && !indexMap.contains(index) => + InventoryItem(obj, index) + case _ => + InventoryItem(obj, -1) + } + }).filterNot(entry => entry.start == -1) + } + (afterHolsters, afterInventory) + } + //delete everything (not dropped) + beforeHolsters.foreach({ elem => + continent.AvatarEvents ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID)) + }) + (beforeHolsters ++ beforeInventory).foreach({ elem => + sendResponse(ObjectDeleteMessage(elem.obj.GUID, 0)) + taskResolver ! GUIDTask.UnregisterEquipment(elem.obj)(continent.GUID) + }) + //report change + sendResponse(ArmorChangedMessage(tplayer.GUID, nextSuit, nextSubtype)) + continent.AvatarEvents ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ArmorChanged(tplayer.GUID, nextSuit, nextSubtype)) + if(nextSuit == ExoSuitType.MAX) { + val (maxWeapons, otherWeapons) = afterHolsters.partition(entry => { + entry.obj.Size == EquipmentSize.Max + }) + val weapon = maxWeapons.headOption match { + case Some(mweapon) => + mweapon.obj + case None => + Tool(GlobalDefinitions.MAXArms(nextSubtype, tplayer.Faction)) + } + taskResolver ! DelayedObjectHeld(tplayer, 0, List(PutEquipmentInSlot(tplayer, weapon, 0))) + otherWeapons + } + else { + afterHolsters + }.foreach(entry => { + entry.obj.Faction = tplayer.Faction + taskResolver ! PutEquipmentInSlot(tplayer, entry.obj, entry.start) + }) + //put items into inventory + afterInventory.foreach(entry => { + entry.obj.Faction = tplayer.Faction + taskResolver ! PutEquipmentInSlot(tplayer, entry.obj, entry.start) + }) + //drop stuff on ground + val pos = tplayer.Position + val orient = Vector3.z(tplayer.Orientation.z) + ((beforeFreeHand match { + case Some(item) => List(InventoryItem(item, -1)) //add the item previously in free hand, if any + case None => Nil + }) ++ dropHolsters ++ dropInventory).foreach(entry => { + entry.obj.Faction = PlanetSideEmpire.NEUTRAL + continent.Ground ! Zone.Ground.DropItem(entry.obj, pos, orient) + }) + lastTerminalOrderFulfillment = 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 + continent.VehicleEvents ! 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 + val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) //dropped items can be forgotten + stow + } + }).foreach({ case InventoryItem(obj, index) => + obj.Faction = tplayer.Faction + 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)) + } + lastTerminalOrderFulfillment = true case Terminal.LearnCertification(cert) => val name = tplayer.Name @@ -2770,7 +3066,7 @@ class WorldSessionActor extends Actor log.info(s"$name is learning the $cert certification for ${Certification.Cost.Of(cert)} points") avatar.Certifications += cert StartBundlingPackets() - UpdateDeployableUIElements(Deployables.AddToDeployableQuantities(avatar, cert, player.Certifications)) + AddToDeployableQuantities(cert, player.Certifications) sendResponse(PlanetsideAttributeMessage(guid, 24, cert.id)) tplayer.Certifications.intersect(Certification.Dependencies.Like(cert)).foreach(entry => { log.info(s"$cert replaces the learned certification $entry that cost ${Certification.Cost.Of(entry)} points") @@ -2793,12 +3089,12 @@ class WorldSessionActor extends Actor log.info(s"$name is forgetting the $cert certification for ${Certification.Cost.Of(cert)} points") avatar.Certifications -= cert StartBundlingPackets() - UpdateDeployableUIElements(Deployables.RemoveFromDeployableQuantities(avatar, cert, player.Certifications)) + RemoveFromDeployablesQuantities(cert, player.Certifications) sendResponse(PlanetsideAttributeMessage(guid, 25, cert.id)) tplayer.Certifications.intersect(Certification.Dependencies.FromAll(cert)).foreach(entry => { log.info(s"$name is also forgetting the ${Certification.Cost.Of(entry)}-point $entry certification which depends on $cert") avatar.Certifications -= entry - UpdateDeployableUIElements(Deployables.RemoveFromDeployableQuantities(avatar, entry, player.Certifications)) + RemoveFromDeployablesQuantities(entry, player.Certifications) sendResponse(PlanetsideAttributeMessage(guid, 25, entry.id)) }) StopBundlingPackets() @@ -2893,20 +3189,11 @@ class WorldSessionActor extends Actor case Terminal.BuyVehicle(vehicle, weapons, trunk) => continent.Map.TerminalToSpawnPad.get(msg.terminal_guid.guid) match { case Some(pad_guid) => - val definition = vehicle.Definition - val vid = definition.ObjectId - val time = System.currentTimeMillis - if(delayedPurchaseEntries.get(vid) match { - case Some(delay) if time - tplayer.GetLastPurchaseTime(vid) > delay => - tplayer.SetLastPurchaseTime(vid, time) - tplayer.ObjectTypeNameReference(vid.toLong, definition.Name) - sendResponse(AvatarVehicleTimerMessage(tplayer.GUID, definition.Name, delay / 1000, true)) - true - case Some(_) => - false - case None => ; - true - }) { + val lTime = System.currentTimeMillis + if(lTime - whenUsedLastItem(vehicle.Definition.ObjectId) > 300000) { + whenUsedLastItem(vehicle.Definition.ObjectId) = lTime + whenUsedLastItemName(vehicle.Definition.ObjectId) = msg.item_name + sendResponse(AvatarVehicleTimerMessage(tplayer.GUID, msg.item_name, 300, true)) val toFaction = tplayer.Faction val pad = continent.GUID(pad_guid).get.asInstanceOf[VehicleSpawnPad] vehicle.Faction = toFaction @@ -2930,7 +3217,7 @@ class WorldSessionActor extends Actor vTrunk.Clear() trunk.foreach(entry => { entry.obj.Faction = toFaction - vTrunk.InsertQuickly(entry.start, entry.obj) + vTrunk += entry.start -> entry.obj }) taskResolver ! RegisterVehicleFromSpawnPad(vehicle, pad) sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) @@ -2944,7 +3231,7 @@ class WorldSessionActor extends Actor } lastTerminalOrderFulfillment = true - case Terminal.NoDeal() => + case _ => val order : String = if(msg == null) { s"order $msg" } @@ -2954,8 +3241,6 @@ class WorldSessionActor extends Actor log.warn(s"${tplayer.Name} made a request but the terminal rejected the $order") sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, false)) lastTerminalOrderFulfillment = true - - case _ => ; } } @@ -3167,36 +3452,6 @@ class WorldSessionActor extends Actor }) sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, true, "", msg, None)) - case VehicleResponse.ChangeLoadout(target, old_weapons, added_weapons, old_inventory, new_inventory) => - //TODO when vehicle weapons can be changed without visual glitches, rewrite this - continent.GUID(target) match { - case Some(vehicle : Vehicle) => - StartBundlingPackets() - if(player.VehicleOwned.contains(target)) { - //owner: must unregister old equipment, and register and install new equipment - (old_weapons ++ old_inventory).foreach { case (obj, guid) => - sendResponse(ObjectDeleteMessage(guid, 0)) - taskResolver ! GUIDTask.UnregisterEquipment(obj)(continent.GUID) - } - ApplyPurchaseTimersBeforePackingLoadout(player, vehicle, added_weapons ++ new_inventory) - } - else if(accessedContainer.contains(target)) { - //external participant: observe changes to equipment - (old_weapons ++ old_inventory).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 0)) } - } - vehicle.PassengerInSeat(player) match { - case Some(seatNum) => - //participant: observe changes to equipment - (old_weapons ++ old_inventory).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 0)) } - UpdateWeaponAtSeatPosition(vehicle, seatNum) - case None => - //observer: observe changes to external equipment - old_weapons.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 0)) } - } - StopBundlingPackets() - case _ => ; - } - case _ => ; } } @@ -3385,7 +3640,7 @@ class WorldSessionActor extends Actor player = tplayer val guid = tplayer.GUID StartBundlingPackets() - UpdateDeployableUIElements(Deployables.InitializeDeployableUIElements(avatar)) + InitializeDeployableUIElements(avatar) sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 75, 0)) sendResponse(SetCurrentAvatarMessage(guid, 0, 0)) sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on //TODO once per respawn? @@ -3770,10 +4025,9 @@ class WorldSessionActor extends Actor log.info(s"CharacterRequest/Select: character $lName found in records") avatar = new Avatar(charId, lName, lFaction, lGender, lHead, lVoice) var faction : String = lFaction.toString.toLowerCase - whenUsedLastMAXName(0) = faction + "hev" - whenUsedLastMAXName(1) = faction + "hev_antipersonnel" - whenUsedLastMAXName(2) = faction + "hev_antivehicular" - whenUsedLastMAXName(3) = faction + "hev_antiaircraft" + whenUsedLastMAXName(2) = faction + "hev_antipersonnel" + whenUsedLastMAXName(3) = faction + "hev_antivehicular" + whenUsedLastMAXName(1) = faction + "hev_antiaircraft" avatar.FirstTimeEvents = ftes accountPersistence ! AccountPersistenceService.Login(lName) case _ => @@ -4261,13 +4515,13 @@ class WorldSessionActor extends Actor log.info(s"ReleaseAvatarRequest: ${player.GUID} on ${continent.Id} has released") reviveTimer.cancel GoToDeploymentMap() + continent.Population ! Zone.Population.Release(avatar) player.VehicleSeated match { case None => PrepareToTurnPlayerIntoCorpse(player, continent) case Some(_) => val player_guid = player.GUID - continent.Population ! Zone.Population.Release(avatar) sendResponse(ObjectDeleteMessage(player_guid, 0)) GetMountableAndSeat(None, player) match { case (Some(obj), Some(seatNum)) => @@ -4940,13 +5194,7 @@ class WorldSessionActor extends Actor player.FreeHand.Equipment match { case Some(item) => if(item.GUID == item_guid) { - CancelZoningProcessWithDescriptiveReason("cancel_use") - continent.GUID(player.VehicleSeated) match { - case Some(_) => - RemoveOldEquipmentFromInventory(player, taskResolver)(item) - case None => - DropEquipmentFromInventory(player)(item) - } + continent.Ground ! Zone.Ground.DropItem(item, player.Position, player.Orientation) } case None => log.warn(s"DropItem: $player wanted to drop a $anItem, but it wasn't at hand") @@ -4964,8 +5212,7 @@ class WorldSessionActor extends Actor case Some(item : Equipment) => player.Fit(item) match { case Some(_) => - CancelZoningProcessWithDescriptiveReason("cancel_use") - PickUpEquipmentFromGround(player)(item) + continent.Ground ! Zone.Ground.PickupItem(item_guid) case None => //skip sendResponse(ActionResultMessage.Fail(16)) //error code? } @@ -4986,25 +5233,25 @@ class WorldSessionActor extends Actor case Nil => log.warn(s"ReloadMessage: no ammunition could be found for $item_guid") case x :: xs => - val (deleteFunc, modifyFunc) : (Equipment=>Future[Any], (AmmoBox, Int) => Unit) = obj match { + val (deleteFunc, modifyFunc) : ((Int, AmmoBox) => Unit, (AmmoBox, Int) => Unit) = obj match { case (veh : Vehicle) => - (RemoveOldEquipmentFromInventory(veh, taskResolver), ModifyAmmunitionInVehicle(veh)) - case o : PlanetSideServerObject with Container => - (RemoveOldEquipmentFromInventory(o, taskResolver), ModifyAmmunition(o)) + (DeleteEquipmentFromVehicle(veh), ModifyAmmunitionInVehicle(veh)) case _ => - throw new Exception("ReloadMessage: should be a server object, not a regular game object") + (DeleteEquipment(obj), ModifyAmmunition(obj)) } - xs.foreach { item => deleteFunc(item.obj) } + xs.foreach(item => { + deleteFunc(item.start, item.obj.asInstanceOf[AmmoBox]) + }) val box = x.obj.asInstanceOf[AmmoBox] val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { - xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum + xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) } val sumReloadValue : Int = box.Capacity + tailReloadValue val actualReloadValue = (if(sumReloadValue <= reloadValue) { - deleteFunc(box) + deleteFunc(x.start, box) sumReloadValue } else { @@ -5196,9 +5443,50 @@ class WorldSessionActor extends Actor case msg@MoveItemMessage(item_guid, source_guid, destination_guid, dest, _) => log.info(s"MoveItem: $msg") - (continent.GUID(source_guid), continent.GUID(destination_guid), ValidObject(item_guid)) match { - case (Some(source : PlanetSideServerObject with Container), Some(destination : PlanetSideServerObject with Container), Some(item : Equipment)) => - source.Actor ! Containable.MoveItem(destination, item, dest) + (continent.GUID(source_guid), continent.GUID(destination_guid), continent.GUID(item_guid)) match { + case (Some(source : Container), Some(destination : Container), Some(item : Equipment)) => + source.Find(item_guid) match { + case Some(index) => + val indexSlot = source.Slot(index) + val tile = item.Definition.Tile + val destinationCollisionTest = destination.Collisions(dest, tile.Width, tile.Height) + val destItemEntry = destinationCollisionTest match { + case Success(entry :: Nil) => + Some(entry) + case _ => + None + } + if( { + destinationCollisionTest match { + case Success(Nil) | Success(_ :: Nil) => + true //no item or one item to swap + case _ => + false //abort when too many items at destination or other failure case + } + } && indexSlot.Equipment.contains(item)) { + if(PermitEquipmentStow(item, destination)) { + StartBundlingPackets() + PerformMoveItem(item, source, index, destination, dest, destItemEntry) + StopBundlingPackets() + } + else { + log.error(s"MoveItem: $item disallowed storage in $destination") + } + } + else if(!indexSlot.Equipment.contains(item)) { + log.error(s"MoveItem: wanted to move $item_guid, but found unexpected ${indexSlot.Equipment} at source location") + } + else { + destinationCollisionTest match { + case Success(_) => + log.error(s"MoveItem: wanted to move $item_guid, but multiple unexpected items at destination blocked progress") + case scala.util.Failure(err) => + log.error(s"MoveItem: wanted to move $item_guid, but $err") + } + } + case _ => + log.error(s"MoveItem: wanted to move $item_guid, but could not find it") + } case (None, _, _) => log.error(s"MoveItem: wanted to move $item_guid from $source_guid, but could not find source object") case (_, None, _) => @@ -5211,27 +5499,34 @@ class WorldSessionActor extends Actor case msg@LootItemMessage(item_guid, target_guid) => log.info(s"LootItem: $msg") - (ValidObject(item_guid), continent.GUID(target_guid)) match { - case (Some(item : Equipment), Some(destination : PlanetSideServerObject with Container)) => + (ValidObject(item_guid), ValidObject(target_guid)) match { + case (Some(item : Equipment), Some(target : Container)) => //figure out the source ( { - val findFunc : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] = FindInLocalContainer(item_guid) + val findFunc : PlanetSideGameObject with Container => Option[(PlanetSideGameObject with Container, Option[Int])] = FindInLocalContainer(item_guid) findFunc(player.Locker) .orElse(findFunc(player)) .orElse(accessedContainer match { - case Some(parent : PlanetSideServerObject) => + case Some(parent) => findFunc(parent) - case _ => + case None => None } ) - }, destination.Fit(item)) match { - case (Some((source, Some(_))), Some(dest)) => - source.Actor ! Containable.MoveItem(destination, item, dest) + }, target.Fit(item)) match { + case (Some((source, Some(index))), Some(dest)) => + if(PermitEquipmentStow(item, target)) { + StartBundlingPackets() + PerformMoveItem(item, source, index, target, dest, None) + StopBundlingPackets() + } + else { + log.error(s"LootItem: $item disallowed storage in $target") + } case (None, _) => log.error(s"LootItem: can not find where $item is put currently") case (_, None) => - log.error(s"LootItem: can not find somwhere to put $item in $destination") + log.error(s"LootItem: can not find somwhere to put $item in $target") case _ => log.error(s"LootItem: wanted to move $item_guid to $target_guid, but multiple problems were encountered") } @@ -5306,86 +5601,88 @@ class WorldSessionActor extends Actor else if(!unk3 && player.isAlive) { //potential kit use ValidObject(item_used_guid) match { case Some(kit : Kit) => - val kid = kit.Definition.ObjectId - val time = System.currentTimeMillis - val lastUse = player.GetLastUsedTime(kid) - val delay = delayedGratificationEntries.getOrElse(kid, 0L) - if((time - lastUse) < delay) { - sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^${((delay / 1000) - math.ceil((time - lastUse) / 1000))}~", None)) - } - else { - val indexOpt = player.Find(kit) - val kitIsUsed = indexOpt match { - case Some(index) => - if(kit.Definition == GlobalDefinitions.medkit) { - if(player.Health == player.MaxHealth) { - sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "@HealComplete", None)) - false - } - else { - player.History(HealFromKit(PlayerSource(player), 25, kit.Definition)) - player.Health = player.Health + 25 - sendResponse(PlanetsideAttributeMessage(avatar_guid, 0, player.Health)) - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 0, player.Health)) - true - } + player.Find(kit) match { + case Some(index) => + if(kit.Definition == GlobalDefinitions.medkit) { + if(player.Health == player.MaxHealth) { + sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "@HealComplete", None)) } - else if(kit.Definition == GlobalDefinitions.super_medkit) { - if(player.Health == player.MaxHealth) { - sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "@HealComplete", None)) - false - } - else { - player.History(HealFromKit(PlayerSource(player), 100, kit.Definition)) - player.Health = player.Health + 100 - sendResponse(PlanetsideAttributeMessage(avatar_guid, 0, player.Health)) - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 0, player.Health)) - true - } - } - else if(kit.Definition == GlobalDefinitions.super_armorkit) { - if(player.Armor == player.MaxArmor) { - sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "Armor at maximum - No repairing required.", None)) - false - } - else { - player.History(RepairFromKit(PlayerSource(player), 200, kit.Definition)) - player.Armor = player.Armor + 200 - sendResponse(PlanetsideAttributeMessage(avatar_guid, 4, player.Armor)) - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 4, player.Armor)) - true - } - } - else if(kit.Definition == GlobalDefinitions.super_staminakit) { - if(player.Stamina == player.MaxStamina) { - sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "Stamina at maximum - No recharge required.", None)) - false - } - else { - player.Stamina = player.Stamina + 100 - sendResponse(PlanetsideAttributeMessage(avatar_guid, 2, player.Stamina)) - true - } + else if(System.currentTimeMillis - whenUsedLastKit < 5000) { + sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^${5 - (System.currentTimeMillis - whenUsedLastKit) / 1000}~", None)) } else { - log.warn(s"UseItem: $kit behavior not supported") - false + whenUsedLastKit = System.currentTimeMillis + player.Slot(index).Equipment = None //remove from slot immediately; must exist on client for next packet + sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + sendResponse(ObjectDeleteMessage(kit.GUID, 0)) + taskResolver ! GUIDTask.UnregisterEquipment(kit)(continent.GUID) + player.History(HealFromKit(PlayerSource(player), 25, kit.Definition)) + player.Health = player.Health + 25 + sendResponse(PlanetsideAttributeMessage(avatar_guid, 0, player.Health)) + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 0, player.Health)) } + } + else if(kit.Definition == GlobalDefinitions.super_medkit) { + if(player.Health == player.MaxHealth) { + sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "@HealComplete", None)) + } + else if(System.currentTimeMillis - whenUsedLastSMKit < 1200000) { + sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^${1200 - (System.currentTimeMillis - whenUsedLastSMKit) / 1000}~", None)) + } + else { + whenUsedLastSMKit = System.currentTimeMillis + player.Slot(index).Equipment = None //remove from slot immediately; must exist on client for next packet + sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + sendResponse(ObjectDeleteMessage(kit.GUID, 0)) + taskResolver ! GUIDTask.UnregisterEquipment(kit)(continent.GUID) + player.History(HealFromKit(PlayerSource(player), 100, kit.Definition)) + player.Health = player.Health + 100 + sendResponse(PlanetsideAttributeMessage(avatar_guid, 0, player.Health)) + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 0, player.Health)) + } + } + else if(kit.Definition == GlobalDefinitions.super_armorkit) { + if(player.Armor == player.MaxArmor) { + sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "Armor at maximum - No repairing required.", None)) + } + else if(System.currentTimeMillis - whenUsedLastSAKit < 1200000) { + sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^${1200 - (System.currentTimeMillis - whenUsedLastSAKit) / 1000}~", None)) + } + else { + whenUsedLastSAKit = System.currentTimeMillis + player.Slot(index).Equipment = None //remove from slot immediately; must exist on client for next packet + sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + sendResponse(ObjectDeleteMessage(kit.GUID, 0)) + taskResolver ! GUIDTask.UnregisterEquipment(kit)(continent.GUID) + player.History(RepairFromKit(PlayerSource(player), 200, kit.Definition)) + player.Armor = player.Armor + 200 + sendResponse(PlanetsideAttributeMessage(avatar_guid, 4, player.Armor)) + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 4, player.Armor)) + } + } + else if(kit.Definition == GlobalDefinitions.super_staminakit) { + if(player.Stamina == player.MaxStamina) { + sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "Stamina at maximum - No recharge required.", None)) + } + else if(System.currentTimeMillis - whenUsedLastSSKit < 1200000) { + sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^${300 - (System.currentTimeMillis - whenUsedLastSSKit) / 1200}~", None)) + } + else { + whenUsedLastSSKit = System.currentTimeMillis + player.Slot(index).Equipment = None //remove from slot immediately; must exist on client for next packet + sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + sendResponse(ObjectDeleteMessage(kit.GUID, 0)) + taskResolver ! GUIDTask.UnregisterEquipment(kit)(continent.GUID) + player.Stamina = player.Stamina + 100 + } + } + else { + log.warn(s"UseItem: $kit behavior not supported") + } - case None => - log.error(s"UseItem: anticipated a $kit, but can't find it") - false - } - if(kitIsUsed) { - //kit was found belonging to player and was used - player.SetLastUsedTime(kid, time) - player.Slot(indexOpt.get).Equipment = None //remove from slot immediately; must exist on client for next packet - sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) - sendResponse(ObjectDeleteMessage(kit.GUID, 0)) - taskResolver ! GUIDTask.UnregisterEquipment(kit)(continent.GUID) - } + case None => + log.error(s"UseItem: anticipated a $kit, but can't find it") } - case Some(item) => log.warn(s"UseItem: looking for Kit to use, but found $item instead") case None => @@ -5519,6 +5816,7 @@ class WorldSessionActor extends Actor CancelZoningProcessWithDescriptiveReason("cancel_use") PlayerActionsToCancel() CancelAllProximityUnits() + continent.Population ! Zone.Population.Release(avatar) GoToDeploymentMap() case _ => ; } @@ -6258,6 +6556,151 @@ class WorldSessionActor extends Actor case default => log.error(s"Unhandled GamePacket $pkt") } + /** + * Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item. + * Remove any encountered items and add them to an output `List`. + * @param iter the `Iterator` of `EquipmentSlot`s + * @param index a number that equals the "current" holster slot (`EquipmentSlot`) + * @param list a persistent `List` of `Equipment` in the holster slots + * @return a `List` of `Equipment` in the holster slots + */ + @tailrec private def clearHolsters(iter : Iterator[EquipmentSlot], index : Int = 0, list : List[InventoryItem] = Nil) : List[InventoryItem] = { + if(!iter.hasNext) { + list + } + else { + val slot = iter.next + slot.Equipment match { + case Some(equipment) => + slot.Equipment = None + clearHolsters(iter, index + 1, InventoryItem(equipment, index) +: list) + case None => + clearHolsters(iter, index + 1, list) + } + } + } + + /** + * Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item. + * For any slots that are not yet occupied by an item, search through the `List` and find an item that fits in that slot. + * Add that item to the slot and remove it from the list. + * @param iter the `Iterator` of `EquipmentSlot`s + * @param list a `List` of all `Equipment` that is not yet assigned to a holster slot or an inventory slot + * @return the `List` of all `Equipment` not yet assigned to a holster slot or an inventory slot + */ + @tailrec private def fillEmptyHolsters(iter : Iterator[EquipmentSlot], list : List[InventoryItem]) : List[InventoryItem] = { + if(!iter.hasNext) { + list + } + else { + val slot = iter.next + if(slot.Equipment.isEmpty) { + list.find(item => item.obj.Size == slot.Size) match { + case Some(obj) => + val index = list.indexOf(obj) + slot.Equipment = obj.obj + fillEmptyHolsters(iter, list.take(index) ++ list.drop(index + 1)) + case None => + fillEmptyHolsters(iter, list) + } + } + else { + fillEmptyHolsters(iter, list) + } + } + } + + /** + * Construct tasking that coordinates the following:
+ * 1) Accept a new piece of `Equipment` and register it with a globally unique identifier.
+ * 2) Once it is registered, give the `Equipment` to `target`. + * @param target what object will accept the new `Equipment` + * @param obj the new `Equipment` + * @param index the slot where the new `Equipment` will be placed + * @see `GUIDTask.RegisterEquipment` + * @see `PutInSlot` + * @return a `TaskResolver.GiveTask` message + */ + private def PutEquipmentInSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = { + val regTask = GUIDTask.RegisterEquipment(obj)(continent.GUID) + obj match { + case tool : Tool => + val linearToolTask = TaskResolver.GiveTask(regTask.task) +: regTask.subs + TaskResolver.GiveTask(PutInSlot(target, tool, index).task, linearToolTask) + case _ => + TaskResolver.GiveTask(PutInSlot(target, obj, index).task, List(regTask)) + } + } + + /** + * Construct tasking that coordinates the following:
+ * 1) Remove a new piece of `Equipment` from where it is currently stored.
+ * 2) Once it is removed, un-register the `Equipment`'s globally unique identifier. + * @param target the object that currently possesses the `Equipment` + * @param obj the `Equipment` + * @param index the slot from where the `Equipment` will be removed + * @see `GUIDTask.UnregisterEquipment` + * @see `RemoveFromSlot` + * @return a `TaskResolver.GiveTask` message + */ + private def RemoveEquipmentFromSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = { + val regTask = GUIDTask.UnregisterEquipment(obj)(continent.GUID) + //to avoid an error from a GUID-less object from being searchable, it is removed from the inventory first + obj match { + case _ : Tool => + TaskResolver.GiveTask(regTask.task, RemoveFromSlot(target, obj, index) +: regTask.subs) + case _ => + TaskResolver.GiveTask(regTask.task, List(RemoveFromSlot(target, obj, index))) + } + } + + /** + * Construct tasking that gives the `Equipment` to `target`. + * @param target what object will accept the new `Equipment` + * @param obj the new `Equipment` + * @param index the slot where the new `Equipment` will be placed + * @return a `TaskResolver.GiveTask` message + */ + private def PutInSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localTarget = target + private val localIndex = index + private val localObject = obj + private val localAnnounce = self + private val localService = continent.AvatarEvents + + override def isComplete : Task.Resolution.Value = { + if(localTarget.Slot(localIndex).Equipment.contains(localObject)) { + Task.Resolution.Success + } + else { + Task.Resolution.Incomplete + } + } + + def Execute(resolver : ActorRef) : Unit = { + localTarget.Slot(localIndex).Equipment = localObject + resolver ! scala.util.Success(this) + } + + override def onSuccess() : Unit = { + val definition = localObject.Definition + localAnnounce ! ResponseToSelf( + ObjectCreateDetailedMessage( + definition.ObjectId, + localObject.GUID, + ObjectCreateMessageParent(localTarget.GUID, localIndex), + definition.Packet.DetailedConstructorData(localObject).get + ) + ) + if(localTarget.VisibleSlots.contains(localIndex)) { + localService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(localTarget.GUID, localTarget.GUID, localIndex, localObject)) + } + } + }) + } + /** * Construct tasking that registers all aspects of a `Player` avatar. * `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled. @@ -6270,8 +6713,6 @@ class WorldSessionActor extends Actor private val localPlayer = tplayer private val localAnnounce = self - override def Description : String = s"register new player avatar ${localPlayer.Name}" - override def isComplete : Task.Resolution.Value = { if(localPlayer.HasGUID) { Task.Resolution.Success @@ -6284,11 +6725,11 @@ class WorldSessionActor extends Actor def Execute(resolver : ActorRef) : Unit = { log.info(s"Player $localPlayer is registered") resolver ! scala.util.Success(this) - localAnnounce ! NewPlayerLoaded(localPlayer) //alerts WorldSessionActor + localAnnounce ! NewPlayerLoaded(localPlayer) //alerts WSA } override def onFailure(ex : Throwable) : Unit = { - localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WorldSessionActor + localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WSA } }, List(GUIDTask.RegisterAvatar(tplayer)(continent.GUID)) ) @@ -6306,8 +6747,6 @@ class WorldSessionActor extends Actor private val localPlayer = tplayer private val localAnnounce = self - override def Description : String = s"register player avatar ${localPlayer.Name}" - override def isComplete : Task.Resolution.Value = { if(localPlayer.HasGUID) { Task.Resolution.Success @@ -6320,11 +6759,11 @@ class WorldSessionActor extends Actor def Execute(resolver : ActorRef) : Unit = { log.info(s"Player $localPlayer is registered") resolver ! scala.util.Success(this) - localAnnounce ! PlayerLoaded(localPlayer) //alerts WorldSessionActor + localAnnounce ! PlayerLoaded(localPlayer) //alerts WSA } override def onFailure(ex : Throwable) : Unit = { - localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WorldSessionActor + localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WSA } }, List(GUIDTask.RegisterPlayer(tplayer)(continent.GUID)) ) @@ -6342,8 +6781,6 @@ class WorldSessionActor extends Actor new Task() { private val localVehicle = vehicle - override def Description : String = s"register a ${localVehicle.Definition.Name}" - override def isComplete : Task.Resolution.Value = { if(localVehicle.HasGUID) { Task.Resolution.Success @@ -6394,8 +6831,6 @@ class WorldSessionActor extends Actor private val localVehicle = vehicle private val localAnnounce = self - override def Description : String = s"register a ${localVehicle.Definition.Name} manned by ${localDriver.Name}" - override def isComplete : Task.Resolution.Value = { if(localVehicle.HasGUID) { Task.Resolution.Success @@ -6436,8 +6871,6 @@ class WorldSessionActor extends Actor private val localVehicleService = continent.VehicleEvents private val localZone = continent - override def Description : String = s"register a ${localVehicle.Definition.Name} for spawn pad" - override def isComplete : Task.Resolution.Value = { if(localVehicle.HasGUID) { Task.Resolution.Success @@ -6461,8 +6894,6 @@ class WorldSessionActor extends Actor private val localDriver = driver private val localAnnounce = self - override def Description : String = s"register a ${localVehicle.Definition.Name} driven by ${localDriver.Name}" - override def isComplete : Task.Resolution.Value = { if(localVehicle.HasGUID && localDriver.HasGUID) { Task.Resolution.Success @@ -6475,12 +6906,12 @@ class WorldSessionActor extends Actor def Execute(resolver : ActorRef) : Unit = { localDriver.VehicleSeated = localVehicle.GUID Vehicles.Own(localVehicle, localDriver) - localAnnounce ! NewPlayerLoaded(localDriver) //alerts WorldSessionActor + localAnnounce ! NewPlayerLoaded(localDriver) //alerts WSA resolver ! scala.util.Success(this) } override def onFailure(ex : Throwable) : Unit = { - localAnnounce ! PlayerFailedToLoad(localDriver) //alerts WorldSessionActor + localAnnounce ! PlayerFailedToLoad(localDriver) //alerts WSA } }, List(GUIDTask.RegisterAvatar(driver)(continent.GUID), GUIDTask.RegisterVehicle(obj)(continent.GUID))) } @@ -6499,8 +6930,6 @@ class WorldSessionActor extends Actor private val globalProjectile = obj private val localAnnounce = self - override def Description : String = s"register a ${globalProjectile.profile.Name}" - override def isComplete : Task.Resolution.Value = { if(globalProjectile.HasGUID) { Task.Resolution.Success @@ -6524,8 +6953,6 @@ class WorldSessionActor extends Actor private val localVehicle = obj private val localDriver = driver - override def Description : String = s"unregister a ${localVehicle.Definition.Name} driven by ${localDriver.Name}" - override def isComplete : Task.Resolution.Value = { if(!localVehicle.HasGUID && !localDriver.HasGUID) { Task.Resolution.Success @@ -6555,8 +6982,6 @@ class WorldSessionActor extends Actor private val localAnnounce = continent.AvatarEvents private val localMsg = AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, obj.GUID, 2)) - override def Description : String = s"unregister a ${globalProjectile.profile.Name}" - override def isComplete : Task.Resolution.Value = { if(!globalProjectile.HasGUID) { Task.Resolution.Success @@ -6574,6 +6999,48 @@ class WorldSessionActor extends Actor ) } + /** + * Construct tasking that removes the `Equipment` to `target`. + * @param target what object that contains the `Equipment` + * @param obj the `Equipment` + * @param index the slot where the `Equipment` is stored + * @return a `TaskResolver.GiveTask` message + */ + private def RemoveFromSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localTarget = target + private val localIndex = index + private val localObject = obj + private val localObjectGUID = obj.GUID + private val localAnnounce = self //self may not be the same when it executes + private val localService = continent.AvatarEvents + private val localContinent = continent.Id + + override def isComplete : Task.Resolution.Value = { + if(localTarget.Slot(localIndex).Equipment.contains(localObject)) { + Task.Resolution.Incomplete + } + else { + Task.Resolution.Success + } + } + + def Execute(resolver : ActorRef) : Unit = { + localTarget.Slot(localIndex).Equipment = None + resolver ! scala.util.Success(this) + } + + override def onSuccess() : Unit = { + localAnnounce ! ResponseToSelf( ObjectDeleteMessage(localObjectGUID, 0)) + if(localTarget.VisibleSlots.contains(localIndex)) { + localService ! AvatarServiceMessage(localContinent, AvatarAction.ObjectDelete(localTarget.GUID, localObjectGUID)) + } + } + } + ) + } + /** * If the projectile object is unregistered, register it. * If the projectile object is already registered, unregister it and then register it again. @@ -6598,6 +7065,45 @@ class WorldSessionActor extends Actor } } + /** + * After some subtasking is completed, draw a particular slot, as if an `ObjectHeldMessage` packet was sent/received.
+ *
+ * The resulting `Task` is most useful for sequencing MAX weaponry when combined with the proper subtasks. + * @param player the player + * @param index the slot to be drawn + * @param priorTasking subtasks that needs to be accomplished first + * @return a `TaskResolver.GiveTask` message + */ + private def DelayedObjectHeld(player : Player, index : Int, priorTasking : List[TaskResolver.GiveTask]) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localPlayer = player + private val localSlot = index + private val localAnnounce = self + private val localService = continent.AvatarEvents + + override def isComplete : Task.Resolution.Value = { + if(localPlayer.DrawnSlot == localSlot) { + Task.Resolution.Success + } + else { + Task.Resolution.Incomplete + } + } + + def Execute(resolver : ActorRef) : Unit = { + localPlayer.DrawnSlot = localSlot + resolver ! scala.util.Success(this) + } + + override def onSuccess() : Unit = { + localAnnounce ! ResponseToSelf( ObjectHeldMessage(localPlayer.GUID, localSlot, true)) + localService ! AvatarServiceMessage(localPlayer.Continent, AvatarAction.ObjectHeld(localPlayer.GUID, localSlot)) + } + }, priorTasking + ) + } + /** * Before calling `Interstellar.GetWorld` to change zones, perform the following task (which can be a nesting of subtasks). * @param priorTask the tasks to perform @@ -6608,13 +7114,10 @@ class WorldSessionActor extends Actor TaskResolver.GiveTask( new Task() { private val localZone = continent - private val localNewZone = zoneId private val localAvatarMsg = Zone.Population.Leave(avatar) private val localService = cluster private val localServiceMsg = InterstellarCluster.GetWorld(zoneId) - override def Description : String = s"additional tasking in zone ${localZone.Id} before switching to zone $localNewZone" - override def isComplete : Task.Resolution.Value = priorTask.task.isComplete def Execute(resolver : ActorRef) : Unit = { @@ -6629,12 +7132,9 @@ class WorldSessionActor extends Actor def CallBackForTask(task : TaskResolver.GiveTask, sendTo : ActorRef, pass : Any) : TaskResolver.GiveTask = { TaskResolver.GiveTask( new Task() { - private val localDesc = task.task.Description private val destination = sendTo private val passMsg = pass - override def Description : String = s"callback for tasking $localDesc" - def Execute(resolver : ActorRef) : Unit = { destination ! passMsg resolver ! scala.util.Success(this) @@ -6868,6 +7368,105 @@ class WorldSessionActor extends Actor */ def FindWeapon : Option[Tool] = FindContainedWeapon._2 + /** + * Within a specified `Container`, find the smallest number of `Equipment` objects of a certain qualifying type + * whose sum count is greater than, or equal to, a `desiredAmount` based on an accumulator method.
+ *
+ * In an occupied `List` of returned `Inventory` entries, all but the last entry is typically considered "emptied." + * For objects with contained quantities, the last entry may require having that quantity be set to a non-zero number. + * @param obj the `Container` to search + * @param filterTest test used to determine inclusivity of `Equipment` collection + * @param desiredAmount how much is requested + * @param counting test used to determine value of found `Equipment`; + * defaults to one per entry + * @return a `List` of all discovered entries totaling approximately the amount requested + */ + def FindEquipmentStock(obj : Container, + filterTest : (Equipment)=>Boolean, + desiredAmount : Int, + counting : (Equipment)=>Int = DefaultCount) : List[InventoryItem] = { + var currentAmount : Int = 0 + obj.Inventory.Items + .filter(item => filterTest(item.obj)) + .toList + .sortBy(_.start) + .takeWhile(entry => { + val previousAmount = currentAmount + currentAmount += counting(entry.obj) + previousAmount < desiredAmount + }) + } + + /** + * The default counting function for an item. + * Counts the number of item(s). + * @param e the `Equipment` object + * @return the quantity; + * always one + */ + def DefaultCount(e : Equipment) : Int = 1 + + /** + * The counting function for an item of `AmmoBox`. + * Counts the `Capacity` of the ammunition. + * @param e the `Equipment` object + * @return the quantity + */ + def CountAmmunition(e : Equipment) : Int = { + e match { + case a : AmmoBox => + a.Capacity + case _ => + 0 + } + } + + /** + * The counting function for an item of `Tool` where the item is also a grenade. + * Counts the number of grenades. + * @see `GlobalDefinitions.isGrenade` + * @param e the `Equipment` object + * @return the quantity + */ + def CountGrenades(e : Equipment) : Int = { + e match { + case t : Tool => + (GlobalDefinitions.isGrenade(t.Definition):Int) * t.Magazine + case _ => + 0 + } + } + + /** + * Flag an `AmmoBox` object that matches for the given ammunition type. + * @param ammo the type of `Ammo` to check + * @param e the `Equipment` object + * @return `true`, if the object is an `AmmoBox` of the correct ammunition type; `false`, otherwise + */ + def FindAmmoBoxThatUses(ammo : Ammo.Value)(e : Equipment) : Boolean = { + e match { + case t : AmmoBox => + t.AmmoType == ammo + case _ => + false + } + } + + /** + * Flag a `Tool` object that matches for loading the given ammunition type. + * @param ammo the type of `Ammo` to check + * @param e the `Equipment` object + * @return `true`, if the object is a `Tool` that loads the correct ammunition type; `false`, otherwise + */ + def FindToolThatUses(ammo : Ammo.Value)(e : Equipment) : Boolean = { + e match { + case t : Tool => + t.Definition.AmmoTypes.map { _.AmmoType }.contains(ammo) + case _ => + false + } + } + /** * Get the current `Vehicle` object that the player is riding/driving. * The vehicle must be found solely through use of `player.VehicleSeated`. @@ -6887,6 +7486,37 @@ class WorldSessionActor extends Actor } } + /** + * 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 item can be found + * @param item an object to unregister; + * not explicitly checked + */ + private def DeleteEquipment(obj : PlanetSideGameObject with Container)(start : Int, item : Equipment) : Unit = { + val item_guid = item.GUID + obj.Slot(start).Equipment = None + //obj.Inventory -= start + taskResolver ! GUIDTask.UnregisterEquipment(item)(continent.GUID) + sendResponse(ObjectDeleteMessage(item_guid, 0)) + } + + /** + * Given a vehicle that contains an item (`Equipment`) in its `Trunk` at a certain location, + * remove it permanently. + * @see `DeleteEquipment` + * @param obj the `Vehicle` + * @param start where the item can be found + * @param item an object to unregister; + * not explicitly checked + */ + private def DeleteEquipmentFromVehicle(obj : Vehicle)(start : Int, item : Equipment) : Unit = { + val item_guid = item.GUID + DeleteEquipment(obj)(start, item) + continent.VehicleEvents ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player.GUID, item_guid)) + } + /** * Given an object that contains a box of amunition in its `Inventory` at a certain location, * change the amount of ammunition within that box. @@ -6918,6 +7548,251 @@ class WorldSessionActor extends Actor } } + /** + * Announce that an already-registered `AmmoBox` object exists in a given position in some `Container` object's inventory. + * @see `StowEquipmentInVehicles` + * @see `ChangeAmmoMessage` + * @param obj the `Container` object + * @param index an index in `obj`'s inventory + * @param item an `AmmoBox` + */ + 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 `StowEquipment` + * @see `ChangeAmmoMessage` + * @param obj the `Vehicle` object + * @param index an index in `obj`'s inventory + * @param item an `AmmoBox` + */ + def StowEquipmentInVehicles(obj : Vehicle)(index : Int, item : AmmoBox) : Unit = { + StowEquipment(obj)(index, item) + continent.VehicleEvents ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player.GUID, obj.GUID, index, item)) + } + + /** + * 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 `StowNewEquipmentInVehicle` + * @see `ChangeAmmoMessage` + * @param obj the `Container` object + * @param index an index in `obj`'s inventory + * @param item the `Equipment` item + * @return a `TaskResolver.GiveTask` chain that executes the action + */ + def StowNewEquipment(obj : PlanetSideGameObject with Container)(index : Int, item : Equipment) : TaskResolver.GiveTask = { + PutEquipmentInSlot(obj, item, index) + } + + /** + * 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 `StowNewEquipment` + * @see `ChangeAmmoMessage` + * @param obj the `Container` object + * @param index an index in `obj`'s inventory + * @param item the `Equipment` item + * @return a `TaskResolver.GiveTask` chain that executes the action + */ + def StowNewEquipmentInVehicle(obj : Vehicle)(index : Int, item : Equipment) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localService = continent.VehicleEvents + private val localPlayer = player + private val localVehicle = obj + private val localIndex = index + private val localItem = item + + override def isComplete : Task.Resolution.Value = Task.Resolution.Success + + def Execute(resolver : ActorRef) : Unit = { + localService ! VehicleServiceMessage( + s"${localVehicle.Actor}", + VehicleAction.StowEquipment(localPlayer.GUID, localVehicle.GUID, localIndex, localItem) + ) + resolver ! scala.util.Success(this) + } + }, + List(StowNewEquipment(obj)(index, item)) + ) + } + + /** + * Given an item, and two places, one where the item currently is and one where the item will be moved, + * perform a controlled transfer of the item. + * If something exists at the `destination` side of the transfer in the position that `item` will occupy, + * resolve its location as well by swapping it with where `item` originally was positioned.
+ *
+ * Parameter checks will not be performed. + * Do perform checks before sending data to this function. + * Do not call with incorrect or unverified data, e.g., `item` not actually being at `source` @ `index`. + * @param item the item being moved + * @param source the container in which `item` is currently located + * @param index the index position in `source` where `item` is currently located + * @param destination the container where `item` is being moved + * @param dest the index position in `destination` where `item` is being moved + * @param destinationCollisionEntry information about the contents in an area of `destination` starting at index `dest` + */ + private def PerformMoveItem(item : Equipment, + source : PlanetSideGameObject with Container, + index : Int, + destination : PlanetSideGameObject with Container, + dest : Int, + destinationCollisionEntry : Option[InventoryItem]) : Unit = { + val item_guid = item.GUID + val source_guid = source.GUID + val destination_guid = destination.GUID + val player_guid = player.GUID + val indexSlot = source.Slot(index) + val sourceIsNotDestination : Boolean = source != destination //if source is destination, explicit OCDM is not required + if(sourceIsNotDestination) { + log.info(s"MoveItem: $item moved from $source @ $index to $destination @ $dest") + } + else { + log.info(s"MoveItem: $item moved from $index to $dest in $source") + } + //remove item from source + indexSlot.Equipment = None + source match { + case obj : Vehicle => + continent.VehicleEvents ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item_guid)) + case obj : Player => + if(obj.isBackpack || source.VisibleSlots.contains(index)) { //corpse being looted, or item was in hands + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, item_guid)) + } + case _ => ; + } + + destinationCollisionEntry match { //do we have a swap item in the destination slot? + case Some(InventoryItem(item2, destIndex)) => //yes, swap + //cleanly shuffle items around to avoid losing icons + //the next ObjectDetachMessage is necessary to avoid icons being lost, but only as part of this swap + sendResponse(ObjectDetachMessage(source_guid, item_guid, Vector3.Zero, 0f)) + val item2_guid = item2.GUID + destination.Slot(destIndex).Equipment = None //remove the swap item from destination + (indexSlot.Equipment = item2) match { + case Some(_) => //item and item2 swapped places successfully + log.info(s"MoveItem: $item2 swapped to $source @ $index") + //remove item2 from destination + sendResponse(ObjectDetachMessage(destination_guid, item2_guid, Vector3.Zero, 0f)) + destination match { + case obj : Vehicle => + continent.VehicleEvents ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item2_guid)) + case obj : Player => + if(obj.isBackpack || destination.VisibleSlots.contains(dest)) { //corpse being looted, or item was accessible + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, item2_guid)) + //put hand down locally + if(dest == player.DrawnSlot) { + player.DrawnSlot = Player.HandsDownSlot + } + } + case _ => ; + } + //display item2 in source + if(sourceIsNotDestination && player == source) { + val objDef = item2.Definition + sendResponse( + ObjectCreateDetailedMessage( + objDef.ObjectId, + item2_guid, + ObjectCreateMessageParent(source_guid, index), + objDef.Packet.DetailedConstructorData(item2).get + ) + ) + } + else { + sendResponse(ObjectAttachMessage(source_guid, item2_guid, index)) + } + source match { + case obj : Vehicle => + item2.Faction = PlanetSideEmpire.NEUTRAL + continent.VehicleEvents ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player_guid, source_guid, index, item2)) + case obj : Player => + item2.Faction = obj.Faction + if(source.VisibleSlots.contains(index)) { //item is put in hands + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(player_guid, source_guid, index, item2)) + } + else if(obj.isBackpack) { //corpse being given item + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.StowEquipment(player_guid, source_guid, index, item2)) + } + case _ => + item2.Faction = PlanetSideEmpire.NEUTRAL + } + + case None => //item2 does not fit; drop on ground + log.info(s"MoveItem: $item2 can not fit in swap location; dropping on ground @ ${source.Position}") + val pos = source.Position + val sourceOrientZ = source.Orientation.z + val orient : Vector3 = Vector3(0f, 0f, sourceOrientZ) + continent.Ground ! Zone.Ground.DropItem(item2, pos, orient) + sendResponse(ObjectDetachMessage(destination_guid, item2_guid, pos, sourceOrientZ)) //ground + val objDef = item2.Definition + destination match { + case obj : Vehicle => + continent.VehicleEvents ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item2_guid)) + case _ => ; + //Player does not require special case; the act of dropping forces the item and icon to change + } + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(player_guid, item2, continent)) + } + + case None => ; + } + //move item into destination slot + destination.Slot(dest).Equipment = item + if(sourceIsNotDestination && player == destination) { + val objDef = item.Definition + sendResponse( + ObjectCreateDetailedMessage( + objDef.ObjectId, + item_guid, + ObjectCreateMessageParent(destination_guid, dest), + objDef.Packet.DetailedConstructorData(item).get + ) + ) + } + else { + sendResponse(ObjectAttachMessage(destination_guid, item_guid, dest)) + } + destination match { + case obj : Vehicle => + item.Faction = PlanetSideEmpire.NEUTRAL + continent.VehicleEvents ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player_guid, destination_guid, dest, item)) + case obj : Player => + if(destination.VisibleSlots.contains(dest)) { //item is put in hands + item.Faction = obj.Faction + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(player_guid, destination_guid, dest, item)) + } + else if(obj.isBackpack) { //corpse being given item + item.Faction = PlanetSideEmpire.NEUTRAL + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.StowEquipment(player_guid, destination_guid, dest, item)) + } + case _ => + item.Faction = PlanetSideEmpire.NEUTRAL + } + } + + /** + * na + * @param equipment na + * @param obj na + * @return `true`, if the object is allowed to contain the type of equipment object + */ + def PermitEquipmentStow(equipment : Equipment, obj : PlanetSideGameObject with Container) : Boolean = { + equipment match { + case _ : BoomerTrigger => + obj.isInstanceOf[Player] //a BoomerTrigger can only be stowed in a player's holsters or inventory + case _ => + true + } + } + /** * na * @param tool na @@ -6932,23 +7807,21 @@ class WorldSessionActor extends Actor FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match { case Nil => ; case x :: xs => - val (deleteFunc, modifyFunc) : (Equipment=>Future[Any], (AmmoBox, Int) => Unit) = obj match { + val (deleteFunc, modifyFunc) : ((Int, AmmoBox)=>Unit, (AmmoBox, Int)=>Unit) = obj match { case (veh : Vehicle) => - (RemoveOldEquipmentFromInventory(veh, taskResolver), ModifyAmmunitionInVehicle(veh)) - case o : PlanetSideServerObject with Container => - (RemoveOldEquipmentFromInventory(o, taskResolver), ModifyAmmunition(o)) + (DeleteEquipmentFromVehicle(veh), ModifyAmmunitionInVehicle(veh)) case _ => - throw new Exception("PerformToolAmmoChange: (remove/modify) should be a server object, not a regular game object") + (DeleteEquipment(obj), ModifyAmmunition(obj)) } - val (stowNewFunc, stowFunc) : (Equipment=>TaskResolver.GiveTask, Equipment=>Future[Any]) = obj match { - case o : PlanetSideServerObject with Container => - (PutNewEquipmentInInventoryOrDrop(o), PutEquipmentInInventoryOrDrop(o)) + val (stowFuncTask, stowFunc) : ((Int, AmmoBox)=>TaskResolver.GiveTask, (Int, AmmoBox)=>Unit) = obj match { + case (veh : Vehicle) => + (StowNewEquipmentInVehicle(veh), StowEquipmentInVehicles(veh)) case _ => - throw new Exception("PerformToolAmmoChange: (new/put) should be a server object, not a regular game object") + (StowNewEquipment(obj), StowEquipment(obj)) } xs.foreach(item => { obj.Inventory -= x.start - deleteFunc(item.obj) + deleteFunc(item.start, item.obj.asInstanceOf[AmmoBox]) }) //box will be the replacement ammo; give it the discovered magazine and load it into the weapon @ 0 @@ -6980,7 +7853,8 @@ class WorldSessionActor extends Actor val splitReloadAmmo : Int = sumReloadValue - fullMagazine log.info(s"ChangeAmmo: taking ${originalBoxCapacity - splitReloadAmmo} from a box of ${originalBoxCapacity} $requestedAmmoType") val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) - taskResolver ! stowNewFunc(boxForInventory) + obj.Inventory += x.start -> boxForInventory //block early; assumption warning: swappable ammo types have the same icon size + taskResolver ! stowFuncTask(x.start, boxForInventory) fullMagazine }) sendResponse(InventoryStateMessage(box.GUID, tool.GUID, box.Capacity)) //should work for both players and vehicles @@ -7014,16 +7888,25 @@ class WorldSessionActor extends Actor if(previousBox.Capacity > 0) { //split previousBox into AmmoBox objects of appropriate max capacity, e.g., 100 9mm -> 2 x 50 9mm obj.Inventory.Fit(previousBox) match { - case Some(_) => - stowFunc(previousBox) + case Some(index) => + stowFunc(index, previousBox) case None => - NormalItemDrop(player, continent)(previousBox) + NormalItemDrop(player, continent, continent.AvatarEvents)(previousBox) } + val dropFunc : (Equipment)=>TaskResolver.GiveTask = NewItemDrop(player, continent, continent.AvatarEvents) AmmoBox.Split(previousBox) match { - case Nil | List(_) => ; //done (the former case is technically not possible) + case Nil | _ :: Nil => ; //done (the former case is technically not possible) case _ :: xs => modifyFunc(previousBox, 0) //update to changed capacity value - xs.foreach(box => { taskResolver ! stowNewFunc(box) }) + xs.foreach(box => { + obj.Inventory.Fit(box) match { + case Some(index) => + obj.Inventory += index -> box //block early, for purposes of Fit + taskResolver ! stowFuncTask(index, box) + case None => + taskResolver ! dropFunc(box) + } + }) } } else { @@ -7044,10 +7927,13 @@ class WorldSessionActor extends Actor * curried for callback * @param zone the continent in which the item is being dropped; * curried for callback + * @param service a reference to the event system that announces that the item has been dropped on the ground; + * "AvatarService"; + * curried for callback * @param item the item */ - def NormalItemDrop(obj : PlanetSideServerObject with Container, zone : Zone)(item : Equipment) : Unit = { - zone.Ground.tell(Zone.Ground.DropItem(item, obj.Position, Vector3.z(obj.Orientation.z)), obj.Actor) + def NormalItemDrop(obj : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : Unit = { + continent.Ground ! Zone.Ground.DropItem(item, obj.Position, Vector3.z(obj.Orientation.z)) } /** @@ -7057,15 +7943,16 @@ class WorldSessionActor extends Actor * curried for callback * @param zone the continent in which the item is being dropped; * curried for callback + * @param service a reference to the event system that announces that the item has been dropped on the ground; + * "AvatarService"; + * curried for callback * @param item the item */ - def NewItemDrop(obj : PlanetSideServerObject with Container, zone : Zone)(item : Equipment) : TaskResolver.GiveTask = { + def NewItemDrop(obj : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : TaskResolver.GiveTask = { TaskResolver.GiveTask( new Task() { private val localItem = item - private val localFunc : (Equipment)=>Unit = NormalItemDrop(obj, zone) - - override def Description : String = s"dropping a new ${localItem.Definition.Name} on the ground" + private val localFunc : (Equipment)=>Unit = NormalItemDrop(obj, zone, service) def Execute(resolver : ActorRef) : Unit = { localFunc(localItem) @@ -7080,21 +7967,21 @@ class WorldSessionActor extends Actor * @param tool a weapon */ def FireCycleCleanup(tool : Tool) : Unit = { - //TODO replaced by more appropriate functionality in the future + //TODO this is temporary and will be replaced by more appropriate functionality in the future. val tdef = tool.Definition if(GlobalDefinitions.isGrenade(tdef)) { val ammoType = tool.AmmoType FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters case Nil => log.info(s"no more $ammoType grenades") - RemoveOldEquipmentFromInventory(player, taskResolver)(tool) + taskResolver ! RemoveEquipmentFromSlot(player, tool, player.Find(tool).get) case x :: xs => //this is similar to ReloadMessage val box = x.obj.asInstanceOf[Tool] val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { xs.map(_.obj.asInstanceOf[Tool].Magazine).reduce(_ + _) } val sumReloadValue : Int = box.Magazine + tailReloadValue val actualReloadValue = (if(sumReloadValue <= 3) { - RemoveOldEquipmentFromInventory(player, taskResolver)(x.obj) + taskResolver ! RemoveEquipmentFromSlot(player, x.obj, x.start) sumReloadValue } else { @@ -7103,14 +7990,36 @@ class WorldSessionActor extends Actor }) log.info(s"found $actualReloadValue more $ammoType grenades to throw") ModifyAmmunition(player)(tool.AmmoSlot.Box, -actualReloadValue) //grenade item already in holster (negative because empty) - xs.foreach(item => { RemoveOldEquipmentFromInventory(player, taskResolver)(item.obj) }) + xs.foreach(item => { + taskResolver ! RemoveEquipmentFromSlot(player, item.obj, item.start) + }) } } else if(tdef == GlobalDefinitions.phoenix) { - RemoveOldEquipmentFromInventory(player, taskResolver)(tool) + taskResolver ! RemoveEquipmentFromSlot(player, tool, player.Find(tool).get) } } + /** + * A predicate used to determine if an `InventoryItem` object contains `Equipment` that should be dropped. + * Used to filter through lists of object data before it is placed into a player's inventory. + * Drop the item if:
+ * - the item is cavern equipment
+ * - the item is a `BoomerTrigger` type object
+ * - the item is a `router_telepad` type object
+ * - the item is another faction's exclusive equipment + * @param tplayer the player + * @return true if the item is to be dropped; false, otherwise + */ + def DropPredicate(tplayer : Player) : (InventoryItem => Boolean) = entry => { + val objDef = entry.obj.Definition + val faction = GlobalDefinitions.isFactionEquipment(objDef) + GlobalDefinitions.isCavernEquipment(objDef) || + objDef == GlobalDefinitions.router_telepad || + entry.obj.isInstanceOf[BoomerTrigger] || + (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL) + } + /** * Given an object globally unique identifier, search in a given location for it. * @param object_guid the object @@ -7119,7 +8028,7 @@ class WorldSessionActor extends Actor * the first value is the container that matched correctly with the object's GUID; * the second value is the slot position of the object */ - def FindInLocalContainer(object_guid : PlanetSideGUID)(parent : PlanetSideServerObject with Container) : Option[(PlanetSideServerObject with Container, Option[Int])] = { + def FindInLocalContainer(object_guid : PlanetSideGUID)(parent : PlanetSideGameObject with Container) : Option[(PlanetSideGameObject with Container, Option[Int])] = { val slot : Option[Int] = parent.Find(object_guid) slot match { case place @ Some(_) => @@ -7163,14 +8072,14 @@ class WorldSessionActor extends Actor else if(vehicle.Definition == GlobalDefinitions.ant) { state match { case DriveState.Deployed => - // We only want this WorldSessionActor (not other player's WorldSessionActor) to manage timers + // We only want this WSA (not other player's WSA) to manage timers if(vehicle.Seats(0).Occupant.contains(player)){ // Start ntu regeneration // If vehicle sends UseItemMessage with silo as target NTU regeneration will be disabled and orb particles will be disabled antChargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuCharging(player, vehicle)) } case DriveState.Undeploying => - // We only want this WorldSessionActor (not other player's WorldSessionActor) to manage timers + // We only want this WSA (not other player's WSA) to manage timers if(vehicle.Seats(0).Occupant.contains(player)){ antChargingTick.cancel() // Stop charging NTU if charging } @@ -7560,7 +8469,7 @@ class WorldSessionActor extends Actor /** * A part of the process of spawning the player into the game world. * The function should work regardless of whether the player is alive or dead - it will make them alive. - * It adds the `WorldSessionActor`-current `Player` to the current zone and sends out the expected packets.
+ * It adds the `WSA`-current `Player` to the current zone and sends out the expected packets.
*
* If that player is in a vehicle, it will construct that vehicle. * If the player is the driver of the vehicle, @@ -7575,7 +8484,7 @@ class WorldSessionActor extends Actor * @see `GetKnownVehicleAndSeat` * @see `LoadZoneTransferPassengerMessages` * @see `Player.Spawn` - * @see `ReloadItemCoolDownTimes` + * @see `ReloadUsedLastCoolDownTimes` * @see `Vehicles.Own` * @see `Vehicles.ReloadAccessPermissions` */ @@ -7662,7 +8571,19 @@ class WorldSessionActor extends Actor continent.Population ! Zone.Population.Spawn(avatar, player) //cautious redundancy deadState = DeadState.Alive - ReloadItemCoolDownTimes() + ReloadUsedLastCoolDownTimes() + + val lTime = System.currentTimeMillis // PTS v3 + for (i <- 0 to whenUsedLastItem.length-1) { + if (lTime - whenUsedLastItem(i) < 300000) { + sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastItemName(i), 300 - ((lTime - whenUsedLastItem(i)) / 1000 toInt), true)) + } + } + for (i <- 1 to 3) { + if (lTime - whenUsedLastMAX(i) < 300000) { + sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastMAXName(i), 300 - ((lTime - whenUsedLastMAX(i)) / 1000 toInt), true)) + } + } } /** @@ -7795,7 +8716,7 @@ class WorldSessionActor extends Actor * @see `ObjectAttachMessage` * @see `ObjectCreateMessage` * @see `PlayerInfo.LoginInfo` - * @see `ReloadItemCoolDownTimes` + * @see `ReloadUsedLastCoolDownTimes` * @see `UpdateWeaponAtSeatPosition` * @see `Vehicles.ReloadAccessPermissions` */ @@ -7842,7 +8763,7 @@ class WorldSessionActor extends Actor } //cautious redundancy deadState = DeadState.Alive - ReloadItemCoolDownTimes() + ReloadUsedLastCoolDownTimes() setupAvatarFunc = AvatarCreate } @@ -7854,10 +8775,10 @@ class WorldSessionActor extends Actor * and this routine's normal operation when it revisits the same code. * @see `avatarSetupFunc` * @see `AvatarCreate` - * @see `ReloadItemCoolDownTimes` + * @see `ReloadUsedLastCoolDownTimes` */ def AvatarDeploymentPassOver() : Unit = { - ReloadItemCoolDownTimes() + ReloadUsedLastCoolDownTimes() setupAvatarFunc = AvatarCreate } @@ -7866,31 +8787,16 @@ class WorldSessionActor extends Actor * This is called "skill". * @see `AvatarVehicleTimerMessage` */ - def ReloadItemCoolDownTimes() : Unit = { - val time = System.currentTimeMillis - //purchases - val lastPurchases = avatar.GetAllLastPurchaseTimes - delayedPurchaseEntries.collect { case (id, delay) if lastPurchases.contains(id) => - val lastTime = lastPurchases.getOrElse(id, 0L) - val delay = delayedPurchaseEntries(id.toInt) - if (time - lastTime < delay) { - sendResponse(AvatarVehicleTimerMessage(player.GUID, player.ObjectTypeNameReference(id), ((delay - (time - lastTime)) / 1000) toInt, true)) + def ReloadUsedLastCoolDownTimes() : Unit = { + val lTime = System.currentTimeMillis + for (i <- 0 to whenUsedLastItem.length-1) { + if (lTime - whenUsedLastItem(i) < 300000) { + sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastItemName(i), 300 - ((lTime - whenUsedLastItem(i)) / 1000 toInt), true)) } } - //uses - val lastUses = avatar.GetAllLastUsedTimes - delayedGratificationEntries.collect { case (id, delay) if lastUses.contains(id) => - val lastTime = lastUses.getOrElse(id, 0L) - val delay = delayedGratificationEntries(id.toInt) - if (time - lastTime < delay) { - sendResponse(AvatarVehicleTimerMessage(player.GUID, player.ObjectTypeNameReference(id), ((delay - (time - lastTime)) / 1000) toInt, true)) - } - } - //max exo-suits (specifically) - (1 to 3).foreach { subtype => - val maxTime = player.GetLastUsedTime(ExoSuitType.MAX, subtype) - if (maxTime > 0 && time - maxTime < 300000) { //5min - sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastMAXName(subtype), 300 - ((time - maxTime) / 1000 toInt), true)) + for (i <- 1 to 3) { + if (lTime - whenUsedLastMAX(i) < 300000) { + sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastMAXName(i), 300 - ((lTime - whenUsedLastMAX(i)) / 1000 toInt), true)) } } } @@ -7917,16 +8823,18 @@ class WorldSessionActor extends Actor * @param obj the player to be turned into a corpse */ def FriskDeadBody(obj : Player) : Unit = { - if(!obj.isAlive) { + if(obj.isBackpack) { obj.Slot(4).Equipment match { case None => ; case Some(knife) => - RemoveOldEquipmentFromInventory(obj, taskResolver)(knife) + obj.Slot(4).Equipment = None + taskResolver ! RemoveEquipmentFromSlot(obj, knife, 4) } obj.Slot(0).Equipment match { case Some(arms : Tool) => if(GlobalDefinitions.isMaxArms(arms.Definition)) { - RemoveOldEquipmentFromInventory(obj, taskResolver)(arms) + obj.Slot(0).Equipment = None + taskResolver ! RemoveEquipmentFromSlot(obj, arms, 0) } case _ => ; } @@ -7941,7 +8849,7 @@ class WorldSessionActor extends Actor } }) val triggers = RemoveBoomerTriggersFromInventory() - triggers.foreach(trigger => { NormalItemDrop(obj, continent)(trigger) }) + triggers.foreach(trigger => { NormalItemDrop(obj, continent, continent.AvatarEvents)(trigger) }) } } @@ -7963,15 +8871,13 @@ class WorldSessionActor extends Actor def PrepareToTurnPlayerIntoCorpse(tplayer : Player, zone : Zone) : Unit = { FriskDeadBody(tplayer) if(!WellLootedDeadBody(tplayer)) { - tplayer.Release - zone.Population ! Zone.Corpse.Add(tplayer) TurnPlayerIntoCorpse(tplayer) + zone.Population ! Zone.Corpse.Add(tplayer) zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.Release(tplayer, zone)) } else { //no items in inventory; leave no corpse val pguid = tplayer.GUID - zone.Population ! Zone.Population.Release(avatar) sendResponse(ObjectDeleteMessage(pguid, 0)) zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.ObjectDelete(pguid, pguid, 0)) taskResolver ! GUIDTask.UnregisterPlayer(tplayer)(zone.GUID) @@ -7998,7 +8904,7 @@ class WorldSessionActor extends Actor * `false`, otherwise */ def WellLootedDeadBody(obj : Player) : Boolean = { - !obj.isAlive && obj.Holsters().count(_.Equipment.nonEmpty) == 0 && obj.Inventory.Size == 0 + obj.isBackpack && obj.Holsters().count(_.Equipment.nonEmpty) == 0 && obj.Inventory.Size == 0 } /** @@ -8008,7 +8914,7 @@ class WorldSessionActor extends Actor * `false`, otherwise */ def TryDisposeOfLootedCorpse(obj : Player) : Boolean = { - if(obj.isBackpack && WellLootedDeadBody(obj)) { + if(WellLootedDeadBody(obj)) { continent.AvatarEvents ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), continent)) true } @@ -8430,7 +9336,7 @@ class WorldSessionActor extends Actor case obj : ComplexDeployable if obj.CanDamage => obj.Actor ! Vitality.Damage(func) case obj : SimpleDeployable if obj.CanDamage => - //damage is synchronized on `LSA` (results returned to and distributed from this `WorldSessionActor`) + //damage is synchronized on `LSA` (results returned to and distributed from this `WSA`) continent.LocalEvents ! Vitality.DamageOn(obj, func) case _ => ; } @@ -8463,11 +9369,53 @@ class WorldSessionActor extends Actor ) } + /** + * Initialize the deployables backend information. + * @param avatar the player's core + */ + def InitializeDeployableQuantities(avatar : Avatar) : Unit = { + log.info("Setting up combat engineering ...") + avatar.Deployables.Initialize(avatar.Certifications.toSet) + } + + /** + * Initialize the UI elements for deployables. + * @param avatar the player's core + */ + def InitializeDeployableUIElements(avatar : Avatar) : Unit = { + log.info("Setting up combat engineering UI ...") + UpdateDeployableUIElements(avatar.Deployables.UpdateUI()) + } + + /** + * The player learned a new certification. + * Update the deployables user interface elements if it was an "Engineering" certification. + * The certification "Advanced Hacking" also relates to an element. + * @param certification the certification that was added + * @param certificationSet all applicable certifications + */ + def AddToDeployableQuantities(certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : Unit = { + avatar.Deployables.AddToDeployableQuantities(certification, certificationSet) + UpdateDeployableUIElements(avatar.Deployables.UpdateUI(certification)) + } + + /** + * The player forgot a certification he previously knew. + * Update the deployables user interface elements if it was an "Engineering" certification. + * The certification "Advanced Hacking" also relates to an element. + * @param certification the certification that was added + * @param certificationSet all applicable certifications + */ + def RemoveFromDeployablesQuantities(certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : Unit = { + avatar.Deployables.RemoveFromDeployableQuantities(certification, certificationSet) + UpdateDeployableUIElements(avatar.Deployables.UpdateUI(certification)) + } + /** * Initialize the deployables user interface elements.
*
* All element initializations require both the maximum deployable amount and the current deployables active counts. - * Until initialized, all elements will be RED 0/0 as if the corresponding certification were not `learn`ed. + * Until initialized, all elements will be RED 0/0 as if the cooresponding certification were not `learn`ed. * The respective element will become a pair of numbers, the second always being non-zero, when properly initialized. * The numbers will appear GREEN when more deployables of that type can be placed. * The numbers will appear RED if the player can not place any more of that type of deployable. @@ -8695,9 +9643,10 @@ class WorldSessionActor extends Actor * @param index the slot index * @param pos where to drop the object in the game world */ - def TryDropFDU(tool : ConstructionItem, index : Int, pos : Vector3) : Unit = { - if(tool.Definition == GlobalDefinitions.advanced_ace) { - DropEquipmentFromInventory(player)(tool, Some(pos)) + def TryDropConstructionTool(tool : ConstructionItem, index : Int, pos : Vector3) : Unit = { + if(tool.Definition == GlobalDefinitions.advanced_ace && + SafelyRemoveConstructionItemFromSlot(tool, index, "TryDropConstructionTool")) { + continent.Ground ! Zone.Ground.DropItem(tool, pos, Vector3.Zero) } } @@ -8825,27 +9774,27 @@ class WorldSessionActor extends Actor * `false`, otherwise */ def FindEquipmentToDelete(object_guid : PlanetSideGUID, obj : Equipment) : Boolean = { - val findFunc : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] = + val findFunc : PlanetSideGameObject with Container => Option[(PlanetSideGameObject with Container, Option[Int])] = FindInLocalContainer(object_guid) findFunc(player.Locker) .orElse(findFunc(player)) .orElse(accessedContainer match { - case Some(parent : PlanetSideServerObject) => + case Some(parent) => findFunc(parent) - case _ => + case None => None }) .orElse(FindLocalVehicle match { - case Some(parent : PlanetSideServerObject) => + case Some(parent) => findFunc(parent) - case _ => + case None => None }) match { case Some((parent, Some(slot))) => obj.Position = Vector3.Zero - RemoveOldEquipmentFromInventory(parent, taskResolver)(obj) + taskResolver ! RemoveEquipmentFromSlot(parent, obj, slot) log.info(s"RequestDestroy: equipment $obj") true @@ -8854,6 +9803,7 @@ class WorldSessionActor extends Actor obj.Position = Vector3.Zero continent.Ground ! Zone.Ground.RemoveItem(object_guid) continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent)) + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), object_guid)) log.info(s"RequestDestroy: equipment $obj on ground") true } @@ -8989,7 +9939,7 @@ class WorldSessionActor extends Actor case Some(vehicle : Vehicle) => //driver or passenger in vehicle using a warp gate, or a droppod LoadZoneInVehicle(vehicle, pos, ori, zone_id) - case _ if player.HasGUID => //player is deconstructing self or instant action + case _ if player.HasGUID => //player is deconstructing self val player_guid = player.GUID sendResponse(ObjectDeleteMessage(player_guid, 4)) continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 4)) @@ -9263,6 +10213,72 @@ class WorldSessionActor extends Actor squadSetup = ZoneChangeSquadSetup } + /** + * Primary functionality for tranferring a piece of equipment from a player's hands or his inventory to the ground. + * Items are always dropped at player's feet because for simplicity's sake + * because, by virtue of already standing there, the stability of the surface has been proven. + * The only exception to this is dropping items while falling. + * @see `Player.Find`
+ * `ObjectDetachMessage` + * @param item the `Equipment` object in the player's hand + * @param pos the game world coordinates where the object will be dropped + * @param orient a suggested orientation in which the object will settle when on the ground; + * as indicated, the simulation is only concerned with certain angles + */ + def PutItemOnGround(item : Equipment, pos : Vector3, orient : Vector3) : Unit = { + CancelZoningProcessWithDescriptiveReason("cancel_use") + //TODO delay or reverse dropping item when player is falling down + item.Position = pos + item.Orientation = Vector3.z(orient.z) + item.Faction = PlanetSideEmpire.NEUTRAL + //dropped items rotate towards the user's standing direction + val exclusionId = player.Find(item) match { + //if the item is in our hands ... + case Some(slotNum) => + player.Slot(slotNum).Equipment = None + sendResponse(ObjectDetachMessage(player.GUID, item.GUID, pos, orient.z)) + sendResponse(ActionResultMessage.Pass) + player.GUID //we're dropping the item; don't need to see it dropped again + case None => + PlanetSideGUID(0) //item is being introduced into the world upon drop + } + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(exclusionId, item, continent)) + } + + /** + * Primary functionality for tranferring a piece of equipment from the ground in a player's hands or his inventory. + * The final destination of the item in terms of slot position is not determined until the attempt is made. + * If it can not be placed in a slot correctly, the item will be returned to the ground in the same place. + * @see `Player.Fit` + * @param item the `Equipment` object on the ground + * @return `true`, if the object was properly picked up; + * `false` if it was returned to the ground + */ + def PutItemInHand(item : Equipment) : Boolean = { + player.Fit(item) match { + case Some(slotNum) => + CancelZoningProcessWithDescriptiveReason("cancel_use") + item.Faction = player.Faction + val item_guid = item.GUID + val player_guid = player.GUID + player.Slot(slotNum).Equipment = item + val definition = item.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + item_guid, + ObjectCreateMessageParent(player_guid, slotNum), + definition.Packet.DetailedConstructorData(item).get + ) + ) + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PickupItem(player_guid, continent, player, slotNum, item)) + true + case None => + continent.Ground ! Zone.Ground.DropItem(item, item.Position, item.Orientation) //restore previous state + false + } + } + /** * Attempt to link the router teleport system using the provided terminal information. * Although additional states are necessary to properly use the teleportation system, @@ -10102,7 +11118,8 @@ class WorldSessionActor extends Actor * @see `Player.Release` */ def GoToDeploymentMap() : Unit = { - deadState = DeadState.Release //we may be alive or dead, may or may not be a corpse + player.Release + deadState = DeadState.Release sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true)) DrawCurrentAmsSpawnPoint() } @@ -10497,57 +11514,6 @@ class WorldSessionActor extends Actor } object WorldSessionActor { - /** Object purchasing cooldowns.
- * key - object id
- * value - time last used (ms) - * */ - val delayedPurchaseEntries : Map[Int, Long] = Map( - GlobalDefinitions.ams.ObjectId -> 300000, //5min - GlobalDefinitions.ant.ObjectId -> 300000, //5min - GlobalDefinitions.apc_nc.ObjectId -> 300000, //5min - GlobalDefinitions.apc_tr.ObjectId -> 300000, //5min - GlobalDefinitions.apc_vs.ObjectId -> 300000, //5min - GlobalDefinitions.aurora.ObjectId -> 300000, //5min - GlobalDefinitions.battlewagon.ObjectId -> 300000, //5min - GlobalDefinitions.dropship.ObjectId -> 300000, //5min - GlobalDefinitions.flail.ObjectId -> 300000, //5min - GlobalDefinitions.fury.ObjectId -> 300000, //5min - GlobalDefinitions.galaxy_gunship.ObjectId -> 600000, //10min - GlobalDefinitions.lodestar.ObjectId -> 300000, //5min - GlobalDefinitions.liberator.ObjectId -> 300000, //5min - GlobalDefinitions.lightgunship.ObjectId -> 300000, //5min - GlobalDefinitions.lightning.ObjectId -> 300000, //5min - GlobalDefinitions.magrider.ObjectId -> 300000, //5min - GlobalDefinitions.mediumtransport.ObjectId -> 300000, //5min - GlobalDefinitions.mosquito.ObjectId -> 300000, //5min - GlobalDefinitions.phantasm.ObjectId -> 300000, //5min - GlobalDefinitions.prowler.ObjectId -> 300000, //5min - GlobalDefinitions.quadassault.ObjectId -> 300000, //5min - GlobalDefinitions.quadstealth.ObjectId -> 300000, //5min - GlobalDefinitions.router.ObjectId -> 300000, //5min - GlobalDefinitions.switchblade.ObjectId -> 300000, //5min - GlobalDefinitions.skyguard.ObjectId -> 300000, //5min - GlobalDefinitions.threemanheavybuggy.ObjectId -> 300000, //5m - GlobalDefinitions.thunderer.ObjectId -> 300000, //5min - GlobalDefinitions.two_man_assault_buggy.ObjectId -> 300000, //5min - GlobalDefinitions.twomanhoverbuggy.ObjectId -> 300000, //5min - GlobalDefinitions.twomanheavybuggy.ObjectId -> 300000, //5min - GlobalDefinitions.vanguard.ObjectId -> 300000, //5min - GlobalDefinitions.vulture.ObjectId -> 300000, //5min - GlobalDefinitions.wasp.ObjectId -> 300000, //5min - GlobalDefinitions.flamethrower.ObjectId -> 180000 //3min - ) - /** Object use cooldowns.
- * key - object id
- * value - time last used (ms) - * */ - val delayedGratificationEntries : Map[Int, Long] = Map( - GlobalDefinitions.medkit.ObjectId -> 5000, //5s - GlobalDefinitions.super_armorkit.ObjectId -> 1200000, //20min - GlobalDefinitions.super_medkit.ObjectId -> 1200000, //20min - GlobalDefinitions.super_staminakit.ObjectId -> 1200000 //20min - ) - final case class ResponseToSelf(pkt : PlanetSideGamePacket) private final case class PokeClient() diff --git a/pslogin/src/test/scala/actor/service/AvatarServiceTest.scala b/pslogin/src/test/scala/actor/service/AvatarServiceTest.scala index 01c6c0fd..42f5ae6d 100644 --- a/pslogin/src/test/scala/actor/service/AvatarServiceTest.scala +++ b/pslogin/src/test/scala/actor/service/AvatarServiceTest.scala @@ -163,7 +163,7 @@ class DroptItemTest extends ActorTest { "AvatarService" should { "pass DropItem" in { service ! Service.Join("test") - service ! AvatarServiceMessage("test", AvatarAction.DropItem(PlanetSideGUID(10), tool)) + service ! AvatarServiceMessage("test", AvatarAction.DropItem(PlanetSideGUID(10), tool, Zone.Nowhere)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.DropItem(pkt))) } } @@ -264,16 +264,41 @@ class PlayerStateTest extends ActorTest { } } -class PickupItemTest extends ActorTest { +class PickupItemATest extends ActorTest { + val obj = Player(Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + obj.GUID = PlanetSideGUID(10) + obj.Slot(5).Equipment.get.GUID = PlanetSideGUID(11) + + val toolDef = GlobalDefinitions.beamer + val tool = Tool(toolDef) + tool.GUID = PlanetSideGUID(40) + tool.AmmoSlots.head.Box.GUID = PlanetSideGUID(41) + val pkt = ObjectCreateMessage( + toolDef.ObjectId, + tool.GUID, + ObjectCreateMessageParent(PlanetSideGUID(10), 0), + toolDef.Packet.ConstructorData(tool).get + ) + + "pass PickUpItem as EquipmentInHand (visible pistol slot)" in { + ServiceManager.boot(system) + val service = system.actorOf(Props(classOf[AvatarService], Zone.Nowhere), AvatarServiceTest.TestName) + service ! Service.Join("test") + service ! AvatarServiceMessage("test", AvatarAction.PickupItem(PlanetSideGUID(10), Zone.Nowhere, obj, 0, tool)) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentInHand(pkt))) + } +} + +class PickupItemBTest extends ActorTest { val obj = Player(Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) val tool = Tool(GlobalDefinitions.beamer) tool.GUID = PlanetSideGUID(40) - "pass PickUpItem" in { + "pass PickUpItem as ObjectDelete (not visible inventory space)" in { ServiceManager.boot(system) val service = system.actorOf(Props(classOf[AvatarService], Zone.Nowhere), AvatarServiceTest.TestName) service ! Service.Join("test") - service ! AvatarServiceMessage("test", AvatarAction.PickupItem(PlanetSideGUID(10), tool)) + service ! AvatarServiceMessage("test", AvatarAction.PickupItem(PlanetSideGUID(10), Zone.Nowhere, obj, 6, tool)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectDelete(tool.GUID, 0))) } }