Let's Move Item, Together (#429)

* mix-in code for akka messaging move item, currently testing on infantry only

* adjusted structure of COntainable so callbacks are separate from message-producing functions, are separate from message-sending functionality; massaged LockerContainer until it could support control agency and created a wrapper for its Equipment interfacing; the control structure starts and stops when PlayerControl starts and stops, and it converts whenever necessary

* added failsafe conditions to Containable, such as blocking certain messages while completing a MoveItem call, or blocking all messages to reset disruptive MoveItem calls; depiction message callbacks for Player, Locker, and Vehicle, to properly depict the manipulation of items; eliminated the old code from WSA

* added useful comments to Containable; moved functionality for deployables, and for container'ing, and dropping logic out from WSA and distributed it appropriately

* handling terminal operations - buying an exosuit and selecting an infantry loadout; starting work on support for more persistent equipment timers local to the avatar (that were removed in this update; see wsa changes)

* linked terminal page/message with routing policy

* tuning vehicle loadout management and display

* separated use time from purchase time and applied a system that limits either if that same event would recur too soon; tuning exosuit and loadout changes

* some ask timeout handling and comments

* normalizing item on ground interactions

* rearranging the project structure

* merged with master; commas removed

* fixing tests

* added description strings to Tasks; adjusted the completion conditions for some Tasks

* a failed purchase will not block future purchases; increased timeout on move-item tasks

* corpses, even one's own, should have properly moveable inventories

* for better persistence, until GlobalDefinitions is renovated, moved the object id->name map onto the avatar object, for the purpose of timers; replaced a use of values in GridInventory for a map conversion

* max loadouts and max exosuit switch use same cooldown now; hopefully better clarifcation regarding held arm position
This commit is contained in:
Fate-JH 2020-05-16 12:18:08 -04:00 committed by GitHub
parent fdb4836fdf
commit 3f2240947b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 3101 additions and 1735 deletions

View file

