diff --git a/build.sbt b/build.sbt index 967ab1924..b825a0165 100644 --- a/build.sbt +++ b/build.sbt @@ -49,7 +49,7 @@ lazy val commonSettings = Seq( "io.kamon" %% "kamon-apm-reporter" % "2.1.0", "org.json4s" %% "json4s-native" % "3.6.8", "com.typesafe.akka" %% "akka-stream" % "2.6.5", - ), + ) ) 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 02c83a268..451d85ffd 100644 --- a/common/src/main/scala/net/psforever/objects/Avatar.scala +++ b/common/src/main/scala/net/psforever/objects/Avatar.scala @@ -57,6 +57,33 @@ 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 @@ -189,7 +216,8 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet def FifthSlot : EquipmentSlot = { new OffhandEquipmentSlot(EquipmentSize.Inventory) { - Equipment = locker + val obj = new LockerEquipment(locker) + Equipment = obj } } @@ -220,6 +248,70 @@ 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 a241e9782..25b2a69fd 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.PlanetSideGUID +import net.psforever.types.{CertificationType, PlanetSideGUID} import services.RemoverActor import services.local.{LocalAction, LocalServiceMessage} @@ -123,4 +123,48 @@ 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 c8541002f..572b48036 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 = "bullet_9mm" + bullet_9mm.Name = "9mmbullet" bullet_9mm.Capacity = 50 bullet_9mm.Tile = InventoryTile.Tile33 - bullet_9mm_AP.Name="bullet_9mm_AP" + bullet_9mm_AP.Name="9mmbullet_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 = "bullet_35mm" + bullet_35mm.Name = "35mmbullet" 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 = "bullet_25mm" + bullet_25mm.Name = "25mmbullet" bullet_25mm.Capacity = 150 bullet_25mm.Tile = InventoryTile.Tile44 - bullet_75mm.Name = "bullet_75mm" + bullet_75mm.Name = "75mmbullet" 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 = "bullet_20mm" + bullet_20mm.Name = "20mmbullet" bullet_20mm.Capacity = 200 bullet_20mm.Tile = InventoryTile.Tile44 - bullet_12mm.Name = "bullet_12mm" + bullet_12mm.Name = "12mmbullet" 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 = "bullet_15mm" + bullet_15mm.Name = "15mmbullet" 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 = "bullet_105mm" + bullet_105mm.Name = "105mmbullet" 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 = "bullet_150mm" + bullet_150mm.Name = "150mmbullet" 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 7793a6a5c..02ab44718 100644 --- a/common/src/main/scala/net/psforever/objects/LockerContainer.scala +++ b/common/src/main/scala/net/psforever/objects/LockerContainer.scala @@ -1,9 +1,17 @@ // 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 @@ -11,9 +19,18 @@ import net.psforever.objects.inventory.{Container, GridInventory} * 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 Equipment with Container { +class LockerContainer extends PlanetSideServerObject + with Container { + private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL 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] @@ -26,3 +43,79 @@ 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 4144415db..7da2c79d7 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -217,6 +217,8 @@ 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) => @@ -617,6 +619,30 @@ 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 486265c73..9260da455 100644 --- a/common/src/main/scala/net/psforever/objects/Players.scala +++ b/common/src/main/scala/net/psforever/objects/Players.scala @@ -1,10 +1,17 @@ // 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") @@ -46,4 +53,71 @@ 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 00356be44..8e5c6a97c 100644 --- a/common/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/common/src/main/scala/net/psforever/objects/Vehicle.scala @@ -3,7 +3,7 @@ package net.psforever.objects import net.psforever.objects.definition.VehicleDefinition import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} -import net.psforever.objects.inventory.{Container, GridInventory, InventoryTile} +import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile} import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity @@ -16,6 +16,7 @@ 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.
@@ -451,6 +452,20 @@ 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 fa455aa3f..b5027cd57 100644 --- a/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -1,134 +1,153 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.avatar -import akka.actor.Actor -import net.psforever.objects.{Default, GlobalDefinitions, ImplantSlot, Player, Players, Tool} -import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile, SourceEntry} +import akka.actor.{Actor, ActorRef, Props} +import net.psforever.objects._ +import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile} import net.psforever.objects.definition.ImplantDefinition -import net.psforever.objects.equipment.{Ammo, JammableBehavior, JammableUnit} +import net.psforever.objects.equipment._ +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.vital.{PlayerSuicide, Vitality} -import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} 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.types.{ExoSuitType, ImplantType, PlanetSideGUID, Vector3} -import services.Service +import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent +import net.psforever.types._ +import services.{RemoverActor, Service} import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.local.{LocalAction, LocalServiceMessage} import scala.concurrent.duration._ import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global - class PlayerControl(player : Player) extends Actor with JammableBehavior - with Damageable { + with Damageable + with ContainableBehavior { 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) // A collection of timers for each slot to trigger stamina drain on an interval val implantSlotStaminaDrainTimers = mutable.HashMap(0 -> Default.Cancellable, 1 -> Default.Cancellable, 2 -> Default.Cancellable) + // 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 = { + lockerControlAgent ! akka.actor.PoisonPill + player.Locker.Actor = Default.Actor + 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) = Default.Cancellable + // 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) = Default.Cancellable - 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) = Default.Cancellable - } - implantSlot.Active = true + 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) = Default.Cancellable + } + implantSlot.Active = true - if (implant.ActivationStaminaCost >= 0) { - player.Stamina -= implant.ActivationStaminaCost // Activation stamina drain - } + 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.scheduleWithFixedDelay(0 seconds, implant.GetCostIntervalByExoSuit(player.ExoSuit) milliseconds, self, Player.DrainStamina(implant.StaminaCost)) - } + if(implant.StaminaCost > 0 && implant.GetCostIntervalByExoSuit(player.ExoSuit) > 0) { // Ongoing stamina drain, if applicable + implantSlotStaminaDrainTimers(slot) = context.system.scheduler.scheduleWithFixedDelay(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 _ => ; + 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) + 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}") } - // Start client side initialization timer - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(player.GUID, ActionProgressMessage(slot + 6, 0))) + case Player.UninitializeImplant(slot: Int) => + PlayerControl.UninitializeImplant(player, slot) - // Callback after initialization timer to complete initialization - implantSlot.InitializeTimer = context.system.scheduler.scheduleOnce(implantSlot.MaxTimer seconds, self, Player.ImplantInitializationComplete(slot)) - } + case Player.ImplantInitializationStart(slot: Int) => + val implantSlot = player.ImplantSlot(slot) + if(implantSlot.Installed.isDefined) { + if(implantSlot.Initialized) { + PlayerControl.UninitializeImplant(player, 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 != Default.Cancellable) { - implantSlot.InitializeTimer.cancel() - implantSlot.InitializeTimer = Default.Cancellable + // 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.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))) + 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 != Default.Cancellable) { + implantSlot.InitializeTimer.cancel() + implantSlot.InitializeTimer = Default.Cancellable + } } - } 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))) - } - } - player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttributeSelf(player.GUID, 2, player.Stamina)) + case Player.DrainStamina(amount : Int) => + player.Stamina -= amount + + 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)) + case Player.Die() => if(player.isAlive) { PlayerControl.DestructionAwareness(player, None) @@ -206,6 +225,263 @@ 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) < 300000L) { + 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) < 300000L) { + 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 _ => ; } @@ -257,12 +533,11 @@ class PlayerControl(player : Player) extends Actor 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 - - for(slot <- 0 to player.Implants.length - 1) { // Deactivate & uninitialize all implants +// 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 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) @@ -274,7 +549,7 @@ class PlayerControl(player : Player) extends Actor } override def CancelJammeredStatus(target: Any): Unit = { - for(slot <- 0 to player.Implants.length - 1) { // Start reinitializing all implants + player.Implants.indices.foreach { slot => // Start reinitializing all implants self ! Player.ImplantInitializationStart(slot) } @@ -299,6 +574,101 @@ 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 { @@ -389,19 +759,20 @@ object PlayerControl { //unjam target.Actor ! JammableUnit.ClearJammeredSound() target.Actor ! JammableUnit.ClearJammeredStatus() - events ! AvatarServiceMessage(nameChannel, AvatarAction.Killed(player_guid)) //align client interface fields with state + events ! AvatarServiceMessage(nameChannel, AvatarAction.Killed(player_guid, target.VehicleSeated)) //align client interface fields with state zone.GUID(target.VehicleSeated) match { case Some(obj : Mountable) => - //boot cadaver from seat - events ! AvatarServiceMessage(nameChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID, - ObjectDetachMessage(obj.GUID, player_guid, target.Position, Vector3.Zero)) - ) + //boot cadaver from seat internally (vehicle perspective) obj.PassengerInSeat(target) match { case Some(index) => obj.Seats(index).Occupant = None case _ => ; } - //make player invisible + //boot cadaver from seat on client + events ! AvatarServiceMessage(nameChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID, + ObjectDetachMessage(obj.GUID, player_guid, target.Position, Vector3.Zero)) + ) + //make player invisible on client events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 29, 1)) //only the dead player should "see" their own body, so that the death camera has something to focus on events ! AvatarServiceMessage(zoneChannel, AvatarAction.ObjectDelete(player_guid, player_guid)) 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 38fe6984e..ba24b1d81 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.LockerContainer +import net.psforever.objects.LockerEquipment 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[LockerContainer]() { - override def ConstructorData(obj : LockerContainer) : Try[LockerContainerData] = { +class LockerContainerConverter extends ObjectCreateConverter[LockerEquipment]() { + override def ConstructorData(obj : LockerEquipment) : Try[LockerContainerData] = { MakeInventory(obj.Inventory) match { case Nil => Success(LockerContainerData(None)) @@ -19,7 +19,7 @@ class LockerContainerConverter extends ObjectCreateConverter[LockerContainer]() } } - override def DetailedConstructorData(obj : LockerContainer) : Try[DetailedLockerContainerData] = { + override def DetailedConstructorData(obj : LockerEquipment) : 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 f62d5ec34..9f9b5c2ec 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,12 +86,11 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { } private def MakeMountings(obj : Vehicle) : List[InventoryItemData.InventoryItem] = { - 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 + 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 } 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 3443ad96f..9107d2a9b 100644 --- a/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala +++ b/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala @@ -43,6 +43,8 @@ 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 } @@ -90,6 +92,9 @@ 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 @@ -124,8 +129,6 @@ object GUIDTask { obj match { case tool : Tool => RegisterTool(tool) - case locker : LockerContainer => - RegisterLocker(locker) case _ => RegisterObjectTask(obj) } @@ -215,6 +218,8 @@ 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 } @@ -254,6 +259,9 @@ 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 @@ -282,8 +290,6 @@ 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 625adc77f..9b4e83e3a 100644 --- a/common/src/main/scala/net/psforever/objects/guid/Task.scala +++ b/common/src/main/scala/net/psforever/objects/guid/Task.scala @@ -4,6 +4,7 @@ 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 557bfece2..ff42d5127 100644 --- a/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala +++ b/common/src/main/scala/net/psforever/objects/guid/TaskResolver.scala @@ -83,7 +83,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") + trace(s"enqueue and start task ${aTask.Description}") entry.Execute(self) StartTimeoutCheck() } @@ -113,12 +113,13 @@ class TaskResolver() extends Actor { private def QueueSubtasks(task : Task, subtasks : List[TaskResolver.GiveTask], resolver : ActorRef = ActorRef.noSender) : Unit = { val entry : TaskResolver.TaskEntry = TaskResolver.TaskEntry(task, subtasks.map(task => task.task), resolver) tasks += entry - trace(s"enqueue task $task") + trace(s"enqueue task ${task.Description}") if(subtasks.isEmpty) { //a leaf in terms of task dependency; so, not dependent on any other work - trace(s"start task $task") + trace(s"start task ${task.Description}") 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 +162,7 @@ class TaskResolver() extends Actor { private def GeneralOnSuccess(index : Int) : Unit = { val entry = tasks(index) entry.task.onSuccess() - trace(s"success with this task ${entry.task}") + trace(s"success with task ${entry.task.Description}") if(entry.supertaskRef != ActorRef.noSender) { entry.supertaskRef ! TaskResolver.CompletedSubtask(entry.task) //alert our dependent task's resolver that we have completed } @@ -178,7 +179,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}") + trace(s"start new task ${entry.task.Description}") entry.Execute(self) StartTimeoutCheck() } @@ -225,7 +226,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 this task $task") + trace(s"failure with task ${task.Description}") task.onAbort(ex) task.onFailure(ex) if(entry.supertaskRef != ActorRef.noSender) { @@ -268,7 +269,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") + trace(s"aborting task ${subtask.Description}") 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 bc08949a4..2038d5aa7 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 gridkeeps track of the location of items by storing the primitive of their GUID in one or more cells. + * The grid keeps 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,17 +188,19 @@ class GridInventory extends Container { } else { val collisions : mutable.Set[InventoryItem] = mutable.Set[InventoryItem]() - 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 + 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 + } } - }) Success(collisions.toList) } } @@ -578,7 +580,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 } @@ -779,4 +781,17 @@ 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 64668c4e7..0f2ad13f7 100644 --- a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala +++ b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala @@ -53,7 +53,9 @@ object Loadout { def Create(vehicle : Vehicle, label : String) : Loadout = { VehicleLoadout( label, - packageSimplifications(vehicle.Weapons.map({ case (index, weapon) => InventoryItem(weapon.Equipment.get, index) }).toList), + packageSimplifications(vehicle.Weapons.collect { case (index, slot) if slot.Equipment.nonEmpty => + InventoryItem(slot.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 new file mode 100644 index 000000000..f6b9fe24d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala @@ -0,0 +1,632 @@ +// 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.{Failure, 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.onComplete { + case Success(Containable.ItemPutInSlot(_, _, _, None)) => ; //successful + + case Success(Containable.ItemPutInSlot(_, _, _, Some(swapItem))) => //successful, but with swap item + PutItBackOrDropIt(source, swapItem, slot, destination.Actor) + + case Success(_ : Containable.CanNotPutItemInSlot) => //failure case ; try restore original item placement + PutItBackOrDropIt(source, item, slot, source.Actor) + + case Failure(_) => //failure case ; try restore original item placement + PutItBackOrDropIt(source, item, slot, source.Actor) + + case _ => ; //TODO what? + } + //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.onComplete { + case Success(_ : Containable.CanNotPutItemInSlot) => + container.Zone.Ground.tell(Zone.Ground.DropItem(item, container.Position, Vector3.z(container.Orientation.z)), to) + + case Failure(_) => + container.Zone.Ground.tell(Zone.Ground.DropItem(item, container.Position, Vector3.z(container.Orientation.z)), to) + + case _ => ; //normal success; //TODO what? + } + } + + /** + * 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 a4bb0e780..dac5366f2 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 +import akka.actor.{ActorContext, ActorRef} import net.psforever.objects.definition.ImplantDefinition import net.psforever.objects.{Default, Player, Vehicle} import net.psforever.objects.equipment.Equipment @@ -92,6 +92,14 @@ 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 { @@ -100,8 +108,9 @@ object OrderTerminalDefinition { * @see `ItemTransactionMessage` */ sealed trait Tab { - def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal() + def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange def Sell(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal() + def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit } /** @@ -119,6 +128,10 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } + + def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { + msg.player.Actor ! msg + } } /** @@ -144,6 +157,13 @@ 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 + } + } } /** @@ -171,6 +191,10 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } + + def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { + sender ! msg + } } /** @@ -187,6 +211,10 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } + + def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { + sender ! msg + } } /** @@ -215,6 +243,10 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } + + def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { + sender ! msg + } } /** @@ -272,6 +304,10 @@ object OrderTerminalDefinition { Terminal.NoDeal() } } + + def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { + msg.player.Actor ! msg + } } /** @@ -300,6 +336,14 @@ 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()) + } + } } /** @@ -331,6 +375,10 @@ 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 b493477a5..525ed0aea 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 +import akka.actor.{Actor, ActorRef} import net.psforever.objects.{GlobalDefinitions, SimpleItem} import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior @@ -30,7 +30,10 @@ class TerminalControl(term : Terminal) extends Actor .orElse(canBeRepairedByNanoDispenser) .orElse { case Terminal.Request(player, msg) => - sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg)) + TerminalControl.Dispatch( + sender, + term, + 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 @@ -46,5 +49,16 @@ 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 10d017600..cb2a2885a 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,6 +1,7 @@ // 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 @@ -22,4 +23,6 @@ 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 2d4d7e6f2..80c3dbe1c 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -1,21 +1,28 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2017-2020 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.JammableMountedWeapons +import net.psforever.objects.equipment.{Equipment, EquipmentSlot, JammableMountedWeapons} +import net.psforever.objects.inventory.{GridInventory, InventoryItem} 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.{DriveState, ExoSuitType, PlanetSideGUID, Vector3} -import services.{RemoverActor, Service} +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 services.vehicle.{VehicleAction, VehicleServiceMessage} import scala.concurrent.ExecutionContext.Implicits.global @@ -36,7 +43,8 @@ class VehicleControl(vehicle : Vehicle) extends Actor with CargoBehavior with DamageableVehicle with RepairableVehicle - with JammableMountedWeapons { + with JammableMountedWeapons + with ContainableBehavior { //make control actors belonging to utilities when making control actor belonging to vehicle vehicle.Utilities.foreach({case (_, util) => util.Setup }) @@ -48,6 +56,7 @@ 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 @@ -72,6 +81,7 @@ class VehicleControl(vehicle : Vehicle) extends Actor .orElse(jammableBehavior) .orElse(takesDamage) .orElse(canBeRepairedByNanoDispenser) + .orElse(containerBehavior) .orElse { case Vehicle.Ownership(None) => LoseOwnership() @@ -143,6 +153,52 @@ 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 : EquipmentSlot) 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) => @@ -261,6 +317,73 @@ class VehicleControl(vehicle : Vehicle) extends Actor case None => ; } } + + 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))) + } } object VehicleControl { 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 7d41516dc..41d375c91 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala @@ -4,6 +4,8 @@ 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 @@ -28,19 +30,28 @@ 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) => - FindItemOnGround(item_guid) //intentionally no callback + //intentionally no callback + FindItemOnGround(item_guid) match { + case Some(item) => + zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PickupItem(Service.defaultPlayerGUID, item, 0)) + case None => ; + } 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 f6bae4bb7..d6225590c 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala @@ -63,7 +63,23 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player } case Zone.Corpse.Add(player) => - if(CorpseAdd(player, corpseList)) { + //player can be a corpse if they are in the current zone or are not in any zone + //player is "found" if their avatar can be matched by name within this zone and it has a character + val (canBeCorpse, playerNotFound) = if(player.Zone == zone) { + playerMap.find { case (a, _) => a.name == player.Name } match { + case Some((a, Some(p))) if p eq player => + PopulationRelease(a, playerMap) + (true, false) + case Some((a, None)) => + (true, true) + case _ => + (false, false) + } + } + else { + (player.Zone == Zone.Nowhere, true) + } + if(canBeCorpse && CorpseAdd(player, corpseList) && playerNotFound) { player.Actor = context.actorOf(Props(classOf[PlayerControl], player), name = s"corpse_of_${GetPlayerControlName(player, None)}") player.Zone = zone } @@ -219,4 +235,4 @@ object ZonePopulationActor { s"${player.CharId}_${player.GUID.guid}_${System.currentTimeMillis}" //new } } -} +} \ No newline at end of file diff --git a/common/src/main/scala/services/avatar/AvatarService.scala b/common/src/main/scala/services/avatar/AvatarService.scala index 93faada03..3f33c4b66 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), @@ -123,9 +123,9 @@ class AvatarService(zone : Zone) extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.HitHint(source_guid)) ) - case AvatarAction.Killed(player_guid) => + case AvatarAction.Killed(player_guid, mount_guid) => AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Killed()) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Killed(mount_guid)) ) case AvatarAction.LoadPlayer(player_guid, object_id, target_guid, cdata, pdata) => val pkt = pdata match { @@ -179,21 +179,10 @@ 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, _, target, slot, item, unk) => + case AvatarAction.PickupItem(player_guid, item, unk) => janitor forward RemoverActor.ClearSpecific(List(item), zone) AvatarEvents.publish( - 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) - } - }) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ObjectDelete(item.GUID, unk)) ) case AvatarAction.PutDownFDU(player_guid) => AvatarEvents.publish( @@ -238,6 +227,19 @@ 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 _ => ; } @@ -259,6 +261,7 @@ 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 e7fc9dab5..ace0f93be 100644 --- a/common/src/main/scala/services/avatar/AvatarServiceMessage.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceMessage.scala @@ -5,12 +5,11 @@ 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 +import net.psforever.objects.inventory.{Container, InventoryItem} 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, Vector3} +import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3} import scala.concurrent.duration.FiniteDuration @@ -36,11 +35,11 @@ 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, zone : Zone) extends Action + final case class DropItem(player_guid : PlanetSideGUID, item : Equipment) 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 - final case class Killed(player_guid : PlanetSideGUID) extends Action + final case class Killed(player_guid : PlanetSideGUID, mount_guid : Option[PlanetSideGUID]) extends Action final case class LoadPlayer(player_guid : PlanetSideGUID, object_id : Int, target_guid : PlanetSideGUID, cdata : ConstructorData, pdata : Option[ObjectCreateMessageParent]) extends Action final case class LoadProjectile(player_guid : PlanetSideGUID, object_id : Int, projectile_guid : PlanetSideGUID, cdata : ConstructorData) extends Action final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action @@ -49,7 +48,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, zone : Zone, target : PlanetSideGameObject with Container, slot : Int, item : Equipment, unk : Int = 0) extends Action + final case class PickupItem(player_guid : PlanetSideGUID, 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 @@ -64,6 +63,10 @@ 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 7920703f6..47472a618 100644 --- a/common/src/main/scala/services/avatar/AvatarServiceResponse.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala @@ -4,10 +4,11 @@ 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.{ImplantAction, ObjectCreateMessage} -import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} +import net.psforever.packet.game.ObjectCreateMessage +import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3} import services.GenericEventBusMsg final case class AvatarServiceResponse(toChannel : String, @@ -33,7 +34,7 @@ object AvatarResponse { final case class EquipmentInHand(pkt : ObjectCreateMessage) extends Response final case class GenericObjectAction(object_guid : PlanetSideGUID, action_code : Int) extends Response final case class HitHint(source_guid : PlanetSideGUID) extends Response - final case class Killed() extends Response + final case class Killed(mount_guid : Option[PlanetSideGUID]) extends Response final case class LoadPlayer(pkt : ObjectCreateMessage) extends Response final case class LoadProjectile(pkt : ObjectCreateMessage) extends Response final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response @@ -56,6 +57,10 @@ 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 d535056e0..ff664e180 100644 --- a/common/src/main/scala/services/vehicle/VehicleService.scala +++ b/common/src/main/scala/services/vehicle/VehicleService.scala @@ -141,6 +141,11 @@ 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 8d101911b..5fa5d672c 100644 --- a/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala +++ b/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala @@ -3,6 +3,7 @@ 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 @@ -47,4 +48,6 @@ 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 68834cc4b..93c16f5aa 100644 --- a/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala +++ b/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala @@ -1,6 +1,8 @@ // 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} @@ -53,4 +55,6 @@ 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 6e1502b15..6c0ff5454 100644 --- a/common/src/test/scala/objects/AvatarTest.scala +++ b/common/src/test/scala/objects/AvatarTest.scala @@ -389,7 +389,10 @@ class AvatarTest extends Specification { "the fifth slot is the locker wrapped in an EquipmentSlot" in { val (_, avatar) = CreatePlayer() - avatar.FifthSlot.Equipment.contains(avatar.Locker) + avatar.FifthSlot.Equipment match { + case Some(slot : LockerEquipment) => slot.Inventory mustEqual avatar.Locker.Inventory + case _ => ko + } } "toString" in { diff --git a/common/src/test/scala/objects/ConverterTest.scala b/common/src/test/scala/objects/ConverterTest.scala index 04a58b5f0..bd1eaaa57 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 = LockerContainer() + val obj = new LockerEquipment(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 = LockerContainer() + val obj = new LockerEquipment(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 828f5a78b..b5b1b4d68 100644 --- a/common/src/test/scala/objects/InventoryTest.scala +++ b/common/src/test/scala/objects/InventoryTest.scala @@ -525,4 +525,136 @@ 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 90338965f..a977c7c8b 100644 --- a/common/src/test/scala/objects/PlayerControlTest.scala +++ b/common/src/test/scala/objects/PlayerControlTest.scala @@ -32,10 +32,12 @@ 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 @@ -102,6 +104,7 @@ 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 @@ -167,10 +170,12 @@ 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 @@ -243,6 +248,7 @@ 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 @@ -309,10 +315,12 @@ 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 @@ -385,10 +393,12 @@ 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 @@ -430,7 +440,7 @@ class PlayerControlDeathStandingTest extends ActorTest { ) assert( msg_avatar(1) match { - case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2))) => true + case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), None)) => true case _ => false } ) @@ -493,10 +503,12 @@ 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 @@ -534,7 +546,7 @@ class PlayerControlDeathSeatedTest extends ActorTest { activityProbe.expectNoMessage(200 milliseconds) assert( msg_avatar.head match { - case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2))) => true + case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), Some(PlanetSideGUID(5)))) => true case _ => false } ) @@ -595,5 +607,4 @@ 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 69507fec2..ee9e43c63 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[LockerContainer] mustEqual true + obj.Slot(5).Equipment.get.isInstanceOf[LockerEquipment] 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 4fdd378f8..632b827e1 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[LockerContainer].Inventory += 0 -> obj_locker_ammo + obj_locker.asInstanceOf[LockerEquipment].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 0171d8d63..e60349b36 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[LockerContainer].Inventory += 0 -> obj_locker_ammo + obj_locker.asInstanceOf[LockerEquipment].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 6bddc5be7..09d8f8e3a 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[LockerContainer].Inventory += 0 -> obj_locker_ammo + obj_locker.asInstanceOf[LockerEquipment].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 3a4bf21a4..03ef7b343 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[LockerContainer].Inventory += 0 -> obj_locker_ammo + obj_locker.asInstanceOf[LockerEquipment].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/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index 1978b2ad4..2502f3e3a 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -273,7 +273,7 @@ object PsLogin { logger.info("Initializing ServiceManager") val serviceManager = ServiceManager.boot serviceManager ! ServiceManager.Register(Props[AccountIntermediaryService], "accountIntermediary") - serviceManager ! ServiceManager.Register(RandomPool(50).props(Props[TaskResolver]), "taskResolver") + serviceManager ! ServiceManager.Register(RandomPool(150).props(Props[TaskResolver]), "taskResolver") serviceManager ! ServiceManager.Register(Props[ChatService], "chat") serviceManager ! ServiceManager.Register(Props[GalaxyService], "galaxy") serviceManager ! ServiceManager.Register(Props[SquadService], "squad") diff --git a/pslogin/src/main/scala/WorldSession.scala b/pslogin/src/main/scala/WorldSession.scala new file mode 100644 index 000000000..a16f0c333 --- /dev/null +++ b/pslogin/src/main/scala/WorldSession.scala @@ -0,0 +1,603 @@ +// 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 e180a64f1..3e79fd73d 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1,6 +1,8 @@ // 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 @@ -31,6 +33,7 @@ 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 @@ -76,6 +79,7 @@ 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) @@ -109,17 +113,7 @@ 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 @@ -129,7 +123,7 @@ class WorldSessionActor extends Actor var shiftPosition : Option[Vector3] = None var shiftOrientation : Option[Vector3] = None var setupAvatarFunc : () => Unit = AvatarCreate - var beginZoningSetCurrentAvatarFunc : (Player) => Unit = SetCurrentAvatarNormally + var setCurrentAvatarFunc : (Player) => Unit = SetCurrentAvatarNormally var persist : () => Unit = NoPersistence /** * used during zone transfers to maintain reference to seated vehicle (which does not yet exist in the new zone) @@ -150,7 +144,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 `WSA`-local variable is then used to indicate the ongoing state of the LFS UI component, + * This `WorldSessionActor`-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. @@ -196,17 +190,6 @@ class WorldSessionActor extends Actor var antDischargingTick : Cancellable = Default.Cancellable var zoningTimer : Cancellable = Default.Cancellable var zoningReset : Cancellable = Default.Cancellable - /** - * 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 @@ -275,7 +258,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 @@ -892,82 +875,6 @@ 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.Name} 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.Name} can not reach it") - case None => - log.warn(s"DropItem: finding an item ($item_guid) on the ground was suggested, but ${player.Name} can not see it") - sendResponse(ObjectDeleteMessage(item_guid, 0)) - } - case Zone.Deployable.DeployableIsBuilt(obj, tool) => val index = player.Find(tool) match { case Some(x) => @@ -994,7 +901,7 @@ class WorldSessionActor extends Actor ) } else { - TryDropConstructionTool(tool, index, obj.Position) + TryDropFDU(tool, index, obj.Position) sendResponse(ObjectDeployedMessage.Failure(obj.Definition.Name)) obj.Position = Vector3.Zero obj.AssignOwnership(None) @@ -1024,11 +931,11 @@ class WorldSessionActor extends Actor val holster = player.Slot(index) if(holster.Equipment.contains(tool)) { holster.Equipment = None - taskResolver ! DelayedObjectHeld(player, index, List(PutEquipmentInSlot(player, trigger, index))) + taskResolver ! HoldNewEquipmentUp(player, taskResolver)(trigger, index) } else { //don't know where boomer trigger should go; drop it on the ground - taskResolver ! NewItemDrop(player, continent, continent.AvatarEvents)(trigger) + taskResolver ! NewItemDrop(player, continent)(trigger) } StopBundlingPackets() @@ -1067,7 +974,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) - CommonDestroyConstructionItem(tool, index) + RemoveOldEquipmentFromInventory(player, taskResolver)(tool) StopBundlingPackets() //it takes 60s for the telepad to become properly active continent.LocalEvents ! LocalServiceMessage.Telepads(RouterTelepadActivation.AddTask(obj, continent)) @@ -1089,16 +996,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") - TryDropConstructionTool(tool, index, obj.Position) + TryDropFDU(tool, index, obj.Position) obj.Position = Vector3.Zero continent.Deployables ! Zone.Deployable.Dismiss(obj) StopBundlingPackets() - //!!only dispatch Zone.Deployable.Dismiss from WSA as cleanup if the target deployable was never fully introduced + //!!only dispatch Zone.Deployable.Dismiss from WorldSessionActor 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 WSA as cleanup if the target deployable was never fully introduced + //!!only dispatch Zone.Deployable.Dismiss from WorldSessionActor as cleanup if the target deployable was never fully introduced case Zone.Deployable.DeployableIsDismissed(obj) => taskResolver ! GUIDTask.UnregisterObjectTask(obj)(continent.GUID) @@ -1446,7 +1353,13 @@ class WorldSessionActor extends Actor oldZone.AvatarEvents ! Service.Leave() oldZone.LocalEvents ! Service.Leave() oldZone.VehicleEvents ! Service.Leave() - self ! NewPlayerLoaded(player) + if(player.isAlive) { + self ! NewPlayerLoaded(player) + } + else { + zoneReload = true + cluster ! Zone.Lattice.RequestSpawnPoint(zone.Number, player, 0) + } } StopBundlingPackets() @@ -1510,13 +1423,13 @@ class WorldSessionActor extends Actor case NewPlayerLoaded(tplayer) => //new zone log.info(s"Player ${tplayer.Name} has been loaded") - player = tplayer //LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar val weaponsEnabled = (continent.Map.Name != "map11" && continent.Map.Name != "map12" && continent.Map.Name != "map13") sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100, 25, weaponsEnabled, continent.Map.Checksum)) - setupAvatarFunc() //important! the LoadMapMessage must be processed by the client before the avatar is created + //important! the LoadMapMessage must be processed by the client before the avatar is created + player = tplayer + setupAvatarFunc() turnCounter = TurnCounterDuringInterim - context.system.scheduler.scheduleOnce(delay = 2000 millisecond, self, SetCurrentAvatar(tplayer, 200)) upstreamMessageCount = 0 persist() @@ -1525,9 +1438,9 @@ class WorldSessionActor extends Actor log.info(s"Player ${tplayer.Name} will respawn") player = tplayer setupAvatarFunc() + turnCounter = TurnCounterDuringInterim upstreamMessageCount = 0 persist() - self ! SetCurrentAvatar(tplayer, 200) case PlayerFailedToLoad(tplayer) => player.Continent match { @@ -1557,14 +1470,18 @@ class WorldSessionActor extends Actor respawnTimer.cancel val waitingOnUpstream = upstreamMessageCount == 0 if(attempt >= max_attempts && waitingOnUpstream) { + log.warn(s"SetCurrentAvatar-max attempt failure: " + + s"zone=${if(zoneLoaded.contains(true)) "loaded" else if(zoneLoaded.contains(false)) "failed" else "unloaded" }," + + s"guid=${tplayer.HasGUID}, control=${(tplayer.Actor != Default.Actor)}, avatar=$waitingOnUpstream") zoneLoaded match { case None | Some(false) => - log.warn("SetCurrentAvatar: failed to load intended destination zone; routing to faction sanctuary") + log.warn("SetCurrentAvatar-max attempt failure: failed to load intended destination zone; routing to faction sanctuary") RequestSanctuaryZoneSpawn(tplayer, continent.Number) case _ => - log.warn("SetCurrentAvatar: the zone loaded but elements remain unready; restarting the process ...") + log.warn("SetCurrentAvatar-max attempt failure: the zone loaded but elements remain unready; restarting the process ...") val pos = shiftPosition.getOrElse(player.Position) val orient = shiftOrientation.getOrElse(player.Orientation) + deadState = DeadState.Release sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, pos, player.Faction, true)) val toZoneId = continent.Id tplayer.Die @@ -1583,7 +1500,7 @@ class WorldSessionActor extends Actor }) ) { if(waitingOnUpstream) { - beginZoningSetCurrentAvatarFunc(tplayer) + setCurrentAvatarFunc(tplayer) respawnTimer = context.system.scheduler.scheduleOnce( delay = (if(attempt <= max_attempts / 2) 10 else 5) seconds, self, @@ -1677,6 +1594,7 @@ class WorldSessionActor extends Actor log.info(s"LoginInfo: player $name is considered a new character") //TODO poll the database for saved zone and coordinates? persist = UpdatePersistence(sender) + deadState = DeadState.RespawnTime //the original standard sim way to load data for this user for the user's avatar and player import net.psforever.types.CertificationType._ val avatar = this.avatar @@ -1719,7 +1637,7 @@ class WorldSessionActor extends Actor // avatar.Certifications += BattleFrameRobotics // avatar.Certifications += BFRAntiInfantry // avatar.Certifications += BFRAntiAircraft - InitializeDeployableQuantities(avatar) //set deployables ui elements + Deployables.InitializeDeployableQuantities(avatar) //set deployables ui elements AwardBattleExperiencePoints(avatar, 20000000L) avatar.CEP = 600000 avatar.Implants(0).Unlocked = true @@ -1746,12 +1664,14 @@ 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 WSA to kill itself by using its own subscriptions against itself + //tell the old WorldSessionActor 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 { case (Some(a), Some(p)) if p.isAlive => //rejoin current avatar/player + log.info(s"LoginInfo: player $playerName is alive") + deadState = DeadState.Alive avatar = a player = p persist() @@ -1759,40 +1679,24 @@ class WorldSessionActor extends Actor UpdateLoginTimeThenDoClientInitialization() case (Some(a), Some(p)) => - //convert player to a corpse (unless in vehicle); go to deployment map + //convert player to a corpse (unless in vehicle); automatic recall to closest spawn point + log.info(s"LoginInfo: player $playerName is dead") + deadState = DeadState.Dead avatar = a player = p persist() player.Zone = inZone - setupAvatarFunc = AvatarDeploymentPassOver - beginZoningSetCurrentAvatarFunc = SetCurrentAvatarUponDeployment - p.Release - inZone.Population ! Zone.Population.Release(avatar) - if(p.VehicleSeated.isEmpty) { - PrepareToTurnPlayerIntoCorpse(p, inZone) - } - else { - inZone.GUID(p.VehicleSeated) match { - case Some(v : Vehicle) if v.Destroyed => - v.Actor ! Vehicle.Deconstruct( - if(v.Flying) { - //TODO gravity - None //immediate deconstruction - } - else { - v.Definition.DeconstructionTime //normal deconstruction - }) - case _ => ; - } - } + HandleReleaseAvatar(p, inZone) UpdateLoginTimeThenDoClientInitialization() case (Some(a), None) => - //respawn avatar as a new player; go to deployment map + //respawn avatar as a new player; automatic recall to closest spawn point + log.info(s"LoginInfo: player $playerName had released recently") + deadState = DeadState.RespawnTime avatar = a - player = inZone.Corpses.find(c => c.Name == playerName) match { + player = inZone.Corpses.findLast(c => c.Name == playerName) match { case Some(c) => - c + c //the last corpse of this user should be where they died case None => val tplayer = Player(a) //throwaway tplayer.Position = pos @@ -1800,8 +1704,6 @@ class WorldSessionActor extends Actor tplayer.Zone = inZone tplayer } - setupAvatarFunc = AvatarDeploymentPassOver - beginZoningSetCurrentAvatarFunc = SetCurrentAvatarUponDeployment UpdateLoginTimeThenDoClientInitialization() case _ => @@ -1810,6 +1712,12 @@ 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") } @@ -2034,7 +1942,6 @@ class WorldSessionActor extends Actor droppod.Invalidate() //now, we must short-circuit the jury-rig interstellarFerry = Some(droppod) //leverage vehicle gating player.Position = droppod.Position - continent.Population ! Zone.Population.Release(avatar) LoadZonePhysicalSpawnPoint(zone.Id, droppod.Position, Vector3.Zero, 0L) /* Don't even think about it. */ } @@ -2201,12 +2108,12 @@ class WorldSessionActor extends Actor CancelZoningProcessWithDescriptiveReason("cancel_dmg") } - case AvatarResponse.Killed() => + case AvatarResponse.Killed(mount) => val respawnTimer = 300000 //milliseconds ToggleMaxSpecialState(enable = false) zoningStatus = Zoning.Status.None deadState = DeadState.Dead - continent.GUID(player.VehicleSeated) match { + continent.GUID(mount) match { case Some(obj : Vehicle) => TotalDriverVehicleControl(obj) UnAccessContents(obj) @@ -2315,7 +2222,7 @@ class WorldSessionActor extends Actor case AvatarResponse.Release(tplayer) => if(tplayer_guid != guid) { - TurnPlayerIntoCorpse(tplayer) + DepictPlayerAsCorpse(tplayer) } case AvatarResponse.Reload(item_guid) => @@ -2346,10 +2253,155 @@ 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 @@ -2748,388 +2800,35 @@ class WorldSessionActor extends Actor */ def HandleTerminalMessage(tplayer : Player, msg : ItemTransactionMessage, order : Terminal.Exchange) : Unit = { order match { - 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) - }) + 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) } 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() => - 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.Name} 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.Name} 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.Name} 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 + SellEquipmentFromInventory(tplayer, taskResolver, tplayer, msg.terminal_guid)(Player.FreeHandSlot) case Terminal.LearnCertification(cert) => val name = tplayer.Name @@ -3138,7 +2837,7 @@ class WorldSessionActor extends Actor log.info(s"$name is learning the $cert certification for ${Certification.Cost.Of(cert)} points") avatar.Certifications += cert StartBundlingPackets() - AddToDeployableQuantities(cert, player.Certifications) + UpdateDeployableUIElements(Deployables.AddToDeployableQuantities(avatar, 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") @@ -3161,12 +2860,12 @@ class WorldSessionActor extends Actor log.info(s"$name is forgetting the $cert certification for ${Certification.Cost.Of(cert)} points") avatar.Certifications -= cert StartBundlingPackets() - RemoveFromDeployablesQuantities(cert, player.Certifications) + UpdateDeployableUIElements(Deployables.RemoveFromDeployableQuantities(avatar, 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 - RemoveFromDeployablesQuantities(entry, player.Certifications) + UpdateDeployableUIElements(Deployables.RemoveFromDeployableQuantities(avatar, entry, player.Certifications)) sendResponse(PlanetsideAttributeMessage(guid, 25, entry.id)) }) StopBundlingPackets() @@ -3261,11 +2960,20 @@ class WorldSessionActor extends Actor case Terminal.BuyVehicle(vehicle, weapons, trunk) => continent.Map.TerminalToSpawnPad.get(msg.terminal_guid.guid) match { case Some(pad_guid) => - 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 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 toFaction = tplayer.Faction val pad = continent.GUID(pad_guid).get.asInstanceOf[VehicleSpawnPad] vehicle.Faction = toFaction @@ -3289,7 +2997,7 @@ class WorldSessionActor extends Actor vTrunk.Clear() trunk.foreach(entry => { entry.obj.Faction = toFaction - vTrunk += entry.start -> entry.obj + vTrunk.InsertQuickly(entry.start, entry.obj) }) taskResolver ! RegisterVehicleFromSpawnPad(vehicle, pad) sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) @@ -3303,7 +3011,7 @@ class WorldSessionActor extends Actor } lastTerminalOrderFulfillment = true - case _ => + case Terminal.NoDeal() => val order : String = if(msg == null) { s"order $msg" } @@ -3313,6 +3021,8 @@ 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 _ => ; } } @@ -3524,6 +3234,36 @@ 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 _ => ; } } @@ -3668,14 +3408,14 @@ class WorldSessionActor extends Actor progressBarValue match { case Some(value) => val next = value + delta - if(value >= 100f) { + if (value >= 100f) { //complete progressBarValue = None tickAction(100) completionAction() } - else if(value < 100f && next >= 100f) { - if(tickAction(99)) { + else if (value < 100f && next >= 100f) { + if (tickAction(99)) { //will complete after this turn progressBarValue = Some(next) import scala.concurrent.ExecutionContext.Implicits.global @@ -3688,7 +3428,7 @@ class WorldSessionActor extends Actor } } else { - if(tickAction(next)) { + if (tickAction(next)) { //normal progress activity progressBarValue = Some(next) import scala.concurrent.ExecutionContext.Implicits.global @@ -3710,14 +3450,17 @@ class WorldSessionActor extends Actor * @param tplayer the target player */ def HandleSetCurrentAvatar(tplayer : Player) : Unit = { + log.info(s"HandleSetCurrentAvatar - ${tplayer.Name}") player = tplayer val guid = tplayer.GUID StartBundlingPackets() - InitializeDeployableUIElements(avatar) + UpdateDeployableUIElements(Deployables.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? - sendResponse(PlayerStateShiftMessage(ShiftState(1, shiftPosition.getOrElse(tplayer.Position), shiftOrientation.getOrElse(tplayer.Orientation).z))) + val pos = player.Position = shiftPosition.getOrElse(tplayer.Position) + val orient = player.Orientation = shiftOrientation.getOrElse(tplayer.Orientation) + sendResponse(PlayerStateShiftMessage(ShiftState(1, pos, orient.z))) shiftPosition = None shiftOrientation = None if(player.spectator) { @@ -3759,6 +3502,7 @@ class WorldSessionActor extends Actor log.warn(s"HandleSetCurrentAvatar: unknown loadout information $data in vehicle list") } sendResponse(SetChatFilterMessage(ChatChannel.Platoon, false, ChatChannel.values.toList)) //TODO will not always be "on" like this + val originalDeadState = deadState deadState = DeadState.Alive sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true)) //looking for squad (members) @@ -3829,8 +3573,8 @@ class WorldSessionActor extends Actor if(noSpawnPointHere) { RequestSanctuaryZoneSpawn(player, continent.Number) } - else if(player.Health == 0) { - //player died during setup; probably a relog + else if(originalDeadState == DeadState.Dead || player.Health == 0) { + //killed during spawn setup or possibly a relog into a corpse (by accident?) player.Actor ! Player.Die() } upstreamMessageCount = 0 @@ -3845,22 +3589,6 @@ class WorldSessionActor extends Actor HandleSetCurrentAvatar(tplayer) } - /** - * An interruption of the normal procedure - - * "instruct the client to treat this player as the avatar" - - * in order to locate a spawn point for this player. - * After a spawn point is located, the actual avatar designation will be made. - * @see `beginZoningSetCurrentAvatarFunc` - * @see `SetCurrentAvatarNormally` - * @see `Zone.Lattice.RequestSpawnPoint` - * @param tplayer the target player - */ - def SetCurrentAvatarUponDeployment(tplayer : Player) : Unit = { - beginZoningSetCurrentAvatarFunc = SetCurrentAvatarNormally - upstreamMessageCount = 0 - continent.Actor ! Zone.Lattice.RequestSpawnPoint(continent.Number, tplayer, 0) - } - /** * These messages are dispatched when first starting up the client and connecting to the server for the first time. * While many of thee messages will be reused for other situations, they appear in this order only during startup. @@ -4106,9 +3834,10 @@ 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(2) = faction + "hev_antipersonnel" - whenUsedLastMAXName(3) = faction + "hev_antivehicular" - whenUsedLastMAXName(1) = faction + "hev_antiaircraft" + whenUsedLastMAXName(0) = faction + "hev" + whenUsedLastMAXName(1) = faction + "hev_antipersonnel" + whenUsedLastMAXName(2) = faction + "hev_antivehicular" + whenUsedLastMAXName(3) = faction + "hev_antiaircraft" avatar.FirstTimeEvents = ftes accountPersistence ! AccountPersistenceService.Login(lName) case _ => @@ -4252,7 +3981,7 @@ class WorldSessionActor extends Actor }) //load corpses in zone continent.Corpses.foreach { - TurnPlayerIntoCorpse + DepictPlayerAsCorpse } //load vehicles in zone (put separate the one we may be using) val (wreckages, (vehicles, usedVehicle)) = { @@ -4607,28 +4336,7 @@ 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 - sendResponse(ObjectDeleteMessage(player_guid, 0)) - GetMountableAndSeat(None, player) match { - case (Some(obj), Some(seatNum)) => - obj.Seats(seatNum).Occupant = None - obj match { - case v : Vehicle if seatNum == 0 && v.Flying => - TotalDriverVehicleControl(v) - UnAccessContents(v) - v.Actor ! Vehicle.Deconstruct() - case _ => ; - } - case _ => ; //found no vehicle where one was expected; since we're dead, let's not dwell on it - } - taskResolver ! GUIDTask.UnregisterPlayer(player)(continent.GUID) - } + HandleReleaseAvatar(player, continent) case msg@SpawnRequestMessage(u1, spawn_type, u3, u4, zone_number) => log.info(s"SpawnRequestMessage: $msg") @@ -5286,7 +4994,13 @@ class WorldSessionActor extends Actor player.FreeHand.Equipment match { case Some(item) => if(item.GUID == item_guid) { - continent.Ground ! Zone.Ground.DropItem(item, player.Position, player.Orientation) + CancelZoningProcessWithDescriptiveReason("cancel_use") + continent.GUID(player.VehicleSeated) match { + case Some(_) => + RemoveOldEquipmentFromInventory(player, taskResolver)(item) + case None => + DropEquipmentFromInventory(player)(item) + } } case None => log.warn(s"DropItem: ${player.Name} wanted to drop a $anItem, but it wasn't at hand") @@ -5304,7 +5018,8 @@ class WorldSessionActor extends Actor case Some(item : Equipment) => player.Fit(item) match { case Some(_) => - continent.Ground ! Zone.Ground.PickupItem(item_guid) + CancelZoningProcessWithDescriptiveReason("cancel_use") + PickUpEquipmentFromGround(player)(item) case None => //skip sendResponse(ActionResultMessage.Fail(16)) //error code? } @@ -5325,25 +5040,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) : ((Int, AmmoBox) => Unit, (AmmoBox, Int) => Unit) = obj match { + val (deleteFunc, modifyFunc) : (Equipment=>Future[Any], (AmmoBox, Int) => Unit) = obj match { case (veh : Vehicle) => - (DeleteEquipmentFromVehicle(veh), ModifyAmmunitionInVehicle(veh)) + (RemoveOldEquipmentFromInventory(veh, taskResolver), ModifyAmmunitionInVehicle(veh)) + case o : PlanetSideServerObject with Container => + (RemoveOldEquipmentFromInventory(o, taskResolver), ModifyAmmunition(o)) case _ => - (DeleteEquipment(obj), ModifyAmmunition(obj)) + throw new Exception("ReloadMessage: should be a server object, not a regular game object") } - xs.foreach(item => { - deleteFunc(item.start, item.obj.asInstanceOf[AmmoBox]) - }) + xs.foreach { item => deleteFunc(item.obj) } val box = x.obj.asInstanceOf[AmmoBox] val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { - xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) + xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum } val sumReloadValue : Int = box.Capacity + tailReloadValue val actualReloadValue = (if(sumReloadValue <= reloadValue) { - deleteFunc(x.start, box) + deleteFunc(box) sumReloadValue } else { @@ -5535,50 +5250,9 @@ 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), 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") - } + (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) case (None, _, _) => log.error(s"MoveItem: wanted to move $item_guid from $source_guid, but could not find source object") case (_, None, _) => @@ -5591,34 +5265,27 @@ class WorldSessionActor extends Actor case msg@LootItemMessage(item_guid, target_guid) => log.info(s"LootItem: $msg") - (ValidObject(item_guid), ValidObject(target_guid)) match { - case (Some(item : Equipment), Some(target : Container)) => + (ValidObject(item_guid), continent.GUID(target_guid)) match { + case (Some(item : Equipment), Some(destination : PlanetSideServerObject with Container)) => //figure out the source ( { - val findFunc : PlanetSideGameObject with Container => Option[(PlanetSideGameObject with Container, Option[Int])] = FindInLocalContainer(item_guid) + val findFunc : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] = FindInLocalContainer(item_guid) findFunc(player.Locker) .orElse(findFunc(player)) .orElse(accessedContainer match { - case Some(parent) => + case Some(parent : PlanetSideServerObject) => findFunc(parent) - case None => + case _ => None } ) - }, 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") - } + }, destination.Fit(item)) match { + case (Some((source, Some(_))), Some(dest)) => + source.Actor ! Containable.MoveItem(destination, item, dest) 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 $target") + log.error(s"LootItem: can not find somwhere to put $item in $destination") case _ => log.error(s"LootItem: wanted to move $item_guid to $target_guid, but multiple problems were encountered") } @@ -5693,88 +5360,86 @@ class WorldSessionActor extends Actor else if(!unk3 && player.isAlive) { //potential kit use ValidObject(item_used_guid) match { case Some(kit : Kit) => - 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(System.currentTimeMillis - whenUsedLastKit < 5000) { - sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^${5 - (System.currentTimeMillis - whenUsedLastKit) / 1000}~", None)) - } - else { - 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") + 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).toDouble) / 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 + } + } + 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 { + log.warn(s"UseItem: $kit behavior not supported") + false + } + + 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 Some(item) => log.warn(s"UseItem: looking for Kit to use, but found $item instead") case None => @@ -5908,7 +5573,6 @@ class WorldSessionActor extends Actor CancelZoningProcessWithDescriptiveReason("cancel_use") PlayerActionsToCancel() CancelAllProximityUnits() - continent.Population ! Zone.Population.Release(avatar) GoToDeploymentMap() case _ => ; } @@ -6505,7 +6169,7 @@ class WorldSessionActor extends Actor case (None, _) => ; log.warn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle") case (_, None) => ; - log.warn(s"DismountVehicleMsg: player ${player.Name}_guid could not be found to kick") + log.warn(s"DismountVehicleMsg: player $player_guid could not be found to kick") case _ => log.warn(s"DismountVehicleMsg: object is either not a Mountable or not a Player") } @@ -6667,151 +6331,6 @@ 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. @@ -6824,6 +6343,8 @@ 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 @@ -6836,11 +6357,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 WSA + localAnnounce ! NewPlayerLoaded(localPlayer) //alerts WorldSessionActor } override def onFailure(ex : Throwable) : Unit = { - localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WSA + localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WorldSessionActor } }, List(GUIDTask.RegisterAvatar(tplayer)(continent.GUID)) ) @@ -6858,6 +6379,8 @@ 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 @@ -6870,11 +6393,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 WSA + localAnnounce ! PlayerLoaded(localPlayer) //alerts WorldSessionActor } override def onFailure(ex : Throwable) : Unit = { - localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WSA + localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WorldSessionActor } }, List(GUIDTask.RegisterPlayer(tplayer)(continent.GUID)) ) @@ -6892,6 +6415,8 @@ 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 @@ -6942,6 +6467,8 @@ 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 @@ -6982,6 +6509,8 @@ 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 @@ -7005,6 +6534,8 @@ 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 @@ -7017,12 +6548,12 @@ class WorldSessionActor extends Actor def Execute(resolver : ActorRef) : Unit = { localDriver.VehicleSeated = localVehicle.GUID Vehicles.Own(localVehicle, localDriver) - localAnnounce ! NewPlayerLoaded(localDriver) //alerts WSA + localAnnounce ! NewPlayerLoaded(localDriver) //alerts WorldSessionActor resolver ! scala.util.Success(this) } override def onFailure(ex : Throwable) : Unit = { - localAnnounce ! PlayerFailedToLoad(localDriver) //alerts WSA + localAnnounce ! PlayerFailedToLoad(localDriver) //alerts WorldSessionActor } }, List(GUIDTask.RegisterAvatar(driver)(continent.GUID), GUIDTask.RegisterVehicle(obj)(continent.GUID))) } @@ -7041,6 +6572,8 @@ 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 @@ -7064,6 +6597,8 @@ 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 @@ -7093,6 +6628,8 @@ 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 @@ -7110,48 +6647,6 @@ 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. @@ -7176,45 +6671,6 @@ 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 @@ -7225,10 +6681,13 @@ 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 = { @@ -7243,9 +6702,12 @@ 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) @@ -7479,105 +6941,6 @@ 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`. @@ -7597,37 +6960,6 @@ 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. @@ -7659,251 +6991,6 @@ 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 @@ -7918,21 +7005,23 @@ class WorldSessionActor extends Actor FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match { case Nil => ; case x :: xs => - val (deleteFunc, modifyFunc) : ((Int, AmmoBox)=>Unit, (AmmoBox, Int)=>Unit) = obj match { + val (deleteFunc, modifyFunc) : (Equipment=>Future[Any], (AmmoBox, Int) => Unit) = obj match { case (veh : Vehicle) => - (DeleteEquipmentFromVehicle(veh), ModifyAmmunitionInVehicle(veh)) + (RemoveOldEquipmentFromInventory(veh, taskResolver), ModifyAmmunitionInVehicle(veh)) + case o : PlanetSideServerObject with Container => + (RemoveOldEquipmentFromInventory(o, taskResolver), ModifyAmmunition(o)) case _ => - (DeleteEquipment(obj), ModifyAmmunition(obj)) + throw new Exception("PerformToolAmmoChange: (remove/modify) should be a server object, not a regular game object") } - val (stowFuncTask, stowFunc) : ((Int, AmmoBox)=>TaskResolver.GiveTask, (Int, AmmoBox)=>Unit) = obj match { - case (veh : Vehicle) => - (StowNewEquipmentInVehicle(veh), StowEquipmentInVehicles(veh)) + val (stowNewFunc, stowFunc) : (Equipment=>TaskResolver.GiveTask, Equipment=>Future[Any]) = obj match { + case o : PlanetSideServerObject with Container => + (PutNewEquipmentInInventoryOrDrop(o), PutEquipmentInInventoryOrDrop(o)) case _ => - (StowNewEquipment(obj), StowEquipment(obj)) + throw new Exception("PerformToolAmmoChange: (new/put) should be a server object, not a regular game object") } xs.foreach(item => { obj.Inventory -= x.start - deleteFunc(item.start, item.obj.asInstanceOf[AmmoBox]) + deleteFunc(item.obj) }) //box will be the replacement ammo; give it the discovered magazine and load it into the weapon @ 0 @@ -7964,8 +7053,7 @@ 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) - obj.Inventory += x.start -> boxForInventory //block early; assumption warning: swappable ammo types have the same icon size - taskResolver ! stowFuncTask(x.start, boxForInventory) + taskResolver ! stowNewFunc(boxForInventory) fullMagazine }) sendResponse(InventoryStateMessage(box.GUID, tool.GUID, box.Capacity)) //should work for both players and vehicles @@ -7999,25 +7087,16 @@ 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(index) => - stowFunc(index, previousBox) + case Some(_) => + stowFunc(previousBox) case None => - NormalItemDrop(player, continent, continent.AvatarEvents)(previousBox) + NormalItemDrop(player, continent)(previousBox) } - val dropFunc : (Equipment)=>TaskResolver.GiveTask = NewItemDrop(player, continent, continent.AvatarEvents) AmmoBox.Split(previousBox) match { - case Nil | _ :: Nil => ; //done (the former case is technically not possible) + case Nil | List(_) => ; //done (the former case is technically not possible) case _ :: xs => modifyFunc(previousBox, 0) //update to changed capacity value - 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) - } - }) + xs.foreach(box => { taskResolver ! stowNewFunc(box) }) } } else { @@ -8038,13 +7117,10 @@ 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 : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : Unit = { - continent.Ground ! Zone.Ground.DropItem(item, obj.Position, Vector3.z(obj.Orientation.z)) + 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) } /** @@ -8054,16 +7130,15 @@ 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 : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : TaskResolver.GiveTask = { + def NewItemDrop(obj : PlanetSideServerObject with Container, zone : Zone)(item : Equipment) : TaskResolver.GiveTask = { TaskResolver.GiveTask( new Task() { private val localItem = item - private val localFunc : (Equipment)=>Unit = NormalItemDrop(obj, zone, service) + private val localFunc : (Equipment)=>Unit = NormalItemDrop(obj, zone) + + override def Description : String = s"dropping a new ${localItem.Definition.Name} on the ground" def Execute(resolver : ActorRef) : Unit = { localFunc(localItem) @@ -8078,21 +7153,21 @@ class WorldSessionActor extends Actor * @param tool a weapon */ def FireCycleCleanup(tool : Tool) : Unit = { - //TODO this is temporary and will be replaced by more appropriate functionality in the future. + //TODO 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") - taskResolver ! RemoveEquipmentFromSlot(player, tool, player.Find(tool).get) + RemoveOldEquipmentFromInventory(player, taskResolver)(tool) 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) { - taskResolver ! RemoveEquipmentFromSlot(player, x.obj, x.start) + RemoveOldEquipmentFromInventory(player, taskResolver)(x.obj) sumReloadValue } else { @@ -8101,36 +7176,14 @@ 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 => { - taskResolver ! RemoveEquipmentFromSlot(player, item.obj, item.start) - }) + xs.foreach(item => { RemoveOldEquipmentFromInventory(player, taskResolver)(item.obj) }) } } else if(tdef == GlobalDefinitions.phoenix) { - taskResolver ! RemoveEquipmentFromSlot(player, tool, player.Find(tool).get) + RemoveOldEquipmentFromInventory(player, taskResolver)(tool) } } - /** - * 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 @@ -8139,7 +7192,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 : PlanetSideGameObject with Container) : Option[(PlanetSideGameObject with Container, Option[Int])] = { + def FindInLocalContainer(object_guid : PlanetSideGUID)(parent : PlanetSideServerObject with Container) : Option[(PlanetSideServerObject with Container, Option[Int])] = { val slot : Option[Int] = parent.Find(object_guid) slot match { case place @ Some(_) => @@ -8183,14 +7236,14 @@ class WorldSessionActor extends Actor else if(vehicle.Definition == GlobalDefinitions.ant) { state match { case DriveState.Deployed => - // We only want this WSA (not other player's WSA) to manage timers + // We only want this WorldSessionActor (not other player's WorldSessionActor) 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 WSA (not other player's WSA) to manage timers + // We only want this WorldSessionActor (not other player's WorldSessionActor) to manage timers if(vehicle.Seats(0).Occupant.contains(player)){ antChargingTick.cancel() // Stop charging NTU if charging } @@ -8506,7 +7559,7 @@ class WorldSessionActor extends Actor } value = start_num + deciseconds_remaining sendResponse(PlanetsideAttributeMessage(target_guid, 20, value)) - GetMountableAndSeat(None, player) match { + GetMountableAndSeat(None, player, continent) match { case (Some(mountable : Amenity), Some(seat)) if mountable.Owner.GUID == capture_terminal.Owner.GUID => mountable.Seats(seat).Occupant = None player.VehicleSeated = None @@ -8580,7 +7633,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 `WSA`-current `Player` to the current zone and sends out the expected packets.
+ * It adds the `WorldSessionActor`-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, @@ -8595,7 +7648,7 @@ class WorldSessionActor extends Actor * @see `GetKnownVehicleAndSeat` * @see `LoadZoneTransferPassengerMessages` * @see `Player.Spawn` - * @see `ReloadUsedLastCoolDownTimes` + * @see `ReloadItemCoolDownTimes` * @see `Vehicles.Own` * @see `Vehicles.ReloadAccessPermissions` */ @@ -8682,19 +7735,9 @@ class WorldSessionActor extends Actor continent.Population ! Zone.Population.Spawn(avatar, player) //cautious redundancy deadState = DeadState.Alive - 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)) - } - } + ReloadItemCoolDownTimes() + //begin looking for conditions to set the avatar + context.system.scheduler.scheduleOnce(delay = 250 millisecond, self, SetCurrentAvatar(player, 200)) } /** @@ -8707,8 +7750,8 @@ class WorldSessionActor extends Actor * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; * `(None, None)`, otherwise (even if the vehicle can be determined) */ - def GetMountableAndSeat(direct : Option[PlanetSideGameObject with Mountable], occupant : Player) : (Option[PlanetSideGameObject with Mountable], Option[Int]) = - direct.orElse(continent.GUID(occupant.VehicleSeated)) match { + def GetMountableAndSeat(direct : Option[PlanetSideGameObject with Mountable], occupant : Player, zone : Zone) : (Option[PlanetSideGameObject with Mountable], Option[Int]) = + direct.orElse(zone.GUID(occupant.VehicleSeated)) match { case Some(obj : PlanetSideGameObject with Mountable) => obj.PassengerInSeat(occupant) match { case index @ Some(_) => @@ -8734,7 +7777,7 @@ class WorldSessionActor extends Actor * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; * `(None, None)`, otherwise (even if the vehicle can be determined) */ - def GetKnownVehicleAndSeat() : (Option[Vehicle], Option[Int]) = GetMountableAndSeat(interstellarFerry, player) match { + def GetKnownVehicleAndSeat() : (Option[Vehicle], Option[Int]) = GetMountableAndSeat(interstellarFerry, player, continent) match { case (Some(v : Vehicle), Some(seat)) => (Some(v), Some(seat)) case _ => (None, None) } @@ -8746,7 +7789,7 @@ class WorldSessionActor extends Actor * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; * `(None, None)`, otherwise (even if the vehicle can be determined) */ - def GetVehicleAndSeat() : (Option[Vehicle], Option[Int]) = GetMountableAndSeat(None, player) match { + def GetVehicleAndSeat() : (Option[Vehicle], Option[Int]) = GetMountableAndSeat(None, player, continent) match { case (Some(v : Vehicle), Some(seat)) => (Some(v), Some(seat)) case _ => (None, None) } @@ -8827,7 +7870,7 @@ class WorldSessionActor extends Actor * @see `ObjectAttachMessage` * @see `ObjectCreateMessage` * @see `PlayerInfo.LoginInfo` - * @see `ReloadUsedLastCoolDownTimes` + * @see `ReloadItemCoolDownTimes` * @see `UpdateWeaponAtSeatPosition` * @see `Vehicles.ReloadAccessPermissions` */ @@ -8858,10 +7901,10 @@ class WorldSessionActor extends Actor player.VehicleSeated = vguid sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata)) sendResponse(ObjectAttachMessage(vguid, pguid, seat)) - //log.info(s"AvatarCreateInVehicle: $pguid -> $pdata") + //log.info(s"AvatarRejoin: $vguid -> $vdata") AccessContents(vehicle) UpdateWeaponAtSeatPosition(vehicle, seat) - //log.trace(s"AvatarCreateInVehicle: ${player.Name} in ${vehicle.Definition.Name}") + log.info(s"AvatarRejoin: ${player.Name} in ${vehicle.Definition.Name}") case _ => player.VehicleSeated = None @@ -8869,28 +7912,15 @@ class WorldSessionActor extends Actor val data = packet.DetailedConstructorData(player).get val guid = player.GUID sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, guid, data)) - //log.info(s"AvatarCreate: $guid -> $data") - //log.trace(s"AvatarCreate: ${player.Name}") + //log.info(s"AvatarRejoin: $guid -> $data") + log.trace(s"AvatarRejoin: ${player.Name}") } //cautious redundancy deadState = DeadState.Alive - ReloadUsedLastCoolDownTimes() - setupAvatarFunc = AvatarCreate - } - - /** - * A part of the process of spawning the player into the game world - * in the case of a restored game connection (relogging). - * Rather than create any avatar here, the process has been skipped for now - * and will be handled by a different operation - * and this routine's normal operation when it revisits the same code. - * @see `avatarSetupFunc` - * @see `AvatarCreate` - * @see `ReloadUsedLastCoolDownTimes` - */ - def AvatarDeploymentPassOver() : Unit = { - ReloadUsedLastCoolDownTimes() + ReloadItemCoolDownTimes() setupAvatarFunc = AvatarCreate + //begin looking for conditions to set the avatar + context.system.scheduler.scheduleOnce(delay = 750 millisecond, self, SetCurrentAvatar(player, 200)) } /** @@ -8898,16 +7928,31 @@ class WorldSessionActor extends Actor * This is called "skill". * @see `AvatarVehicleTimerMessage` */ - 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)) + 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)) } } - for (i <- 1 to 3) { - if (lTime - whenUsedLastMAX(i) < 300000) { - sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastMAXName(i), 300 - ((lTime - whenUsedLastMAX(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)) } } } @@ -8934,18 +7979,16 @@ class WorldSessionActor extends Actor * @param obj the player to be turned into a corpse */ def FriskDeadBody(obj : Player) : Unit = { - if(obj.isBackpack) { + if(!obj.isAlive) { obj.Slot(4).Equipment match { case None => ; case Some(knife) => - obj.Slot(4).Equipment = None - taskResolver ! RemoveEquipmentFromSlot(obj, knife, 4) + RemoveOldEquipmentFromInventory(obj, taskResolver)(knife) } obj.Slot(0).Equipment match { case Some(arms : Tool) => if(GlobalDefinitions.isMaxArms(arms.Definition)) { - obj.Slot(0).Equipment = None - taskResolver ! RemoveEquipmentFromSlot(obj, arms, 0) + RemoveOldEquipmentFromInventory(obj, taskResolver)(arms) } case _ => ; } @@ -8960,7 +8003,7 @@ class WorldSessionActor extends Actor } }) val triggers = RemoveBoomerTriggersFromInventory() - triggers.foreach(trigger => { NormalItemDrop(obj, continent, continent.AvatarEvents)(trigger) }) + triggers.foreach(trigger => { NormalItemDrop(obj, continent)(trigger) }) } } @@ -8980,15 +8023,15 @@ class WorldSessionActor extends Actor * @param tplayer the player */ def PrepareToTurnPlayerIntoCorpse(tplayer : Player, zone : Zone) : Unit = { + tplayer.Release FriskDeadBody(tplayer) if(!WellLootedDeadBody(tplayer)) { - TurnPlayerIntoCorpse(tplayer) - zone.Population ! Zone.Corpse.Add(tplayer) - zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.Release(tplayer, zone)) + TurnPlayerIntoCorpse(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) @@ -9001,7 +8044,20 @@ class WorldSessionActor extends Actor * @see `CorpseConverter.converter` * @param tplayer the player */ - def TurnPlayerIntoCorpse(tplayer : Player) : Unit = { + def TurnPlayerIntoCorpse(tplayer : Player, zone : Zone) : Unit = { + tplayer.Release + DepictPlayerAsCorpse(tplayer) + zone.Population ! Zone.Corpse.Add(tplayer) + zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.Release(tplayer, zone)) + } + + /** + * Creates a player that has the characteristics of a corpse. + * To the game, that is a backpack (or some pastry, festive graphical modification allowing). + * @see `CorpseConverter.converter` + * @param tplayer the player + */ + def DepictPlayerAsCorpse(tplayer : Player) : Unit = { val guid = tplayer.GUID sendResponse( ObjectCreateDetailedMessage(ObjectClass.avatar, guid, CorpseConverter.converter.DetailedConstructorData(tplayer).get) @@ -9015,7 +8071,7 @@ class WorldSessionActor extends Actor * `false`, otherwise */ def WellLootedDeadBody(obj : Player) : Boolean = { - obj.isBackpack && obj.Holsters().count(_.Equipment.nonEmpty) == 0 && obj.Inventory.Size == 0 + !obj.isAlive && obj.Holsters().count(_.Equipment.nonEmpty) == 0 && obj.Inventory.Size == 0 } /** @@ -9025,7 +8081,7 @@ class WorldSessionActor extends Actor * `false`, otherwise */ def TryDisposeOfLootedCorpse(obj : Player) : Boolean = { - if(WellLootedDeadBody(obj)) { + if(obj.isBackpack && WellLootedDeadBody(obj)) { continent.AvatarEvents ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), continent)) true } @@ -9447,7 +8503,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 `WSA`) + //damage is synchronized on `LSA` (results returned to and distributed from this `WorldSessionActor`) continent.LocalEvents ! Vitality.DamageOn(obj, func) case _ => ; } @@ -9480,53 +8536,11 @@ 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 cooresponding certification were not `learn`ed. + * Until initialized, all elements will be RED 0/0 as if the corresponding 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. @@ -9754,10 +8768,9 @@ class WorldSessionActor extends Actor * @param index the slot index * @param pos where to drop the object in the game world */ - 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) + def TryDropFDU(tool : ConstructionItem, index : Int, pos : Vector3) : Unit = { + if(tool.Definition == GlobalDefinitions.advanced_ace) { + DropEquipmentFromInventory(player)(tool, Some(pos)) } } @@ -9885,27 +8898,27 @@ class WorldSessionActor extends Actor * `false`, otherwise */ def FindEquipmentToDelete(object_guid : PlanetSideGUID, obj : Equipment) : Boolean = { - val findFunc : PlanetSideGameObject with Container => Option[(PlanetSideGameObject with Container, Option[Int])] = + val findFunc : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] = FindInLocalContainer(object_guid) findFunc(player.Locker) .orElse(findFunc(player)) .orElse(accessedContainer match { - case Some(parent) => + case Some(parent : PlanetSideServerObject) => findFunc(parent) - case None => + case _ => None }) .orElse(FindLocalVehicle match { - case Some(parent) => + case Some(parent : PlanetSideServerObject) => findFunc(parent) - case None => + case _ => None }) match { case Some((parent, Some(slot))) => obj.Position = Vector3.Zero - taskResolver ! RemoveEquipmentFromSlot(parent, obj, slot) + RemoveOldEquipmentFromInventory(parent, taskResolver)(obj) log.info(s"RequestDestroy: equipment $obj") true @@ -9914,7 +8927,6 @@ 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 } @@ -10050,7 +9062,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 + case _ if player.HasGUID => //player is deconstructing self or instant action val player_guid = player.GUID sendResponse(ObjectDeleteMessage(player_guid, 4)) continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 4)) @@ -10326,72 +9338,6 @@ 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, @@ -11231,8 +10177,7 @@ class WorldSessionActor extends Actor * @see `Player.Release` */ def GoToDeploymentMap() : Unit = { - player.Release - deadState = DeadState.Release + deadState = DeadState.Release //we may be alive or dead, may or may not be a corpse sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true)) DrawCurrentAmsSpawnPoint() } @@ -11511,6 +10456,24 @@ class WorldSessionActor extends Actor } } + /** + * na + * @param tplayer na + * @param zone na + */ + def HandleReleaseAvatar(tplayer : Player, zone : Zone) : Unit = { + tplayer.Release + tplayer.VehicleSeated match { + case None => + PrepareToTurnPlayerIntoCorpse(tplayer, zone) + case Some(_) => + tplayer.VehicleSeated = None + zone.Population ! Zone.Population.Release(avatar) + sendResponse(ObjectDeleteMessage(tplayer.GUID, 0)) + taskResolver ! GUIDTask.UnregisterPlayer(tplayer)(zone.GUID) + } + } + /** * The upstream counter accumulates when the server receives sp[ecific messages from the client. * It counts upwards until it reach maximum value, and then starts over. @@ -11657,6 +10620,57 @@ 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, //5min + 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 afa314daf..801f44edd 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, Zone.Nowhere)) + service ! AvatarServiceMessage("test", AvatarAction.DropItem(PlanetSideGUID(10), tool)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.DropItem(pkt))) } } @@ -264,41 +264,16 @@ class PlayerStateTest 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 { +class PickupItemTest 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 as ObjectDelete (not visible inventory space)" in { + "pass PickUpItem" 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, 6, tool)) + service ! AvatarServiceMessage("test", AvatarAction.PickupItem(PlanetSideGUID(10), tool)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectDelete(tool.GUID, 0))) } }