diff --git a/build.sbt b/build.sbt
index 967ab192..b825a016 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 02c83a26..451d85ff 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 a241e978..25b2a69f 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 c8541002..572b4803 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 7793a6a5..02ab4471 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 4144415d..7da2c79d 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 486265c7..9260da45 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 00356be4..8e5c6a97 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 fa455aa3..b5027cd5 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 38fe6984..ba24b1d8 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 f62d5ec3..9f9b5c2e 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 3443ad96..9107d2a9 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 625adc77..9b4e83e3 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 557bfece..ff42d512 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 bc08949a..2038d5aa 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 64668c4e..0f2ad13f 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 00000000..f6b9fe24
--- /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 a4bb0e78..dac5366f 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 b493477a..525ed0ae 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 10d01760..cb2a2885 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 2d4d7e6f..80c3dbe1 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 7d41516d..41d375c9 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 f6bae4bb..d6225590 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 93faada0..3f33c4b6 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 e7fc9dab..ace0f93b 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 7920703f..47472a61 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 d535056e..ff664e18 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 8d101911..5fa5d672 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 68834cc4..93c16f5a 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 6e1502b1..6c0ff545 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 04a58b5f..bd1eaaa5 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 828f5a78..b5b1b4d6 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 90338965..a977c7c8 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 69507fec..ee9e43c6 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 4fdd378f..632b827e 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 0171d8d6..e60349b3 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 6bddc5be..09d8f8e3 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 3a4bf21a..03ef7b34 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 1978b2ad..2502f3e3 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 00000000..a16f0c33
--- /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 e180a64f..3e79fd73 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 afa314da..801f44ed 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)))
}
}