@ -48,8 +48,8 @@ lazy val commonSettings = Seq(
"org.scala-graph" %% "graph-core" % "1.12.5",
"io.kamon" %% "kamon-bundle" % "2.1.0",
"io.kamon" %% "kamon-apm-reporter" % "2.1.0",
"org.json4s" %% "json4s-native" % "3.6.8",
),
"org.json4s" %% "json4s-native" % "3.6.8"
)
)
lazy val pscryptoSettings = Seq(

View file

@ -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<br>
* 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<br>
* key - exo-suit id<br>
* 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<br>
* key - subtype id<br>
* value - time last used (ms)
* */
private val lastUsedMaxExoSuitTimes : Array[Long] = Array.fill[Long](4)(0L) //invalid, ai, av, aa
/** key - object id<br>
* 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`.<br>
* key - object id<br>
* 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
/*

View file

@ -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)
}
}

View file

@ -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

View file

@ -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)))
}
}

View file

@ -216,6 +216,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) =>
@ -608,6 +610,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

View file

@ -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
}
}
}

View file

@ -4,7 +4,7 @@ package net.psforever.objects
import akka.actor.ActorRef
import net.psforever.objects.definition.VehicleDefinition
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, 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
@ -17,6 +17,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.<br>
@ -452,6 +453,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`

View file

@ -1,22 +1,28 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.avatar
import akka.actor.Actor
import net.psforever.objects.{DefaultCancellable, GlobalDefinitions, ImplantSlot, Player, Players, Tool}
import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile, SourceEntry}
import akka.actor.{Actor, ActorRef, Props}
import net.psforever.objects.{Players, _}
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.{Ammo, Equipment, EquipmentSize, JammableBehavior, JammableUnit}
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
import net.psforever.objects.loadouts.Loadout
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
import net.psforever.objects.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.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3}
import services.{RemoverActor, Service}
import services.avatar.{AvatarAction, AvatarServiceMessage}
import services.local.{LocalAction, LocalServiceMessage}
import scala.concurrent.duration._
import scala.collection.mutable
@ -25,112 +31,119 @@ 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)
private[this] val log = org.log4s.getLogger(player.Name)
private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel)
// A collection of timers for each slot to trigger stamina drain on an interval
val implantSlotStaminaDrainTimers = mutable.HashMap(0 -> DefaultCancellable.obj, 1 -> DefaultCancellable.obj, 2 -> DefaultCancellable.obj)
// control agency for the player's locker container (dedicated inventory slot #5)
val lockerControlAgent : ActorRef = {
val locker = player.Locker
locker.Zone = player.Zone
locker.Actor = context.actorOf(Props(classOf[LockerContainerControl], locker, player.Name), PlanetSideServerObject.UniqueActorName(locker))
}
override def postStop() : Unit = {
context.stop(lockerControlAgent)
player.Locker.Actor = ActorRef.noSender
implantSlotStaminaDrainTimers.values.foreach { _.cancel }
}
def receive : Receive = jammableBehavior
.orElse(takesDamage)
.orElse(containerBehavior)
.orElse {
case Player.ImplantActivation(slot: Int, status : Int) =>
// todo: disable implants with stamina cost when changing armour type
val implantSlot = player.ImplantSlot(slot)
case Player.ImplantActivation(slot : Int, status : Int) =>
// todo: disable implants with stamina cost when changing armour type
val implantSlot = player.ImplantSlot(slot)
// Allow uninitialized implants to be deactivated in case they're stuck in a state where they are no longer active or initialized but still draining stamina (e.g. by EMP)
if(status == 0 && (implantSlot.Active || !implantSlot.Initialized)) {
// Cancel stamina drain timer
implantSlotStaminaDrainTimers(slot).cancel()
implantSlotStaminaDrainTimers(slot) = DefaultCancellable.obj
implantSlot.Active = false
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.DeactivateImplantSlot(player.GUID, slot))
} else if (status == 1 && implantSlot.Initialized && !player.Fatigued) {
implantSlot.Installed match {
case Some(implant: ImplantDefinition) =>
if(implantSlot.Active) {
// Some events such as zoning will reset the implant on the client side without sending a deactivation packet
// But the implant will remain in an active state server side. For now, allow reactivation of the implant.
// todo: Deactivate implants server side when actions like zoning happen. (Other actions?)
log.warn(s"Implant ${slot} is already active, but activating again")
implantSlotStaminaDrainTimers(slot).cancel()
implantSlotStaminaDrainTimers(slot) = DefaultCancellable.obj
}
implantSlot.Active = true
if (implant.ActivationStaminaCost >= 0) {
player.Stamina -= implant.ActivationStaminaCost // Activation stamina drain
}
if(implant.StaminaCost > 0 && implant.GetCostIntervalByExoSuit(player.ExoSuit) > 0) { // Ongoing stamina drain, if applicable
implantSlotStaminaDrainTimers(slot) = context.system.scheduler.schedule(0 seconds, implant.GetCostIntervalByExoSuit(player.ExoSuit) milliseconds, self, Player.DrainStamina(implant.StaminaCost))
}
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2 + 1)) // Activation sound / effect
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.ActivateImplantSlot(player.GUID, slot))
case _ => ;
// Allow uninitialized implants to be deactivated in case they're stuck in a state where they are no longer active or initialized but still draining stamina (e.g. by EMP)
if (status == 0 && (implantSlot.Active || !implantSlot.Initialized)) {
// Cancel stamina drain timer
implantSlotStaminaDrainTimers(slot).cancel()
implantSlotStaminaDrainTimers(slot) = DefaultCancellable.obj
implantSlot.Active = false
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.DeactivateImplantSlot(player.GUID, slot))
}
}
else {
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 if (status == 1 && implantSlot.Initialized && !player.Fatigued) {
implantSlot.Installed match {
case Some(implant : ImplantDefinition) =>
if (implantSlot.Active) {
// Some events such as zoning will reset the implant on the client side without sending a deactivation packet
// But the implant will remain in an active state server side. For now, allow reactivation of the implant.
// todo: Deactivate implants server side when actions like zoning happen. (Other actions?)
log.warn(s"Implant $slot is already active, but activating again")
implantSlotStaminaDrainTimers(slot).cancel()
implantSlotStaminaDrainTimers(slot) = DefaultCancellable.obj
}
implantSlot.Active = true
if (implant.ActivationStaminaCost >= 0) {
player.Stamina -= implant.ActivationStaminaCost // Activation stamina drain
}
if (implant.StaminaCost > 0 && implant.GetCostIntervalByExoSuit(player.ExoSuit) > 0) { // Ongoing stamina drain, if applicable
implantSlotStaminaDrainTimers(slot) = context.system.scheduler.schedule(0 seconds, implant.GetCostIntervalByExoSuit(player.ExoSuit) milliseconds, self, Player.DrainStamina(implant.StaminaCost))
}
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2 + 1)) // Activation sound / effect
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.ActivateImplantSlot(player.GUID, slot))
case _ => ;
}
}
// 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 != DefaultCancellable.obj) {
implantSlot.InitializeTimer.cancel()
implantSlot.InitializeTimer = DefaultCancellable.obj
// 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 != DefaultCancellable.obj) {
implantSlot.InitializeTimer.cancel()
implantSlot.InitializeTimer = DefaultCancellable.obj
}
}
} 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) {
if (player.isAlive) {
PlayerControl.DestructionAwareness(player, None)
}
@ -138,7 +151,7 @@ class PlayerControl(player : Player) extends Actor
//heal
val originalHealth = player.Health
val definition = player.Definition
if(player.MaxHealth > 0 && originalHealth < player.MaxHealth &&
if (player.MaxHealth > 0 && originalHealth < player.MaxHealth &&
user.Faction == player.Faction &&
item.Magazine > 0 &&
Vector3.Distance(user.Position, player.Position) < definition.RepairDistance) {
@ -146,14 +159,14 @@ class PlayerControl(player : Player) extends Actor
val events = zone.AvatarEvents
val uname = user.Name
val guid = player.GUID
if(!(player.isMoving || user.isMoving)) { //only allow stationary heals
if (!(player.isMoving || user.isMoving)) { //only allow stationary heals
val newHealth = player.Health = originalHealth + 10
val magazine = item.Discharge
events ! AvatarServiceMessage(uname, AvatarAction.SendResponse(Service.defaultPlayerGUID, InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong)))
events ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttributeToAll(guid, 0, newHealth))
player.History(HealFromEquipment(PlayerSource(player), PlayerSource(user), newHealth - originalHealth, GlobalDefinitions.medicalapplicator))
}
if(player != user) {
if (player != user) {
//"Someone is trying to heal you"
events ! AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(guid, 55, 1))
//progress bar remains visible for all heal attempts
@ -163,7 +176,7 @@ class PlayerControl(player : Player) extends Actor
case CommonMessages.Use(user, Some(item : Tool)) if item.Definition == GlobalDefinitions.medicalapplicator =>
//revive
if(user != player &&
if (user != player &&
user.Faction == player.Faction &&
user.isAlive && !user.isMoving &&
!player.isAlive && !player.isBackpack &&
@ -178,7 +191,7 @@ class PlayerControl(player : Player) extends Actor
case CommonMessages.Use(user, Some(item : Tool)) if item.Definition == GlobalDefinitions.bank =>
val originalArmor = player.Armor
val definition = player.Definition
if(player.MaxArmor > 0 && originalArmor < player.MaxArmor &&
if (player.MaxArmor > 0 && originalArmor < player.MaxArmor &&
user.Faction == player.Faction &&
item.AmmoType == Ammo.armor_canister && item.Magazine > 0 &&
Vector3.Distance(user.Position, player.Position) < definition.RepairDistance) {
@ -186,15 +199,15 @@ class PlayerControl(player : Player) extends Actor
val events = zone.AvatarEvents
val uname = user.Name
val guid = player.GUID
if(!(player.isMoving || user.isMoving)) { //only allow stationary repairs
if (!(player.isMoving || user.isMoving)) { //only allow stationary repairs
val newArmor = player.Armor = originalArmor + Repairable.Quality + RepairValue(item) + definition.RepairMod
val magazine = item.Discharge
events ! AvatarServiceMessage(uname, AvatarAction.SendResponse(Service.defaultPlayerGUID, InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong)))
events ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttributeToAll(guid, 4, player.Armor))
player.History(RepairFromEquipment(PlayerSource(player), PlayerSource(user), newArmor - originalArmor, GlobalDefinitions.bank))
}
if(player != user) {
if(player.isAlive) {
if (player != user) {
if (player.isAlive) {
//"Someone is trying to repair you" gets strobed twice for visibility
val msg = AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(guid, 56, 1))
events ! msg
@ -206,6 +219,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) < 30000L) {
false
}
else {
player.SetLastUsedTime(exosuit, subtype, time)
true
}
}
else {
player.SetLastUsedTime(exosuit, subtype, time)
true
})
val result = if (requestToChangeArmor && allowedToChangeArmor) {
log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit")
player.SetLastUsedTime(exosuit, subtype, System.currentTimeMillis())
val beforeHolsters = Players.clearHolsters(player.Holsters().iterator)
val beforeInventory = player.Inventory.Clear()
//change suit
val originalArmor = player.Armor
player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit
val toMaxArmor = player.MaxArmor
if (originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) {
player.History(HealFromExoSuitChange(PlayerSource(player), exosuit))
player.Armor = toMaxArmor
}
else {
player.Armor = originalArmor
}
//ensure arm is down, even if it needs to go back up
if (player.DrawnSlot != Player.HandsDownSlot) {
player.DrawnSlot = Player.HandsDownSlot
}
val normalHolsters = if (originalSuit == ExoSuitType.MAX) {
val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max)
toDelete ++= maxWeapons
normalWeapons
}
else {
beforeHolsters
}
//populate holsters
val (afterHolsters, finalInventory) = if (exosuit == ExoSuitType.MAX) {
(normalHolsters, Players.fillEmptyHolsters(List(player.Slot(4)).iterator, normalHolsters) ++ beforeInventory)
}
else if (originalSuit == exosuit) { //note - this will rarely be the situation
(normalHolsters, Players.fillEmptyHolsters(player.Holsters().iterator, normalHolsters))
}
else {
val (afterHolsters, toInventory) = normalHolsters.partition(elem => elem.obj.Size == player.Slot(elem.start).Size)
afterHolsters.foreach({ elem => player.Slot(elem.start).Equipment = elem.obj })
val remainder = Players.fillEmptyHolsters(player.Holsters().iterator, toInventory ++ beforeInventory)
(
player.Holsters()
.zipWithIndex
.map { case (slot, i) => (slot.Equipment, i) }
.collect { case (Some(obj), index) => InventoryItem(obj, index) }
.toList,
remainder
)
}
//put items back into inventory
val (stow, drop) = if (originalSuit == exosuit) {
(finalInventory, Nil)
}
else {
val (a, b) = GridInventory.recoverInventory(finalInventory, player.Inventory)
(a, b.map {
InventoryItem(_, -1)
})
}
stow.foreach { elem =>
player.Inventory.InsertQuickly(elem.start, elem.obj)
}
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id,
AvatarAction.ChangeExosuit(player.GUID, exosuit, subtype, player.LastDrawnSlot, exosuit == ExoSuitType.MAX && requestToChangeArmor,
beforeHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterHolsters,
beforeInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, stow, drop,
toDelete.map { case InventoryItem(obj, _) => (obj, obj.GUID) }
)
)
true
}
else {
false
}
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result))
case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) =>
log.info(s"wants to change equipment loadout to their option #${msg.unk1 + 1}")
val fallbackSubtype = 0
val fallbackSuit = ExoSuitType.Standard
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player)
//sanitize exo-suit for change
val dropPred = ContainableBehavior.DropPredicate(player)
val oldHolsters = Players.clearHolsters(player.Holsters().iterator)
val dropHolsters = oldHolsters.filter(dropPred)
val oldInventory = player.Inventory.Clear()
val dropInventory = oldInventory.filter(dropPred)
val toDeleteOrDrop : List[InventoryItem] = (player.FreeHand.Equipment match {
case Some(obj) =>
val out = InventoryItem(obj, -1)
player.FreeHand.Equipment = None
if (dropPred(out)) {
List(out)
}
else {
Nil
}
case _ =>
Nil
}) ++ dropHolsters ++ dropInventory
//a loadout with a prohibited exo-suit type will result in the fallback exo-suit type
//imposed 5min delay on mechanized exo-suit switches
val time = System.currentTimeMillis()
val (nextSuit, nextSubtype) = if (Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
if (time - player.GetLastUsedTime(exosuit, subtype) < 30000L) {
false
}
else {
player.SetLastUsedTime(exosuit, subtype, time)
true
}
}
else {
player.SetLastUsedTime(exosuit, subtype, time)
true
})) {
(exosuit, subtype)
}
else {
log.warn(s"no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead")
player.SetLastUsedTime(fallbackSuit, fallbackSubtype, time)
(fallbackSuit, fallbackSubtype)
}
//sanitize (incoming) inventory
//TODO equipment permissions; these loops may be expanded upon in future
val curatedHolsters = for {
item <- holsters
//id = item.obj.Definition.ObjectId
//lastTime = player.GetLastUsedTime(id)
if true
} yield item
val curatedInventory = for {
item <- inventory
//id = item.obj.Definition.ObjectId
//lastTime = player.GetLastUsedTime(id)
if true
} yield item
//update suit internally
val originalArmor = player.Armor
player.ExoSuit = nextSuit
val toMaxArmor = player.MaxArmor
if (originalSuit != nextSuit || originalSubtype != nextSubtype || originalArmor > toMaxArmor) {
player.History(HealFromExoSuitChange(PlayerSource(player), nextSuit))
player.Armor = toMaxArmor
}
else {
player.Armor = originalArmor
}
//ensure arm is down, even if it needs to go back up
if (player.DrawnSlot != Player.HandsDownSlot) {
player.DrawnSlot = Player.HandsDownSlot
}
//a change due to exo-suit permissions mismatch will result in (more) items being re-arranged and/or dropped
//dropped items are not registered and can just be forgotten
val (afterHolsters, afterInventory) = if (nextSuit == exosuit) {
(
//melee slot preservation for MAX
if (nextSuit == ExoSuitType.MAX) {
holsters.filter(_.start == 4)
}
else {
curatedHolsters.filterNot(dropPred)
},
curatedInventory.filterNot(dropPred)
)
}
else {
//our exo-suit type was hijacked by changing permissions; we shouldn't even be able to use that loadout(!)
//holsters
val leftoversForInventory = Players.fillEmptyHolsters(
player.Holsters().iterator,
(curatedHolsters ++ curatedInventory).filterNot(dropPred)
)
val finalHolsters = player.Holsters()
.zipWithIndex
.collect { case (slot, index) if slot.Equipment.nonEmpty => InventoryItem(slot.Equipment.get, index) }
.toList
//inventory
val (finalInventory, _) = GridInventory.recoverInventory(leftoversForInventory, player.Inventory)
(finalHolsters, finalInventory)
}
(afterHolsters ++ afterInventory).foreach { entry => entry.obj.Faction = player.Faction }
toDeleteOrDrop.foreach { entry => entry.obj.Faction = PlanetSideEmpire.NEUTRAL }
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id,
AvatarAction.ChangeLoadout(player.GUID, nextSuit, nextSubtype, player.LastDrawnSlot, exosuit == ExoSuitType.MAX,
oldHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterHolsters,
oldInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterInventory, toDeleteOrDrop)
)
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true))
case _ => ; //terminal messages not handled here
}
case Zone.Ground.ItemOnGround(item, _, _) => ;
val name = player.Name
val zone = player.Zone
val avatarEvents = zone.AvatarEvents
val localEvents = zone.LocalEvents
item match {
case trigger : BoomerTrigger =>
//dropped the trigger, no longer own the boomer; make certain whole faction is aware of that
(zone.GUID(trigger.Companion), zone.Players.find { _.name == name}) match {
case (Some(boomer : BoomerDeployable), Some(avatar)) =>
val guid = boomer.GUID
val factionChannel = boomer.Faction.toString
if(avatar.Deployables.Remove(boomer)) {
boomer.Faction = PlanetSideEmpire.NEUTRAL
boomer.AssignOwnership(None)
avatar.Deployables.UpdateUIElement(boomer.Definition.Item).foreach { case (currElem, curr, maxElem, max) =>
avatarEvents ! AvatarServiceMessage(name, AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, maxElem, max))
avatarEvents ! AvatarServiceMessage(name, AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, currElem, curr))
}
localEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(boomer, zone))
localEvents ! LocalServiceMessage(factionChannel,
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss,
DeployableInfo(guid, DeployableIcon.Boomer, boomer.Position, PlanetSideGUID(0))
)
)
avatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, PlanetSideEmpire.NEUTRAL))
}
case _ => ; //pointless trigger? or a trigger being deleted?
}
case _ => ;
}
case Zone.Ground.CanNotDropItem(_, item, reason) =>
log.warn(s"${player.Name} tried to drop a ${item.Definition.Name} on the ground, but it $reason")
case Zone.Ground.ItemInHand(_) => ;
case Zone.Ground.CanNotPickupItem(_, item_guid, reason) =>
log.warn(s"${player.Name} failed to pick up an item ($item_guid) from the ground because $reason")
case _ => ;
}
@ -254,27 +524,22 @@ class PlayerControl(player : Player) extends Actor
* @param target an object that can be affected by the jammered status
* @param dur the duration of the timer, in milliseconds
*/
override def StartJammeredStatus(target : Any, dur : Int) : Unit = target match {
case obj : Player =>
//TODO these features
val guid = obj.GUID
val zone = obj.Zone
val zoneId = zone.Id
val events = zone.AvatarEvents
override def StartJammeredStatus(target : Any, dur : Int) : Unit = {
//TODO these features
val zone = player.Zone
for(slot <- 0 to player.Implants.length - 1) { // Deactivate & uninitialize all implants
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
self ! Player.ImplantActivation(slot, 0)
PlayerControl.UninitializeImplant(player, slot)
}
player.Implants.indices.foreach { slot => // Deactivate & uninitialize all implants
zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
self ! Player.ImplantActivation(slot, 0)
PlayerControl.UninitializeImplant(player, slot)
}
obj.skipStaminaRegenForTurns = math.max(obj.skipStaminaRegenForTurns, 10)
super.StartJammeredStatus(target, dur)
case _ => ;
player.skipStaminaRegenForTurns = math.max(player.skipStaminaRegenForTurns, 10)
super.StartJammeredStatus(target, dur)
}
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 +564,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 {

View file

@ -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)),

View file

@ -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] = {

View file

@ -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)
}

View file

@ -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

View file

@ -82,7 +82,7 @@ class TaskResolver() extends Actor {
private def GiveTask(aTask : Task) : Unit = {
val entry : TaskResolver.TaskEntry = TaskResolver.TaskEntry(aTask)
tasks += entry
trace(s"enqueue and start task $aTask")
trace(s"enqueue and start task ${aTask.Description}")
entry.Execute(self)
StartTimeoutCheck()
}
@ -112,12 +112,13 @@ class TaskResolver() extends Actor {
private def QueueSubtasks(task : Task, subtasks : List[TaskResolver.GiveTask], resolver : ActorRef = Actor.noSender) : Unit = {
val entry : TaskResolver.TaskEntry = TaskResolver.TaskEntry(task, subtasks.map(task => task.task), resolver)
tasks += entry
trace(s"enqueue task $task")
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
})
@ -160,7 +161,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
}
@ -177,7 +178,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()
}
@ -224,7 +225,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) {
@ -267,7 +268,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))

View file

@ -109,7 +109,7 @@ class GridInventory extends Container {
* Test whether a given piece of `Equipment` would collide with any stowed content in the inventory.<br>
* <br>
* 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.
@ -187,17 +187,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)
}
}
@ -577,7 +579,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
}
@ -778,4 +780,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
}
}

View file

@ -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
)

View file

@ -0,0 +1,631 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.containable
import akka.actor.{Actor, ActorRef}
import akka.pattern.{AskTimeoutException, ask}
import akka.util.Timeout
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.{Container, InventoryItem}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.zones.Zone
import net.psforever.objects.{BoomerTrigger, GlobalDefinitions, Player}
import net.psforever.types.{PlanetSideEmpire, Vector3}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.util.Success
/** Parent of all standard (input) messages handled by a `ContainableBehavior` object for the purposes of item transfer */
sealed trait ContainableMsg
/** `ContainableBehavior` messages that are allowed to be temporarily blocked in event of a complicated item transfer */
sealed trait DeferrableMsg extends ContainableMsg
/**
* A mixin for handling synchronized movement of `Equipment` items into or out from `Container` entities.
* The most important feature of this synchronization is the movmement of equipment
* out from one container into another container
* without causing representation overlap, overwriting, or unintended stacking of other equipment
* including equipment that has nort yet been inserted.
*/
trait ContainableBehavior {
_ : Actor =>
def ContainerObject : PlanetSideServerObject with Container
/**
* A flag for handling deferred messages during an attempt at complicated item movement (`MoveItem`) procedures.
* Complicated item movement procedures generally occur when the source and the destination are not the same container.
* The flag is set to `1` on the destination and is designed to block interference other normal insertion messages
* by taking those messages and pushing them back into the mailbox after a short delay.
* If two attempts on the same destination occur due to extremely coincidental item movement messages,
* the flag is set to `2` and most all messages involving item movement and item insertion are deferred.
* The destination is set back to normal - flag to `0` - when both of the attempts short-circuit due to timeout.
*/
private var waitOnMoveItemOps : Int = 0
final val containerBehavior : Receive = {
/* messages that modify delivery order */
case ContainableBehavior.Wait() => Wait()
case ContainableBehavior.Resume() => Resume()
case repeatMsg @ ContainableBehavior.Defer(msg, sentBy) =>
//received a previously blocked message; is it still blocked?
msg match {
case _ : ContainableMsg if waitOnMoveItemOps == 2 => RepeatMessageLater(repeatMsg)
case _ : DeferrableMsg if waitOnMoveItemOps == 1 => RepeatMessageLater(repeatMsg)
case _ => self.tell(msg, sentBy)
}
case msg : ContainableMsg if waitOnMoveItemOps == 2 =>
//all standard messages are blocked
RepeatMessageLater(ContainableBehavior.Defer(msg, sender))
MessageDeferredCallback(msg)
case msg : DeferrableMsg if waitOnMoveItemOps == 1 =>
//insertion messages not related to an item move attempt are blocked
RepeatMessageLater(ContainableBehavior.Defer(msg, sender))
MessageDeferredCallback(msg)
/* normal messages */
case Containable.RemoveItemFromSlot(None, Some(slot)) =>
sender ! LocalRemoveItemFromSlot(slot)
case Containable.RemoveItemFromSlot(Some(item), _) =>
sender ! LocalRemoveItemFromSlot(item)
case Containable.PutItemInSlot(item, dest) => /* can be deferred */
sender ! LocalPutItemInSlot(item, dest)
case Containable.PutItemInSlotOnly(item, dest) => /* can be deferred */
sender ! LocalPutItemInSlotOnly(item, dest)
case Containable.PutItemAway(item) => /* can be deferred */
sender ! LocalPutItemAway(item)
case Containable.PutItemInSlotOrAway(item, dest) => /* can be deferred */
sender ! LocalPutItemInSlotOrAway(item, dest)
case msg @ Containable.MoveItem(destination, equipment, destSlot) => /* can be deferred */
if(ContainableBehavior.TestPutItemInSlot(destination, equipment, destSlot).nonEmpty) { //test early, before we try to move the item
val source = ContainerObject
val item = equipment
val dest = destSlot
LocalRemoveItemFromSlot(item) match {
case Containable.ItemFromSlot(_, Some(_), slot @ Some(originalSlot)) =>
if(source eq destination) {
//when source and destination are the same, moving the item can be performed in one pass
LocalPutItemInSlot(item, dest) match {
case Containable.ItemPutInSlot(_, _, _, None) => ; //success
case Containable.ItemPutInSlot(_, _, _, Some(swapItem)) => //success, but with swap item
LocalPutItemInSlotOnlyOrAway(swapItem, slot) match {
case Containable.ItemPutInSlot(_, _, _, None) => ;
case _ =>
source.Zone.Ground.tell(Zone.Ground.DropItem(swapItem, source.Position, Vector3.z(source.Orientation.z)), source.Actor) //drop it
}
case _ : Containable.CanNotPutItemInSlot => //failure case ; try restore original item placement
LocalPutItemInSlot(item, originalSlot)
}
}
else {
//destination sync
destination.Actor ! ContainableBehavior.Wait()
implicit val timeout = new Timeout(1000 milliseconds)
val moveItemOver = ask(destination.Actor, ContainableBehavior.MoveItemPutItemInSlot(item, dest))
moveItemOver.onSuccess {
case Containable.ItemPutInSlot(_, _, _, None) => ; //successful
case Containable.ItemPutInSlot(_, _, _, Some(swapItem)) => //successful, but with swap item
PutItBackOrDropIt(source, swapItem, slot, destination.Actor)
case _ : Containable.CanNotPutItemInSlot => //failure case ; try restore original item placement
PutItBackOrDropIt(source, item, slot, source.Actor)
}
moveItemOver.onFailure {
case _ => //failure case ; try restore original item placement
PutItBackOrDropIt(source, item, slot, source.Actor)
}
//always do this
moveItemOver
.recover { case _ : AskTimeoutException => destination.Actor ! ContainableBehavior.Resume() }
.onComplete { _ => destination.Actor ! ContainableBehavior.Resume() }
}
case _ => ;
//we could not find the item to be moved in the source location; trying to act on old data?
}
}
else {
MessageDeferredCallback(msg)
}
case ContainableBehavior.MoveItemPutItemInSlot(item, dest) =>
sender ! LocalPutItemInSlot(item, dest)
case ContainableBehavior.MoveItemPutItemInSlotOrAway(item, dest) =>
sender ! LocalPutItemInSlotOrAway(item, dest)
}
/* Functions (message control) */
/**
* Defer a message until later.
* @see `ContainableBehavior.Defer`
* @see `DeferrableMsg`
* @param msg the message to defer
*/
def RepeatMessageLater(msg : Any) : Unit = {
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(100 milliseconds, self, msg)
}
/**
* Increment the flag for blocking messages.
*/
def Wait() : Unit = {
waitOnMoveItemOps = math.min(waitOnMoveItemOps + 1, 2)
}
/**
* Decrement the flag for blocking messages.
*/
def Resume() : Unit = {
waitOnMoveItemOps = math.max(0, waitOnMoveItemOps - 1)
}
/**
* Stop blocking messages.
*/
def Reset() : Unit = {
waitOnMoveItemOps = 0
}
/* Functions (item transfer) */
private def LocalRemoveItemFromSlot(slot : Int) : Any = {
val source = ContainerObject
val (outSlot, item) = ContainableBehavior.TryRemoveItemFromSlot(source, slot)
item match {
case Some(thing) => RemoveItemFromSlotCallback(thing, outSlot.get)
case None => ;
}
Containable.ItemFromSlot(source, item, outSlot)
}
private def LocalRemoveItemFromSlot(item : Equipment) : Any = {
val source = ContainerObject
val(slot, retItem) = ContainableBehavior.TryRemoveItemFromSlot(source, item)
retItem match {
case Some(thing) => RemoveItemFromSlotCallback(thing, slot.get)
case None => ;
}
Containable.ItemFromSlot(source, Some(item), slot)
}
private def LocalPutItemInSlot(item : Equipment, dest : Int) : Any = {
val destination = ContainerObject
ContainableBehavior.TryPutItemInSlot(destination, item, dest) match {
case (true, swapItem) =>
swapItem match {
case Some(thing) => SwapItemCallback(thing)
case None => ;
}
PutItemInSlotCallback(item, dest)
Containable.ItemPutInSlot(destination, item, dest, swapItem)
case (false, _) =>
Containable.CanNotPutItemInSlot(destination, item, dest)
}
}
private def LocalPutItemInSlotOnly(item : Equipment, dest : Int) : Any = {
val destination = ContainerObject
if(ContainableBehavior.TryPutItemInSlotOnly(destination, item, dest)) {
PutItemInSlotCallback(item, dest)
Containable.ItemPutInSlot(destination, item, dest, None)
}
else {
Containable.CanNotPutItemInSlot(destination, item, dest)
}
}
private def LocalPutItemAway(item : Equipment) : Any = {
val destination = ContainerObject
ContainableBehavior.TryPutItemAway(destination, item) match {
case Some(dest) =>
PutItemInSlotCallback(item, dest)
Containable.ItemPutInSlot(destination, item, dest, None)
case _ =>
Containable.CanNotPutItemInSlot(destination, item, -1)
}
}
private def LocalPutItemInSlotOrAway(item : Equipment, dest : Option[Int]) : Any = {
val destination = ContainerObject
ContainableBehavior.TryPutItemInSlotOrAway(destination, item, dest) match {
case (Some(slot), swapItem) =>
swapItem match {
case Some(thing) => SwapItemCallback(thing)
case None => ;
}
PutItemInSlotCallback(item, slot)
Containable.ItemPutInSlot(destination, item, slot, swapItem)
case (None, _) =>
Containable.CanNotPutItemInSlot(destination, item, dest.getOrElse(-1))
}
}
private def LocalPutItemInSlotOnlyOrAway(item : Equipment, dest : Option[Int]) : Any = {
val destination = ContainerObject
ContainableBehavior.TryPutItemInSlotOnlyOrAway(destination, item, dest) match {
case (Some(slot), None) =>
PutItemInSlotCallback(item, slot)
Containable.ItemPutInSlot(destination, item, slot, None)
case _ =>
Containable.CanNotPutItemInSlot(destination, item, dest.getOrElse(-1))
}
}
/**
* A controlled response where, in certain situations,
* it is appropriate to attempt to place an item into a specific container,
* first testing a specific slot,
* and attempting anywhere available in the container if not that slot,
* and, if nowhere is available, then it gets dropped on the ground.
* The inserted item is not permitted to swap places with another item in this case.
* @param container the container
* @param item the item to be inserted
* @param slot in which slot the insertion is prioritized (upper left corner of item)
* @param to a recipient to redirect the response message
* @param timeout how long the request has to complete before expiring
*/
private def PutItBackOrDropIt(container : PlanetSideServerObject with Container, item : Equipment, slot : Option[Int], to : ActorRef)(implicit timeout : Timeout) : Unit = {
val restore = ask(container.Actor, ContainableBehavior.MoveItemPutItemInSlotOrAway(item, slot))
restore.onSuccess {
case _ : Containable.CanNotPutItemInSlot =>
container.Zone.Ground.tell(Zone.Ground.DropItem(item, container.Position, Vector3.z(container.Orientation.z)), to)
case _ =>
}
restore.onFailure {
case _ =>
container.Zone.Ground.tell(Zone.Ground.DropItem(item, container.Position, Vector3.z(container.Orientation.z)), to)
}
}
/**
* Reaction to the initial deferrence of a message that should handle the visual aspects of not immediately addressing the message.
* To be implemented.
* @param msg the deferred message
*/
def MessageDeferredCallback(msg : Any) : Unit
/**
* Reaction to an item being removed a container.
* To be implemented.
* @param item the item that was removed
* @param slot the slot from which is was removed
*/
def RemoveItemFromSlotCallback(item : Equipment, slot : Int) : Unit
/**
* Reaction to an item being placed into a container.
* To be implemented.
* @param item the item that was removed
* @param slot the slot from which is was removed
*/
def PutItemInSlotCallback(item : Equipment, slot : Int) : Unit
/**
* Reaction to the existence of a swap item being produced from a container into the environment.
* To be implemented.
* @param item the item that was removed
*/
def SwapItemCallback(item : Equipment) : Unit
}
object ContainableBehavior {
/** Control message for temporarily blocking some messages to maintain integrity of underlying `Container` object */
private case class Wait()
/** Control message for unblocking all messages */
private case class Resume()
/** Internal message for the purpose of refreshing a blocked message in the mailbox */
private case class Defer(msg : Any, from : ActorRef)
/* The same as `PutItemInSlot`, but is not a `DeferrableMsg` for the purposes of completing a `MoveItem` */
private case class MoveItemPutItemInSlot(item : Equipment, slot : Int) extends ContainableMsg
/* The same as `PutItemInSlotOrAway`, but is not a `DeferrableMsg` for the purposes of completing a `MoveItem` */
private case class MoveItemPutItemInSlotOrAway(item : Equipment, slot : Option[Int]) extends ContainableMsg
/* Functions */
/**
* If the target item can be found in a container, remove the item from the container.
* This process can fail if the item can not be found or if it can not be removed for some reason.
* @see `Container.Find`
* @see `EquipmentSlot.Equipment`
* @param source the container in which the `item` is currently located
* @param item the item to be removed
* @return a `Tuple` of two optional values;
* the first is from what index in the container the `item` was removed, if it was removed;
* the second is the item again, if it has been removed;
* will use `(None, None)` to report failure
*/
def TryRemoveItemFromSlot(source : PlanetSideServerObject with Container, item : Equipment) : (Option[Int], Option[Equipment]) = {
source.Find(item) match {
case slot @ Some(index) =>
source.Slot(index).Equipment = None
if(source.Slot(index).Equipment.isEmpty) {
(slot, Some(item))
}
else {
(None, None)
}
case None =>
(None, None)
}
}
/**
* If the target slot of a container contains an item, remove that item from the container
* fromthe upper left corner position of the item as found in the container.
* This process can fail if no item can be found or if it can not be removed for some reason.
* @see `Container.Find`
* @see `EquipmentSlot.Equipment`
* @param source the container in which the `slot` is to be searched
* @param slot where the container will be searched
* @return a `Tuple` of two values;
* the first is from what `slot` in the container an `item` was removed, if any item removed;
* the second is the item, if it has been removed;
* will use `(None, None)` to report failure
*/
def TryRemoveItemFromSlot(source : PlanetSideServerObject with Container, slot : Int) : (Option[Int], Option[Equipment]) = {
val (item, outSlot) = source.Slot(slot).Equipment match {
case Some(thing) => (Some(thing), source.Find(thing))
case None => (None, None)
}
source.Slot(slot).Equipment = None
item match {
case Some(_) if item.nonEmpty && source.Slot(slot).Equipment.isEmpty =>
(outSlot, item)
case None =>
(None, None)
}
}
/**
* Are the conditions for an item insertion acceptable?
* If another item occupies the expected region of insertion (collision of bounding regions),
* the insertion can still be permitted with the assumption that
* the displaced item ("swap item") will have to be put somewhere else.
* @see `ContainableBehavior.PermitEquipmentStow`
* @see `Container.Collisions`
* @see `InventoryTile`
* @param destination the container
* @param item the item to be tested for insertion
* @param dest the upper left corner of the insertion position
* @return the results of the insertion test, if an insertion can be permitted;
* `None`, otherwise, and the insertion is not permitted
*/
def TestPutItemInSlot(destination : PlanetSideServerObject with Container, item : Equipment, dest : Int) : Option[List[InventoryItem]] = {
if(ContainableBehavior.PermitEquipmentStow(destination, item)) {
val tile = item.Definition.Tile
val destinationCollisionTest = destination.Collisions(dest, tile.Width, tile.Height)
destinationCollisionTest match {
case Success(Nil) => Some(Nil) //no item to swap
case Success(out @ List(_)) => Some(out) //one item to swap
case _ => None //abort when too many items at destination or other failure case
}
}
else {
None //blocked insertion (object type not permitted in container)
}
}
/**
* Put an item in a container at the given position.
* The inserted item may swap places with another item.
* If the new item can not be inserted, the swap item is kept in its original position.
* @param destination the container
* @param item the item to be inserted
* @param dest in which slot the insertion is expected to occur (upper left corner of item)
* @return a `Tuple` of two values;
* the first is `true` if the insertion occurred; and, `false`, otherwise
* the second is an optional item that was removed from a coincidental position in the container ("swap item")
*/
def TryPutItemInSlot(destination : PlanetSideServerObject with Container, item : Equipment, dest : Int) : (Boolean, Option[Equipment]) = {
ContainableBehavior.TestPutItemInSlot(destination, item, dest) match {
case Some(results) =>
//insert and swap, if applicable
val (swapItem, swapSlot) = results match {
case List(InventoryItem(obj, start)) => (Some(obj), start)
case _ => (None, dest)
}
destination.Slot(swapSlot).Equipment = None
if((destination.Slot(dest).Equipment = item).contains(item)) {
(true, swapItem)
}
else {
//put the swapItem back
destination.Slot(swapSlot).Equipment = swapItem
(false, None)
}
case None =>
(false, None)
}
}
/**
* Put an item in a container at the given position.
* The inserted item is not permitted to swap places with another item in this case.
* @param destination the container
* @param item the item to be inserted
* @param dest in which slot the insertion is expected to occur (upper left corner of item)
* @return `true` if the insertion occurred;
* `false`, otherwise
*/
def TryPutItemInSlotOnly(destination : PlanetSideServerObject with Container, item : Equipment, dest : Int) : Boolean = {
ContainableBehavior.TestPutItemInSlot(destination, item, dest).contains(Nil) && (destination.Slot(dest).Equipment = item).contains(item)
}
/**
* Put an item in a container in the whatever position it cleanly fits.
* The inserted item will not swap places with another item in this case.
* @param destination the container
* @param item the item to be inserted
* @return the slot index of the insertion point;
* `None`, if a clean insertion is not possible
*/
def TryPutItemAway(destination : PlanetSideServerObject with Container, item : Equipment) : Option[Int] = {
destination.Fit(item) match {
case out @ Some(dest)
if ContainableBehavior.PermitEquipmentStow(destination, item) && (destination.Slot(dest).Equipment = item).contains(item) =>
out
case _ =>
None
}
}
/**
* Attempt to put an item in a container at the given position.
* The inserted item may swap places with another item at this time.
* If the targeted insertion at this position fails,
* attempt to put the item in the container in the whatever position it cleanly fits.
* @param destination the container
* @param item the item to be inserted
* @param dest in which specific slot the insertion is first tested (upper left corner of item)
* @return na
*/
def TryPutItemInSlotOrAway(destination : PlanetSideServerObject with Container, item : Equipment, dest : Option[Int]) : (Option[Int], Option[Equipment]) = {
(dest match {
case Some(slot) => ContainableBehavior.TryPutItemInSlot(destination, item, slot)
case None => (false, None)
}) match {
case (true, swapItem) =>
(dest, swapItem)
case _ =>
ContainableBehavior.TryPutItemAway(destination, item) match {
case out @ Some(_) => (out, None)
case None => (None, None)
}
}
}
/**
* Attempt to put an item in a container at the given position.
* The inserted item may not swap places with another item at this time.
* If the targeted insertion at this position fails,
* attempt to put the item in the container in the whatever position it cleanly fits.
* @param destination the container
* @param item the item to be inserted
* @param dest in which specific slot the insertion is first tested (upper left corner of item)
* @return na
*/
def TryPutItemInSlotOnlyOrAway(destination : PlanetSideServerObject with Container, item : Equipment, dest : Option[Int]) : (Option[Int], Option[Equipment]) = {
(dest match {
case Some(slot) if ContainableBehavior.TestPutItemInSlot(destination, item, slot).contains(Nil) => ContainableBehavior.TryPutItemInSlot(destination, item, slot)
case None => (false, None)
}) match {
case (true, swapItem) =>
(dest, swapItem)
case _ =>
ContainableBehavior.TryPutItemAway(destination, item) match {
case out @ Some(_) => (out, None)
case None => (None, None)
}
}
}
/**
* Apply incontestable, arbitrary limitations
* whereby certain items are denied insertion into certain containers
* for vaguely documented but assuredly fantastic excuses on the part of the developer.
* @param destination the container
* @param equipment the item to be inserted
* @return `true`, if the object is allowed to contain the type of equipment object;
* `false`, otherwise
*/
def PermitEquipmentStow(destination : PlanetSideServerObject with Container, equipment : Equipment) : Boolean = {
import net.psforever.objects.{BoomerTrigger, Player}
equipment match {
case _ : BoomerTrigger =>
//a BoomerTrigger can only be stowed in a player's holsters or inventory
//this is only a requirement until they, and their Boomer explosive complement, are cleaned-up properly
destination.isInstanceOf[Player]
case _ =>
true
}
}
/**
* A predicate used to determine if an `InventoryItem` object contains `Equipment` that should be dropped.
* Used to filter through lists of object data before it is placed into a player's inventory.
* Drop the item if:<br>
* - the item is cavern equipment<br>
* - the item is a `BoomerTrigger` type object<br>
* - the item is a `router_telepad` type object<br>
* - 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
}

View file

@ -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.{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
}
}
/**

View file

@ -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)
}
}
}

View file

@ -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 = { }
}

View file

@ -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, 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) 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) =>
@ -244,6 +300,73 @@ class VehicleControl(vehicle : Vehicle) extends Actor
}
}
def MessageDeferredCallback(msg : Any) : Unit = {
msg match {
case Containable.MoveItem(_, item, _) =>
//momentarily put item back where it was originally
val obj = ContainerObject
obj.Find(item) match {
case Some(slot) =>
obj.Zone.AvatarEvents ! AvatarServiceMessage(
self.toString,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectAttachMessage(obj.GUID, item.GUID, slot))
)
case None => ;
}
case _ => ;
}
}
def RemoveItemFromSlotCallback(item : Equipment, slot : Int) : Unit = {
val zone = ContainerObject.Zone
zone.VehicleEvents ! VehicleServiceMessage(self.toString, VehicleAction.UnstowEquipment(Service.defaultPlayerGUID, item.GUID))
}
def PutItemInSlotCallback(item : Equipment, slot : Int) : Unit = {
val obj = ContainerObject
val oguid = obj.GUID
val zone = obj.Zone
val channel = self.toString
val events = zone.VehicleEvents
val iguid = item.GUID
val definition = item.Definition
item.Faction = obj.Faction
events ! VehicleServiceMessage(
//TODO when a new weapon, the equipment slot ui goes blank, but the weapon functions; remount vehicle to correct it
if(obj.VisibleSlots.contains(slot)) zone.Id else channel,
VehicleAction.SendResponse(
Service.defaultPlayerGUID,
ObjectCreateMessage(
definition.ObjectId,
iguid,
ObjectCreateMessageParent(oguid, slot),
definition.Packet.ConstructorData(item).get
)
)
)
item match {
case box : AmmoBox =>
events ! VehicleServiceMessage(
channel,
VehicleAction.InventoryState2(Service.defaultPlayerGUID, iguid, oguid, box.Capacity)
)
case weapon : Tool =>
weapon.AmmoSlots.map { slot => slot.Box }.foreach { box =>
events ! VehicleServiceMessage(
channel,
VehicleAction.InventoryState2(Service.defaultPlayerGUID, iguid, weapon.GUID, box.Capacity)
)
}
case _ => ;
}
}
def SwapItemCallback(item : Equipment) : Unit = {
val obj = ContainerObject
val zone = obj.Zone
zone.VehicleEvents ! VehicleServiceMessage(self.toString, VehicleAction.SendResponse(Service.defaultPlayerGUID, ObjectDetachMessage(obj.GUID, item.GUID, Vector3.Zero, 0f)))
}
def LoseOwnership() : Unit = {
val obj = MountableObject
Vehicles.Disown(obj.GUID, obj)

View file

@ -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 _ => ;
}

View file

@ -63,13 +63,26 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player
}
case Zone.Corpse.Add(player) =>
CorpseAdd(player, corpseList)
val (playerIsCorpse, playerInZone) = CorpseAdd(player, playerMap, corpseList)
if(playerIsCorpse) {
if(!playerInZone && player.Actor == ActorRef.noSender) {
player.Zone = zone
player.Actor = context.actorOf(Props(classOf[PlayerControl], player), s"corpse_of_${player.CharId}_${player.GUID.guid}_${System.currentTimeMillis}")
}
}
case Zone.Corpse.Remove(player) =>
CorpseRemove(player, corpseList)
if(CorpseRemove(player, corpseList)) {
PlayerLeave(player)
}
case _ => ;
}
def PlayerLeave(player : Player) : Unit = {
context.stop(player.Actor)
player.Actor = ActorRef.noSender
}
}
object ZonePopulationActor {
@ -148,18 +161,33 @@ object ZonePopulationActor {
/**
* If the given `player` passes a condition check, add it to the list.
* Also, ensure that "this player" is not currently counted among the living.
* @param player a `Player` object
* @param playerMap the mapping of `Avatar` objects to `Player` objects
* @param corpseList a list of `Player` objects
* @return true, if the `player` was added to the list;
* false, otherwise
* @return a `Tuple` of two flags;
* the first is whether the player was turned into a corpse or not;
* the second is whether the player was found in the zone before being turned into a corpse
*/
def CorpseAdd(player : Player, corpseList : ListBuffer[Player]) : Boolean = {
def CorpseAdd(player : Player, playerMap : TrieMap[Avatar, Option[Player]], corpseList : ListBuffer[Player]) : (Boolean, Boolean) = {
if(player.isBackpack) {
corpseList += player
true
val playerFoundInZone = playerMap.find {
case (_, Some(p)) => p.CharId == player.CharId
case (_, None) => false
} match {
case Some((a, _)) => PopulationRelease(a, playerMap).nonEmpty
case _ => false
}
corpseList.find { _ eq player } match {
case None => ;
corpseList += player
(true, playerFoundInZone)
case _ =>
(false, false)
}
}
else {
false
(false, false)
}
}
@ -167,20 +195,19 @@ object ZonePopulationActor {
* Remove the given `player` from the list.
* @param player a `Player` object
* @param corpseList a list of `Player` objects
* @return `true`, if the corpse was found and removed;
* `false`, otherwise
*/
def CorpseRemove(player : Player, corpseList : ListBuffer[Player]) : Unit = {
def CorpseRemove(player : Player, corpseList : ListBuffer[Player]) : Boolean = {
recursiveFindCorpse(corpseList.iterator, player) match {
case None => ;
case None =>
false
case Some(index) =>
corpseList.remove(index)
true
}
}
def PlayerLeave(player : Player) : Unit = {
player.Actor ! akka.actor.PoisonPill
player.Actor = ActorRef.noSender
}
/**
* A recursive function that finds and removes a specific player from a list of players.
* @param iter an `Iterator` of `Player` objects

View file

@ -94,7 +94,7 @@ class AvatarService(zone : Zone) extends Actor {
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.DestroyDisplay(killer, victim, method, unk))
)
case AvatarAction.DropItem(player_guid, item, _) =>
case AvatarAction.DropItem(player_guid, item) =>
val definition = item.Definition
val objectData = DroppedItemData(
PlacementData(item.Position, item.Orientation),
@ -179,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")
}

View file

@ -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,7 +35,7 @@ object AvatarAction {
final case class ActivateImplantSlot(player_guid : PlanetSideGUID, slot : Int) extends Action
final case class Destroy(victim : PlanetSideGUID, killer : PlanetSideGUID, weapon : PlanetSideGUID, pos : Vector3) extends Action
final case class DestroyDisplay(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int = 121) extends Action
final case class DropItem(player_guid : PlanetSideGUID, item : Equipment, 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
@ -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

View file

@ -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,
@ -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
}

View file

@ -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 _ => ;
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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
@ -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
@ -595,5 +607,4 @@ class PlayerControlDeathSeatedTest extends ActorTest {
}
}
object PlayerControlTest { }

View file

@ -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 {

View file

@ -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)

View file

@ -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)

View file

@ -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")

View file

@ -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")

View file

@ -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.<br>
* <br>
* 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
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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)))
}
}