mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-20 02:24:45 +00:00
Let's Move Item, Together, Again (#445)
* 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 * manual merge-rebase of master with hand reconstruction of WorldSessionActor and PlayerControl, and variations for other files necessary to maintain both inventory operations and login reliability * test fixes; MAX exo-suit cooldown is now five minutes again
This commit is contained in:
parent
d6397d54a1
commit
bd82d332fa
|
|
@ -49,7 +49,7 @@ lazy val commonSettings = Seq(
|
|||
"io.kamon" %% "kamon-apm-reporter" % "2.1.0",
|
||||
"org.json4s" %% "json4s-native" % "3.6.8",
|
||||
"com.typesafe.akka" %% "akka-stream" % "2.6.5",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
lazy val pscryptoSettings = Seq(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,6 +217,8 @@ class Player(private val core : Avatar) extends PlanetSideServerObject
|
|||
|
||||
def Locker : LockerContainer = core.Locker
|
||||
|
||||
def FifthSlot : EquipmentSlot = core.FifthSlot
|
||||
|
||||
override def Fit(obj : Equipment) : Option[Int] = {
|
||||
recursiveHolsterFit(holsters.iterator, obj.Size) match {
|
||||
case Some(index) =>
|
||||
|
|
@ -617,6 +619,30 @@ class Player(private val core : Avatar) extends PlanetSideServerObject
|
|||
|
||||
def VehicleOwned_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = core.VehicleOwned_=(guid)
|
||||
|
||||
def GetLastUsedTime(code : Int) : Long = core.GetLastUsedTime(code)
|
||||
|
||||
def GetLastUsedTime(code : ExoSuitType.Value) : Long = core.GetLastUsedTime(code)
|
||||
|
||||
def GetLastUsedTime(code : ExoSuitType.Value, subtype : Int) : Long = core.GetLastUsedTime(code, subtype)
|
||||
|
||||
def SetLastUsedTime(code : Int, time : Long) : Unit = core.SetLastUsedTime(code, time)
|
||||
|
||||
def SetLastUsedTime(code : ExoSuitType.Value): Unit = core.SetLastUsedTime(code)
|
||||
|
||||
def SetLastUsedTime(code : ExoSuitType.Value, time : Long) : Unit = core.SetLastUsedTime(code, time)
|
||||
|
||||
def SetLastUsedTime(code : ExoSuitType.Value, subtype : Int): Unit = core.SetLastUsedTime(code, subtype)
|
||||
|
||||
def SetLastUsedTime(code : ExoSuitType.Value, subtype : Int, time : Long) : Unit = core.SetLastUsedTime(code, subtype, time)
|
||||
|
||||
def GetLastPurchaseTime(code : Int) : Long = core.GetLastPurchaseTime(code)
|
||||
|
||||
def SetLastPurchaseTime(code : Int, time : Long) : Unit = core.SetLastPurchaseTime(code, time)
|
||||
|
||||
def ObjectTypeNameReference(id : Long) : String = core.ObjectTypeNameReference(id)
|
||||
|
||||
def ObjectTypeNameReference(id : Long, name : String) : String = core.ObjectTypeNameReference(id, name)
|
||||
|
||||
def DamageModel = exosuit.asInstanceOf[DamageResistanceModel]
|
||||
|
||||
def Definition : AvatarDefinition = core.Definition
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package net.psforever.objects
|
|||
|
||||
import net.psforever.objects.definition.VehicleDefinition
|
||||
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
|
||||
import net.psforever.objects.inventory.{Container, GridInventory, InventoryTile}
|
||||
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile}
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
|
|
@ -16,6 +16,7 @@ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
|
|||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.util.{Success, Try}
|
||||
|
||||
/**
|
||||
* The server-side support object that represents a vehicle.<br>
|
||||
|
|
@ -451,6 +452,20 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends AmenityOwner
|
|||
}
|
||||
}
|
||||
|
||||
override def Collisions(dest : Int, width : Int, height : Int) : Try[List[InventoryItem]] = {
|
||||
weapons.get(dest) match {
|
||||
case Some(slot) =>
|
||||
slot.Equipment match {
|
||||
case Some(item) =>
|
||||
Success(List(InventoryItem(item, dest)))
|
||||
case None =>
|
||||
Success(List())
|
||||
}
|
||||
case None =>
|
||||
super.Collisions(dest, width, height)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference to the `Vehicle` `Trunk` space.
|
||||
* @return this `Vehicle` `Trunk`
|
||||
|
|
|
|||
|
|
@ -1,134 +1,153 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.objects.avatar
|
||||
|
||||
import akka.actor.Actor
|
||||
import net.psforever.objects.{Default, GlobalDefinitions, ImplantSlot, Player, Players, Tool}
|
||||
import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile, SourceEntry}
|
||||
import akka.actor.{Actor, ActorRef, Props}
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile}
|
||||
import net.psforever.objects.definition.ImplantDefinition
|
||||
import net.psforever.objects.equipment.{Ammo, JammableBehavior, JammableUnit}
|
||||
import net.psforever.objects.equipment._
|
||||
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
|
||||
import net.psforever.objects.loadouts.Loadout
|
||||
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
|
||||
import net.psforever.objects.vital.{PlayerSuicide, Vitality}
|
||||
import net.psforever.objects.serverobject.CommonMessages
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.serverobject.damage.Damageable
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.repair.Repairable
|
||||
import net.psforever.objects.serverobject.terminals.Terminal
|
||||
import net.psforever.objects.vital._
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types.{ExoSuitType, ImplantType, PlanetSideGUID, Vector3}
|
||||
import services.Service
|
||||
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
|
||||
import net.psforever.types._
|
||||
import services.{RemoverActor, Service}
|
||||
import services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import services.local.{LocalAction, LocalServiceMessage}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
|
||||
class PlayerControl(player : Player) extends Actor
|
||||
with JammableBehavior
|
||||
with Damageable {
|
||||
with Damageable
|
||||
with ContainableBehavior {
|
||||
def JammableObject = player
|
||||
def DamageableObject = player
|
||||
def ContainerObject = player
|
||||
|
||||
private [this] val log = org.log4s.getLogger(player.Name)
|
||||
private [this] val damageLog = org.log4s.getLogger(Damageable.LogChannel)
|
||||
|
||||
// A collection of timers for each slot to trigger stamina drain on an interval
|
||||
val implantSlotStaminaDrainTimers = mutable.HashMap(0 -> Default.Cancellable, 1 -> Default.Cancellable, 2 -> Default.Cancellable)
|
||||
// control agency for the player's locker container (dedicated inventory slot #5)
|
||||
val lockerControlAgent : ActorRef = {
|
||||
val locker = player.Locker
|
||||
locker.Zone = player.Zone
|
||||
locker.Actor = context.actorOf(Props(classOf[LockerContainerControl], locker, player.Name), PlanetSideServerObject.UniqueActorName(locker))
|
||||
}
|
||||
|
||||
override def postStop() : Unit = {
|
||||
lockerControlAgent ! akka.actor.PoisonPill
|
||||
player.Locker.Actor = Default.Actor
|
||||
implantSlotStaminaDrainTimers.values.foreach { _.cancel }
|
||||
}
|
||||
|
||||
def receive : Receive = jammableBehavior
|
||||
.orElse(takesDamage)
|
||||
.orElse(containerBehavior)
|
||||
.orElse {
|
||||
case Player.ImplantActivation(slot: Int, status : Int) =>
|
||||
// todo: disable implants with stamina cost when changing armour type
|
||||
val implantSlot = player.ImplantSlot(slot)
|
||||
case Player.ImplantActivation(slot: Int, status : Int) =>
|
||||
// todo: disable implants with stamina cost when changing armour type
|
||||
val implantSlot = player.ImplantSlot(slot)
|
||||
|
||||
// Allow uninitialized implants to be deactivated in case they're stuck in a state where they are no longer active or initialized but still draining stamina (e.g. by EMP)
|
||||
if(status == 0 && (implantSlot.Active || !implantSlot.Initialized)) {
|
||||
// Cancel stamina drain timer
|
||||
implantSlotStaminaDrainTimers(slot).cancel()
|
||||
implantSlotStaminaDrainTimers(slot) = Default.Cancellable
|
||||
// Allow uninitialized implants to be deactivated in case they're stuck in a state where they are no longer active or initialized but still draining stamina (e.g. by EMP)
|
||||
if(status == 0 && (implantSlot.Active || !implantSlot.Initialized)) {
|
||||
// Cancel stamina drain timer
|
||||
implantSlotStaminaDrainTimers(slot).cancel()
|
||||
implantSlotStaminaDrainTimers(slot) = Default.Cancellable
|
||||
|
||||
implantSlot.Active = false
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.DeactivateImplantSlot(player.GUID, slot))
|
||||
} else if (status == 1 && implantSlot.Initialized && !player.Fatigued) {
|
||||
implantSlot.Installed match {
|
||||
case Some(implant: ImplantDefinition) =>
|
||||
if(implantSlot.Active) {
|
||||
// Some events such as zoning will reset the implant on the client side without sending a deactivation packet
|
||||
// But the implant will remain in an active state server side. For now, allow reactivation of the implant.
|
||||
// todo: Deactivate implants server side when actions like zoning happen. (Other actions?)
|
||||
log.warn(s"Implant ${slot} is already active, but activating again")
|
||||
implantSlotStaminaDrainTimers(slot).cancel()
|
||||
implantSlotStaminaDrainTimers(slot) = Default.Cancellable
|
||||
}
|
||||
implantSlot.Active = true
|
||||
implantSlot.Active = false
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.DeactivateImplantSlot(player.GUID, slot))
|
||||
} else if (status == 1 && implantSlot.Initialized && !player.Fatigued) {
|
||||
implantSlot.Installed match {
|
||||
case Some(implant: ImplantDefinition) =>
|
||||
if(implantSlot.Active) {
|
||||
// Some events such as zoning will reset the implant on the client side without sending a deactivation packet
|
||||
// But the implant will remain in an active state server side. For now, allow reactivation of the implant.
|
||||
// todo: Deactivate implants server side when actions like zoning happen. (Other actions?)
|
||||
log.warn(s"Implant $slot is already active, but activating again")
|
||||
implantSlotStaminaDrainTimers(slot).cancel()
|
||||
implantSlotStaminaDrainTimers(slot) = Default.Cancellable
|
||||
}
|
||||
implantSlot.Active = true
|
||||
|
||||
if (implant.ActivationStaminaCost >= 0) {
|
||||
player.Stamina -= implant.ActivationStaminaCost // Activation stamina drain
|
||||
}
|
||||
if (implant.ActivationStaminaCost >= 0) {
|
||||
player.Stamina -= implant.ActivationStaminaCost // Activation stamina drain
|
||||
}
|
||||
|
||||
if(implant.StaminaCost > 0 && implant.GetCostIntervalByExoSuit(player.ExoSuit) > 0) { // Ongoing stamina drain, if applicable
|
||||
implantSlotStaminaDrainTimers(slot) = context.system.scheduler.scheduleWithFixedDelay(0 seconds, implant.GetCostIntervalByExoSuit(player.ExoSuit) milliseconds, self, Player.DrainStamina(implant.StaminaCost))
|
||||
}
|
||||
if(implant.StaminaCost > 0 && implant.GetCostIntervalByExoSuit(player.ExoSuit) > 0) { // Ongoing stamina drain, if applicable
|
||||
implantSlotStaminaDrainTimers(slot) = context.system.scheduler.scheduleWithFixedDelay(0 seconds, implant.GetCostIntervalByExoSuit(player.ExoSuit) milliseconds, self, Player.DrainStamina(implant.StaminaCost))
|
||||
}
|
||||
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2 + 1)) // Activation sound / effect
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.ActivateImplantSlot(player.GUID, slot))
|
||||
case _ => ;
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2 + 1)) // Activation sound / effect
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.ActivateImplantSlot(player.GUID, slot))
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn(s"Can't handle ImplantActivation: Player GUID: ${player.GUID} Slot ${slot} Status: ${status} Initialized: ${implantSlot.Initialized} Active: ${implantSlot.Active} Fatigued: ${player.Fatigued}")
|
||||
}
|
||||
|
||||
case Player.UninitializeImplant(slot: Int) => {
|
||||
PlayerControl.UninitializeImplant(player, slot)
|
||||
}
|
||||
|
||||
case Player.ImplantInitializationStart(slot: Int) =>
|
||||
val implantSlot = player.ImplantSlot(slot)
|
||||
if(implantSlot.Installed.isDefined) {
|
||||
if(implantSlot.Initialized) {
|
||||
PlayerControl.UninitializeImplant(player, slot)
|
||||
else {
|
||||
log.warn(s"Can't handle ImplantActivation: Player GUID: ${player.GUID} Slot $slot Status: $status Initialized: ${implantSlot.Initialized} Active: ${implantSlot.Active} Fatigued: ${player.Fatigued}")
|
||||
}
|
||||
|
||||
// Start client side initialization timer
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(player.GUID, ActionProgressMessage(slot + 6, 0)))
|
||||
case Player.UninitializeImplant(slot: Int) =>
|
||||
PlayerControl.UninitializeImplant(player, slot)
|
||||
|
||||
// Callback after initialization timer to complete initialization
|
||||
implantSlot.InitializeTimer = context.system.scheduler.scheduleOnce(implantSlot.MaxTimer seconds, self, Player.ImplantInitializationComplete(slot))
|
||||
}
|
||||
case Player.ImplantInitializationStart(slot: Int) =>
|
||||
val implantSlot = player.ImplantSlot(slot)
|
||||
if(implantSlot.Installed.isDefined) {
|
||||
if(implantSlot.Initialized) {
|
||||
PlayerControl.UninitializeImplant(player, slot)
|
||||
}
|
||||
|
||||
case Player.ImplantInitializationComplete(slot: Int) =>
|
||||
val implantSlot = player.ImplantSlot(slot)
|
||||
if(implantSlot.Installed.isDefined) {
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.Initialization, slot, 1)))
|
||||
implantSlot.Initialized = true
|
||||
if(implantSlot.InitializeTimer != Default.Cancellable) {
|
||||
implantSlot.InitializeTimer.cancel()
|
||||
implantSlot.InitializeTimer = Default.Cancellable
|
||||
// Start client side initialization timer
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(player.GUID, ActionProgressMessage(slot + 6, 0)))
|
||||
|
||||
// Callback after initialization timer to complete initialization
|
||||
implantSlot.InitializeTimer = context.system.scheduler.scheduleOnce(implantSlot.MaxTimer seconds, self, Player.ImplantInitializationComplete(slot))
|
||||
}
|
||||
}
|
||||
|
||||
case Player.DrainStamina(amount : Int) =>
|
||||
player.Stamina -= amount
|
||||
|
||||
case Player.StaminaChanged(currentStamina : Int) =>
|
||||
if(currentStamina == 0) {
|
||||
player.Fatigued = true
|
||||
player.skipStaminaRegenForTurns += 4
|
||||
for(slot <- 0 to player.Implants.length - 1) { // Disable all implants
|
||||
self ! Player.ImplantActivation(slot, 0)
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponseTargeted(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 1)))
|
||||
case Player.ImplantInitializationComplete(slot: Int) =>
|
||||
val implantSlot = player.ImplantSlot(slot)
|
||||
if(implantSlot.Installed.isDefined) {
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.Initialization, slot, 1)))
|
||||
implantSlot.Initialized = true
|
||||
if(implantSlot.InitializeTimer != Default.Cancellable) {
|
||||
implantSlot.InitializeTimer.cancel()
|
||||
implantSlot.InitializeTimer = Default.Cancellable
|
||||
}
|
||||
}
|
||||
} else if (player.Fatigued && currentStamina >= 20) {
|
||||
player.Fatigued = false
|
||||
for(slot <- 0 to player.Implants.length - 1) { // Re-enable all implants
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponseTargeted(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 0)))
|
||||
}
|
||||
}
|
||||
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttributeSelf(player.GUID, 2, player.Stamina))
|
||||
case Player.DrainStamina(amount : Int) =>
|
||||
player.Stamina -= amount
|
||||
|
||||
case Player.StaminaChanged(currentStamina : Int) =>
|
||||
if(currentStamina == 0) {
|
||||
player.Fatigued = true
|
||||
player.skipStaminaRegenForTurns += 4
|
||||
player.Implants.indices.foreach { slot => // Disable all implants
|
||||
self ! Player.ImplantActivation(slot, 0)
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponseTargeted(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 1)))
|
||||
}
|
||||
} else if (player.Fatigued && currentStamina >= 20) {
|
||||
player.Fatigued = false
|
||||
player.Implants.indices.foreach { slot => // Re-enable all implants
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponseTargeted(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 0)))
|
||||
}
|
||||
}
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttributeSelf(player.GUID, 2, player.Stamina))
|
||||
|
||||
case Player.Die() =>
|
||||
if(player.isAlive) {
|
||||
PlayerControl.DestructionAwareness(player, None)
|
||||
|
|
@ -206,6 +225,263 @@ class PlayerControl(player : Player) extends Actor
|
|||
}
|
||||
}
|
||||
|
||||
case Terminal.TerminalMessage(_, msg, order) =>
|
||||
order match {
|
||||
case Terminal.BuyExosuit(exosuit, subtype) =>
|
||||
val time = System.currentTimeMillis
|
||||
var toDelete : List[InventoryItem] = Nil
|
||||
val originalSuit = player.ExoSuit
|
||||
val originalSubtype = Loadout.DetermineSubtype(player)
|
||||
val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype
|
||||
val allowedToChangeArmor = Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
|
||||
(if (exosuit == ExoSuitType.MAX) {
|
||||
if (time - player.GetLastUsedTime(exosuit, subtype) < 300000L) {
|
||||
false
|
||||
}
|
||||
else {
|
||||
player.SetLastUsedTime(exosuit, subtype, time)
|
||||
true
|
||||
}
|
||||
}
|
||||
else {
|
||||
player.SetLastUsedTime(exosuit, subtype, time)
|
||||
true
|
||||
})
|
||||
val result = if (requestToChangeArmor && allowedToChangeArmor) {
|
||||
log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit")
|
||||
player.SetLastUsedTime(exosuit, subtype, System.currentTimeMillis())
|
||||
val beforeHolsters = Players.clearHolsters(player.Holsters().iterator)
|
||||
val beforeInventory = player.Inventory.Clear()
|
||||
//change suit
|
||||
val originalArmor = player.Armor
|
||||
player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit
|
||||
val toMaxArmor = player.MaxArmor
|
||||
if (originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) {
|
||||
player.History(HealFromExoSuitChange(PlayerSource(player), exosuit))
|
||||
player.Armor = toMaxArmor
|
||||
}
|
||||
else {
|
||||
player.Armor = originalArmor
|
||||
}
|
||||
//ensure arm is down, even if it needs to go back up
|
||||
if (player.DrawnSlot != Player.HandsDownSlot) {
|
||||
player.DrawnSlot = Player.HandsDownSlot
|
||||
}
|
||||
val normalHolsters = if (originalSuit == ExoSuitType.MAX) {
|
||||
val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max)
|
||||
toDelete ++= maxWeapons
|
||||
normalWeapons
|
||||
}
|
||||
else {
|
||||
beforeHolsters
|
||||
}
|
||||
//populate holsters
|
||||
val (afterHolsters, finalInventory) = if (exosuit == ExoSuitType.MAX) {
|
||||
(normalHolsters, Players.fillEmptyHolsters(List(player.Slot(4)).iterator, normalHolsters) ++ beforeInventory)
|
||||
}
|
||||
else if (originalSuit == exosuit) { //note - this will rarely be the situation
|
||||
(normalHolsters, Players.fillEmptyHolsters(player.Holsters().iterator, normalHolsters))
|
||||
}
|
||||
else {
|
||||
val (afterHolsters, toInventory) = normalHolsters.partition(elem => elem.obj.Size == player.Slot(elem.start).Size)
|
||||
afterHolsters.foreach({ elem => player.Slot(elem.start).Equipment = elem.obj })
|
||||
val remainder = Players.fillEmptyHolsters(player.Holsters().iterator, toInventory ++ beforeInventory)
|
||||
(
|
||||
player.Holsters()
|
||||
.zipWithIndex
|
||||
.map { case (slot, i) => (slot.Equipment, i) }
|
||||
.collect { case (Some(obj), index) => InventoryItem(obj, index) }
|
||||
.toList,
|
||||
remainder
|
||||
)
|
||||
}
|
||||
//put items back into inventory
|
||||
val (stow, drop) = if (originalSuit == exosuit) {
|
||||
(finalInventory, Nil)
|
||||
}
|
||||
else {
|
||||
val (a, b) = GridInventory.recoverInventory(finalInventory, player.Inventory)
|
||||
(a, b.map {
|
||||
InventoryItem(_, -1)
|
||||
})
|
||||
}
|
||||
stow.foreach { elem =>
|
||||
player.Inventory.InsertQuickly(elem.start, elem.obj)
|
||||
}
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id,
|
||||
AvatarAction.ChangeExosuit(player.GUID, exosuit, subtype, player.LastDrawnSlot, exosuit == ExoSuitType.MAX && requestToChangeArmor,
|
||||
beforeHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterHolsters,
|
||||
beforeInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, stow, drop,
|
||||
toDelete.map { case InventoryItem(obj, _) => (obj, obj.GUID) }
|
||||
)
|
||||
)
|
||||
true
|
||||
}
|
||||
else {
|
||||
false
|
||||
}
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result))
|
||||
|
||||
case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) =>
|
||||
log.info(s"wants to change equipment loadout to their option #${msg.unk1 + 1}")
|
||||
val fallbackSubtype = 0
|
||||
val fallbackSuit = ExoSuitType.Standard
|
||||
val originalSuit = player.ExoSuit
|
||||
val originalSubtype = Loadout.DetermineSubtype(player)
|
||||
//sanitize exo-suit for change
|
||||
val dropPred = ContainableBehavior.DropPredicate(player)
|
||||
val oldHolsters = Players.clearHolsters(player.Holsters().iterator)
|
||||
val dropHolsters = oldHolsters.filter(dropPred)
|
||||
val oldInventory = player.Inventory.Clear()
|
||||
val dropInventory = oldInventory.filter(dropPred)
|
||||
val toDeleteOrDrop : List[InventoryItem] = (player.FreeHand.Equipment match {
|
||||
case Some(obj) =>
|
||||
val out = InventoryItem(obj, -1)
|
||||
player.FreeHand.Equipment = None
|
||||
if (dropPred(out)) {
|
||||
List(out)
|
||||
}
|
||||
else {
|
||||
Nil
|
||||
}
|
||||
case _ =>
|
||||
Nil
|
||||
}) ++ dropHolsters ++ dropInventory
|
||||
//a loadout with a prohibited exo-suit type will result in the fallback exo-suit type
|
||||
//imposed 5min delay on mechanized exo-suit switches
|
||||
val time = System.currentTimeMillis()
|
||||
val (nextSuit, nextSubtype) = if (Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
|
||||
(if (exosuit == ExoSuitType.MAX) {
|
||||
if (time - player.GetLastUsedTime(exosuit, subtype) < 300000L) {
|
||||
false
|
||||
}
|
||||
else {
|
||||
player.SetLastUsedTime(exosuit, subtype, time)
|
||||
true
|
||||
}
|
||||
}
|
||||
else {
|
||||
player.SetLastUsedTime(exosuit, subtype, time)
|
||||
true
|
||||
})) {
|
||||
(exosuit, subtype)
|
||||
}
|
||||
else {
|
||||
log.warn(s"no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead")
|
||||
player.SetLastUsedTime(fallbackSuit, fallbackSubtype, time)
|
||||
(fallbackSuit, fallbackSubtype)
|
||||
}
|
||||
//sanitize (incoming) inventory
|
||||
//TODO equipment permissions; these loops may be expanded upon in future
|
||||
val curatedHolsters = for {
|
||||
item <- holsters
|
||||
//id = item.obj.Definition.ObjectId
|
||||
//lastTime = player.GetLastUsedTime(id)
|
||||
if true
|
||||
} yield item
|
||||
val curatedInventory = for {
|
||||
item <- inventory
|
||||
//id = item.obj.Definition.ObjectId
|
||||
//lastTime = player.GetLastUsedTime(id)
|
||||
if true
|
||||
} yield item
|
||||
//update suit internally
|
||||
val originalArmor = player.Armor
|
||||
player.ExoSuit = nextSuit
|
||||
val toMaxArmor = player.MaxArmor
|
||||
if (originalSuit != nextSuit || originalSubtype != nextSubtype || originalArmor > toMaxArmor) {
|
||||
player.History(HealFromExoSuitChange(PlayerSource(player), nextSuit))
|
||||
player.Armor = toMaxArmor
|
||||
}
|
||||
else {
|
||||
player.Armor = originalArmor
|
||||
}
|
||||
//ensure arm is down, even if it needs to go back up
|
||||
if (player.DrawnSlot != Player.HandsDownSlot) {
|
||||
player.DrawnSlot = Player.HandsDownSlot
|
||||
}
|
||||
//a change due to exo-suit permissions mismatch will result in (more) items being re-arranged and/or dropped
|
||||
//dropped items are not registered and can just be forgotten
|
||||
val (afterHolsters, afterInventory) = if (nextSuit == exosuit) {
|
||||
(
|
||||
//melee slot preservation for MAX
|
||||
if (nextSuit == ExoSuitType.MAX) {
|
||||
holsters.filter(_.start == 4)
|
||||
}
|
||||
else {
|
||||
curatedHolsters.filterNot(dropPred)
|
||||
},
|
||||
curatedInventory.filterNot(dropPred)
|
||||
)
|
||||
}
|
||||
else {
|
||||
//our exo-suit type was hijacked by changing permissions; we shouldn't even be able to use that loadout(!)
|
||||
//holsters
|
||||
val leftoversForInventory = Players.fillEmptyHolsters(
|
||||
player.Holsters().iterator,
|
||||
(curatedHolsters ++ curatedInventory).filterNot(dropPred)
|
||||
)
|
||||
val finalHolsters = player.Holsters()
|
||||
.zipWithIndex
|
||||
.collect { case (slot, index) if slot.Equipment.nonEmpty => InventoryItem(slot.Equipment.get, index) }
|
||||
.toList
|
||||
//inventory
|
||||
val (finalInventory, _) = GridInventory.recoverInventory(leftoversForInventory, player.Inventory)
|
||||
(finalHolsters, finalInventory)
|
||||
}
|
||||
(afterHolsters ++ afterInventory).foreach { entry => entry.obj.Faction = player.Faction }
|
||||
toDeleteOrDrop.foreach { entry => entry.obj.Faction = PlanetSideEmpire.NEUTRAL }
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id,
|
||||
AvatarAction.ChangeLoadout(player.GUID, nextSuit, nextSubtype, player.LastDrawnSlot, exosuit == ExoSuitType.MAX,
|
||||
oldHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterHolsters,
|
||||
oldInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterInventory, toDeleteOrDrop)
|
||||
)
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true))
|
||||
|
||||
case _ => ; //terminal messages not handled here
|
||||
}
|
||||
|
||||
case Zone.Ground.ItemOnGround(item, _, _) => ;
|
||||
val name = player.Name
|
||||
val zone = player.Zone
|
||||
val avatarEvents = zone.AvatarEvents
|
||||
val localEvents = zone.LocalEvents
|
||||
item match {
|
||||
case trigger : BoomerTrigger =>
|
||||
//dropped the trigger, no longer own the boomer; make certain whole faction is aware of that
|
||||
(zone.GUID(trigger.Companion), zone.Players.find { _.name == name}) match {
|
||||
case (Some(boomer : BoomerDeployable), Some(avatar)) =>
|
||||
val guid = boomer.GUID
|
||||
val factionChannel = boomer.Faction.toString
|
||||
if(avatar.Deployables.Remove(boomer)) {
|
||||
boomer.Faction = PlanetSideEmpire.NEUTRAL
|
||||
boomer.AssignOwnership(None)
|
||||
avatar.Deployables.UpdateUIElement(boomer.Definition.Item).foreach { case (currElem, curr, maxElem, max) =>
|
||||
avatarEvents ! AvatarServiceMessage(name, AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, maxElem, max))
|
||||
avatarEvents ! AvatarServiceMessage(name, AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, currElem, curr))
|
||||
}
|
||||
localEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(boomer, zone))
|
||||
localEvents ! LocalServiceMessage(factionChannel,
|
||||
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss,
|
||||
DeployableInfo(guid, DeployableIcon.Boomer, boomer.Position, PlanetSideGUID(0))
|
||||
)
|
||||
)
|
||||
avatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, PlanetSideEmpire.NEUTRAL))
|
||||
}
|
||||
case _ => ; //pointless trigger? or a trigger being deleted?
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
|
||||
case Zone.Ground.CanNotDropItem(_, item, reason) =>
|
||||
log.warn(s"${player.Name} tried to drop a ${item.Definition.Name} on the ground, but it $reason")
|
||||
|
||||
case Zone.Ground.ItemInHand(_) => ;
|
||||
|
||||
case Zone.Ground.CanNotPickupItem(_, item_guid, reason) =>
|
||||
log.warn(s"${player.Name} failed to pick up an item ($item_guid) from the ground because $reason")
|
||||
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
|
|
@ -257,12 +533,11 @@ class PlayerControl(player : Player) extends Actor
|
|||
override def StartJammeredStatus(target : Any, dur : Int) : Unit = target match {
|
||||
case obj : Player =>
|
||||
//TODO these features
|
||||
val guid = obj.GUID
|
||||
val zone = obj.Zone
|
||||
val zoneId = zone.Id
|
||||
val events = zone.AvatarEvents
|
||||
|
||||
for(slot <- 0 to player.Implants.length - 1) { // Deactivate & uninitialize all implants
|
||||
// val guid = obj.GUID
|
||||
// val zone = obj.Zone
|
||||
// val zoneId = zone.Id
|
||||
// val events = zone.AvatarEvents
|
||||
player.Implants.indices.foreach { slot => // Deactivate & uninitialize all implants
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
|
||||
self ! Player.ImplantActivation(slot, 0)
|
||||
PlayerControl.UninitializeImplant(player, slot)
|
||||
|
|
@ -274,7 +549,7 @@ class PlayerControl(player : Player) extends Actor
|
|||
}
|
||||
|
||||
override def CancelJammeredStatus(target: Any): Unit = {
|
||||
for(slot <- 0 to player.Implants.length - 1) { // Start reinitializing all implants
|
||||
player.Implants.indices.foreach { slot => // Start reinitializing all implants
|
||||
self ! Player.ImplantInitializationStart(slot)
|
||||
}
|
||||
|
||||
|
|
@ -299,6 +574,101 @@ class PlayerControl(player : Player) extends Actor
|
|||
else {
|
||||
item.FireMode.Modifiers.Damage3
|
||||
}
|
||||
|
||||
def MessageDeferredCallback(msg : Any) : Unit = {
|
||||
msg match {
|
||||
case Containable.MoveItem(_, item, _) =>
|
||||
//momentarily put item back where it was originally
|
||||
val obj = ContainerObject
|
||||
obj.Find(item) match {
|
||||
case Some(slot) =>
|
||||
obj.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||
player.Name,
|
||||
AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectAttachMessage(obj.GUID, item.GUID, slot))
|
||||
)
|
||||
case None => ;
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
|
||||
def RemoveItemFromSlotCallback(item : Equipment, slot : Int) : Unit = {
|
||||
val obj = ContainerObject
|
||||
val zone = obj.Zone
|
||||
val name = player.Name
|
||||
val toChannel = if(obj.VisibleSlots.contains(slot) || obj.isBackpack) zone.Id else name
|
||||
val events = zone.AvatarEvents
|
||||
item.Faction = PlanetSideEmpire.NEUTRAL
|
||||
if(slot == obj.DrawnSlot) {
|
||||
obj.DrawnSlot = Player.HandsDownSlot
|
||||
}
|
||||
events ! AvatarServiceMessage(toChannel, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID))
|
||||
}
|
||||
|
||||
def PutItemInSlotCallback(item : Equipment, slot : Int) : Unit = {
|
||||
val obj = ContainerObject
|
||||
val guid = obj.GUID
|
||||
val zone = obj.Zone
|
||||
val events = zone.AvatarEvents
|
||||
val name = player.Name
|
||||
val definition = item.Definition
|
||||
val msg = AvatarAction.SendResponse(
|
||||
Service.defaultPlayerGUID,
|
||||
ObjectCreateDetailedMessage(
|
||||
definition.ObjectId,
|
||||
item.GUID,
|
||||
ObjectCreateMessageParent(guid, slot),
|
||||
definition.Packet.DetailedConstructorData(item).get
|
||||
)
|
||||
)
|
||||
if(obj.isBackpack) {
|
||||
item.Faction = PlanetSideEmpire.NEUTRAL
|
||||
events ! AvatarServiceMessage(zone.Id, msg)
|
||||
}
|
||||
else {
|
||||
val faction = obj.Faction
|
||||
item.Faction = faction
|
||||
events ! AvatarServiceMessage(name, msg)
|
||||
if(obj.VisibleSlots.contains(slot)) {
|
||||
events ! AvatarServiceMessage(zone.Id, AvatarAction.EquipmentInHand(guid, guid, slot, item))
|
||||
}
|
||||
//handle specific types of items
|
||||
item match {
|
||||
case trigger : BoomerTrigger =>
|
||||
//pick up the trigger, own the boomer; make certain whole faction is aware of that
|
||||
(zone.GUID(trigger.Companion), zone.Players.find { _.name == name }) match {
|
||||
case (Some(boomer : BoomerDeployable), Some(avatar))
|
||||
if !boomer.OwnerName.contains(name) || boomer.Faction != faction =>
|
||||
val bguid = boomer.GUID
|
||||
val faction = player.Faction
|
||||
val factionChannel = faction.toString
|
||||
if(avatar.Deployables.Add(boomer)) {
|
||||
boomer.Faction = faction
|
||||
boomer.AssignOwnership(player)
|
||||
avatar.Deployables.UpdateUIElement(boomer.Definition.Item).foreach { case (currElem, curr, maxElem, max) =>
|
||||
events ! AvatarServiceMessage(name, AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, maxElem, max))
|
||||
events ! AvatarServiceMessage(name, AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, currElem, curr))
|
||||
}
|
||||
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(boomer), zone))
|
||||
events ! AvatarServiceMessage(factionChannel, AvatarAction.SetEmpire(Service.defaultPlayerGUID, bguid, faction))
|
||||
zone.LocalEvents ! LocalServiceMessage(factionChannel,
|
||||
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build,
|
||||
DeployableInfo(bguid, DeployableIcon.Boomer, boomer.Position, boomer.Owner.getOrElse(PlanetSideGUID(0)))
|
||||
)
|
||||
)
|
||||
}
|
||||
case _ => ; //pointless trigger?
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def SwapItemCallback(item : Equipment) : Unit = {
|
||||
val obj = ContainerObject
|
||||
val zone = obj.Zone
|
||||
zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectDetachMessage(obj.GUID, item.GUID, Vector3.Zero, 0f)))
|
||||
}
|
||||
}
|
||||
|
||||
object PlayerControl {
|
||||
|
|
@ -389,19 +759,20 @@ object PlayerControl {
|
|||
//unjam
|
||||
target.Actor ! JammableUnit.ClearJammeredSound()
|
||||
target.Actor ! JammableUnit.ClearJammeredStatus()
|
||||
events ! AvatarServiceMessage(nameChannel, AvatarAction.Killed(player_guid)) //align client interface fields with state
|
||||
events ! AvatarServiceMessage(nameChannel, AvatarAction.Killed(player_guid, target.VehicleSeated)) //align client interface fields with state
|
||||
zone.GUID(target.VehicleSeated) match {
|
||||
case Some(obj : Mountable) =>
|
||||
//boot cadaver from seat
|
||||
events ! AvatarServiceMessage(nameChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID,
|
||||
ObjectDetachMessage(obj.GUID, player_guid, target.Position, Vector3.Zero))
|
||||
)
|
||||
//boot cadaver from seat internally (vehicle perspective)
|
||||
obj.PassengerInSeat(target) match {
|
||||
case Some(index) =>
|
||||
obj.Seats(index).Occupant = None
|
||||
case _ => ;
|
||||
}
|
||||
//make player invisible
|
||||
//boot cadaver from seat on client
|
||||
events ! AvatarServiceMessage(nameChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID,
|
||||
ObjectDetachMessage(obj.GUID, player_guid, target.Position, Vector3.Zero))
|
||||
)
|
||||
//make player invisible on client
|
||||
events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 29, 1))
|
||||
//only the dead player should "see" their own body, so that the death camera has something to focus on
|
||||
events ! AvatarServiceMessage(zoneChannel, AvatarAction.ObjectDelete(player_guid, player_guid))
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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] = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class TaskResolver() extends Actor {
|
|||
private def GiveTask(aTask : Task) : Unit = {
|
||||
val entry : TaskResolver.TaskEntry = TaskResolver.TaskEntry(aTask)
|
||||
tasks += entry
|
||||
trace(s"enqueue and start task $aTask")
|
||||
trace(s"enqueue and start task ${aTask.Description}")
|
||||
entry.Execute(self)
|
||||
StartTimeoutCheck()
|
||||
}
|
||||
|
|
@ -113,12 +113,13 @@ class TaskResolver() extends Actor {
|
|||
private def QueueSubtasks(task : Task, subtasks : List[TaskResolver.GiveTask], resolver : ActorRef = ActorRef.noSender) : Unit = {
|
||||
val entry : TaskResolver.TaskEntry = TaskResolver.TaskEntry(task, subtasks.map(task => task.task), resolver)
|
||||
tasks += entry
|
||||
trace(s"enqueue task $task")
|
||||
trace(s"enqueue task ${task.Description}")
|
||||
if(subtasks.isEmpty) { //a leaf in terms of task dependency; so, not dependent on any other work
|
||||
trace(s"start task $task")
|
||||
trace(s"start task ${task.Description}")
|
||||
entry.Execute(self)
|
||||
}
|
||||
else {
|
||||
trace(s"enqueuing ${subtasks.length} substask(s) belonging to ${task.Description}")
|
||||
subtasks.foreach({subtask =>
|
||||
context.parent ! TaskResolver.GiveSubtask(subtask.task, subtask.subs, self) //route back to submit subtask to pool
|
||||
})
|
||||
|
|
@ -161,7 +162,7 @@ class TaskResolver() extends Actor {
|
|||
private def GeneralOnSuccess(index : Int) : Unit = {
|
||||
val entry = tasks(index)
|
||||
entry.task.onSuccess()
|
||||
trace(s"success with this task ${entry.task}")
|
||||
trace(s"success with task ${entry.task.Description}")
|
||||
if(entry.supertaskRef != ActorRef.noSender) {
|
||||
entry.supertaskRef ! TaskResolver.CompletedSubtask(entry.task) //alert our dependent task's resolver that we have completed
|
||||
}
|
||||
|
|
@ -178,7 +179,7 @@ class TaskResolver() extends Actor {
|
|||
case Some(index) =>
|
||||
val entry = tasks(index)
|
||||
if(TaskResolver.filterCompletionMatch(entry.subtasks.iterator, Task.Resolution.Success)) {
|
||||
trace(s"start new task ${entry.task}")
|
||||
trace(s"start new task ${entry.task.Description}")
|
||||
entry.Execute(self)
|
||||
StartTimeoutCheck()
|
||||
}
|
||||
|
|
@ -225,7 +226,7 @@ class TaskResolver() extends Actor {
|
|||
private def GeneralOnFailure(index : Int, ex : Throwable) : Unit = {
|
||||
val entry = tasks(index)
|
||||
val task = entry.task
|
||||
trace(s"failure with this task $task")
|
||||
trace(s"failure with task ${task.Description}")
|
||||
task.onAbort(ex)
|
||||
task.onFailure(ex)
|
||||
if(entry.supertaskRef != ActorRef.noSender) {
|
||||
|
|
@ -268,7 +269,7 @@ class TaskResolver() extends Actor {
|
|||
private def PropagateAbort(index : Int, ex : Throwable) : Unit = {
|
||||
tasks(index).subtasks.foreach({subtask =>
|
||||
if(subtask.isComplete == Task.Resolution.Success) {
|
||||
trace(s"aborting task $subtask")
|
||||
trace(s"aborting task ${subtask.Description}")
|
||||
subtask.onAbort(ex)
|
||||
}
|
||||
context.parent ! Broadcast(TaskResolver.AbortTask(subtask, ex))
|
||||
|
|
|
|||
|
|
@ -110,7 +110,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.
|
||||
|
|
@ -188,17 +188,19 @@ class GridInventory extends Container {
|
|||
}
|
||||
else {
|
||||
val collisions : mutable.Set[InventoryItem] = mutable.Set[InventoryItem]()
|
||||
items.values.foreach({ item : InventoryItem =>
|
||||
val actualItemStart : Int = item.start - offset
|
||||
val itemx : Int = actualItemStart % width
|
||||
val itemy : Int = actualItemStart / width
|
||||
val tile = item.obj.Tile
|
||||
val clipsOnX : Boolean = if(itemx < startx) { itemx + tile.Width > startx } else { itemx <= startw }
|
||||
val clipsOnY : Boolean = if(itemy < starty) { itemy + tile.Height > starty } else { itemy <= starth }
|
||||
if(clipsOnX && clipsOnY) {
|
||||
collisions += item
|
||||
items
|
||||
.map { case (_, item : InventoryItem) => item }
|
||||
.foreach { item : InventoryItem =>
|
||||
val actualItemStart : Int = item.start - offset
|
||||
val itemx : Int = actualItemStart % width
|
||||
val itemy : Int = actualItemStart / width
|
||||
val tile = item.obj.Tile
|
||||
val clipsOnX : Boolean = if(itemx < startx) { itemx + tile.Width > startx } else { itemx <= startw }
|
||||
val clipsOnY : Boolean = if(itemy < starty) { itemy + tile.Height > starty } else { itemy <= starth }
|
||||
if(clipsOnX && clipsOnY) {
|
||||
collisions += item
|
||||
}
|
||||
}
|
||||
})
|
||||
Success(collisions.toList)
|
||||
}
|
||||
}
|
||||
|
|
@ -578,7 +580,7 @@ class GridInventory extends Container {
|
|||
def Clear() : List[InventoryItem] = {
|
||||
val list = items.values.toList
|
||||
items.clear
|
||||
//entryIndex.set(0)
|
||||
entryIndex.set(0)
|
||||
grid = SetCellsOnlyNoOffset(0, width, height)
|
||||
list
|
||||
}
|
||||
|
|
@ -779,4 +781,17 @@ object GridInventory {
|
|||
node.down.get(node.x, node.y + height, node.width, node.height - height)
|
||||
node.right.get(node.x + width, node.y, node.width - width, height)
|
||||
}
|
||||
|
||||
def toPrintedList(inv : GridInventory) : String = {
|
||||
val list = new StringBuilder
|
||||
list.append("\n")
|
||||
inv.Items.zipWithIndex.foreach { case (InventoryItem(obj, start), index) =>
|
||||
list.append(s"${index+1}: ${obj.Definition.Name}@${obj.GUID} -> $start\n")
|
||||
}
|
||||
list.toString
|
||||
}
|
||||
|
||||
def toPrintedGrid(inv : GridInventory) : String = {
|
||||
new StringBuilder().append("\n").append(inv.grid.toSeq.grouped(inv.width).mkString("\n")).toString
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,632 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.objects.serverobject.containable
|
||||
|
||||
import akka.actor.{Actor, ActorRef}
|
||||
import akka.pattern.{AskTimeoutException, ask}
|
||||
import akka.util.Timeout
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.objects.inventory.{Container, InventoryItem}
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects.{BoomerTrigger, GlobalDefinitions, Player}
|
||||
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/** Parent of all standard (input) messages handled by a `ContainableBehavior` object for the purposes of item transfer */
|
||||
sealed trait ContainableMsg
|
||||
/** `ContainableBehavior` messages that are allowed to be temporarily blocked in event of a complicated item transfer */
|
||||
sealed trait DeferrableMsg extends ContainableMsg
|
||||
|
||||
/**
|
||||
* A mixin for handling synchronized movement of `Equipment` items into or out from `Container` entities.
|
||||
* The most important feature of this synchronization is the movmement of equipment
|
||||
* out from one container into another container
|
||||
* without causing representation overlap, overwriting, or unintended stacking of other equipment
|
||||
* including equipment that has nort yet been inserted.
|
||||
*/
|
||||
trait ContainableBehavior {
|
||||
_ : Actor =>
|
||||
def ContainerObject : PlanetSideServerObject with Container
|
||||
|
||||
/**
|
||||
* A flag for handling deferred messages during an attempt at complicated item movement (`MoveItem`) procedures.
|
||||
* Complicated item movement procedures generally occur when the source and the destination are not the same container.
|
||||
* The flag is set to `1` on the destination and is designed to block interference other normal insertion messages
|
||||
* by taking those messages and pushing them back into the mailbox after a short delay.
|
||||
* If two attempts on the same destination occur due to extremely coincidental item movement messages,
|
||||
* the flag is set to `2` and most all messages involving item movement and item insertion are deferred.
|
||||
* The destination is set back to normal - flag to `0` - when both of the attempts short-circuit due to timeout.
|
||||
*/
|
||||
private var waitOnMoveItemOps : Int = 0
|
||||
|
||||
final val containerBehavior : Receive = {
|
||||
/* messages that modify delivery order */
|
||||
case ContainableBehavior.Wait() => Wait()
|
||||
|
||||
case ContainableBehavior.Resume() => Resume()
|
||||
|
||||
case repeatMsg @ ContainableBehavior.Defer(msg, sentBy) =>
|
||||
//received a previously blocked message; is it still blocked?
|
||||
msg match {
|
||||
case _ : ContainableMsg if waitOnMoveItemOps == 2 => RepeatMessageLater(repeatMsg)
|
||||
case _ : DeferrableMsg if waitOnMoveItemOps == 1 => RepeatMessageLater(repeatMsg)
|
||||
case _ => self.tell(msg, sentBy)
|
||||
}
|
||||
|
||||
case msg : ContainableMsg if waitOnMoveItemOps == 2 =>
|
||||
//all standard messages are blocked
|
||||
RepeatMessageLater(ContainableBehavior.Defer(msg, sender))
|
||||
MessageDeferredCallback(msg)
|
||||
|
||||
case msg : DeferrableMsg if waitOnMoveItemOps == 1 =>
|
||||
//insertion messages not related to an item move attempt are blocked
|
||||
RepeatMessageLater(ContainableBehavior.Defer(msg, sender))
|
||||
MessageDeferredCallback(msg)
|
||||
|
||||
/* normal messages */
|
||||
case Containable.RemoveItemFromSlot(None, Some(slot)) =>
|
||||
sender ! LocalRemoveItemFromSlot(slot)
|
||||
|
||||
case Containable.RemoveItemFromSlot(Some(item), _) =>
|
||||
sender ! LocalRemoveItemFromSlot(item)
|
||||
|
||||
case Containable.PutItemInSlot(item, dest) => /* can be deferred */
|
||||
sender ! LocalPutItemInSlot(item, dest)
|
||||
|
||||
case Containable.PutItemInSlotOnly(item, dest) => /* can be deferred */
|
||||
sender ! LocalPutItemInSlotOnly(item, dest)
|
||||
|
||||
case Containable.PutItemAway(item) => /* can be deferred */
|
||||
sender ! LocalPutItemAway(item)
|
||||
|
||||
case Containable.PutItemInSlotOrAway(item, dest) => /* can be deferred */
|
||||
sender ! LocalPutItemInSlotOrAway(item, dest)
|
||||
|
||||
case msg @ Containable.MoveItem(destination, equipment, destSlot) => /* can be deferred */
|
||||
if(ContainableBehavior.TestPutItemInSlot(destination, equipment, destSlot).nonEmpty) { //test early, before we try to move the item
|
||||
val source = ContainerObject
|
||||
val item = equipment
|
||||
val dest = destSlot
|
||||
LocalRemoveItemFromSlot(item) match {
|
||||
case Containable.ItemFromSlot(_, Some(_), slot @ Some(originalSlot)) =>
|
||||
if(source eq destination) {
|
||||
//when source and destination are the same, moving the item can be performed in one pass
|
||||
LocalPutItemInSlot(item, dest) match {
|
||||
case Containable.ItemPutInSlot(_, _, _, None) => ; //success
|
||||
case Containable.ItemPutInSlot(_, _, _, Some(swapItem)) => //success, but with swap item
|
||||
LocalPutItemInSlotOnlyOrAway(swapItem, slot) match {
|
||||
case Containable.ItemPutInSlot(_, _, _, None) => ;
|
||||
case _ =>
|
||||
source.Zone.Ground.tell(Zone.Ground.DropItem(swapItem, source.Position, Vector3.z(source.Orientation.z)), source.Actor) //drop it
|
||||
}
|
||||
case _ : Containable.CanNotPutItemInSlot => //failure case ; try restore original item placement
|
||||
LocalPutItemInSlot(item, originalSlot)
|
||||
}
|
||||
}
|
||||
else {
|
||||
//destination sync
|
||||
destination.Actor ! ContainableBehavior.Wait()
|
||||
implicit val timeout = new Timeout(1000 milliseconds)
|
||||
val moveItemOver = ask(destination.Actor, ContainableBehavior.MoveItemPutItemInSlot(item, dest))
|
||||
moveItemOver.onComplete {
|
||||
case Success(Containable.ItemPutInSlot(_, _, _, None)) => ; //successful
|
||||
|
||||
case Success(Containable.ItemPutInSlot(_, _, _, Some(swapItem))) => //successful, but with swap item
|
||||
PutItBackOrDropIt(source, swapItem, slot, destination.Actor)
|
||||
|
||||
case Success(_ : Containable.CanNotPutItemInSlot) => //failure case ; try restore original item placement
|
||||
PutItBackOrDropIt(source, item, slot, source.Actor)
|
||||
|
||||
case Failure(_) => //failure case ; try restore original item placement
|
||||
PutItBackOrDropIt(source, item, slot, source.Actor)
|
||||
|
||||
case _ => ; //TODO what?
|
||||
}
|
||||
//always do this
|
||||
moveItemOver
|
||||
.recover { case _ : AskTimeoutException => destination.Actor ! ContainableBehavior.Resume() }
|
||||
.onComplete { _ => destination.Actor ! ContainableBehavior.Resume() }
|
||||
}
|
||||
case _ => ;
|
||||
//we could not find the item to be moved in the source location; trying to act on old data?
|
||||
}
|
||||
}
|
||||
else {
|
||||
MessageDeferredCallback(msg)
|
||||
}
|
||||
|
||||
case ContainableBehavior.MoveItemPutItemInSlot(item, dest) =>
|
||||
sender ! LocalPutItemInSlot(item, dest)
|
||||
|
||||
case ContainableBehavior.MoveItemPutItemInSlotOrAway(item, dest) =>
|
||||
sender ! LocalPutItemInSlotOrAway(item, dest)
|
||||
}
|
||||
|
||||
/* Functions (message control) */
|
||||
|
||||
/**
|
||||
* Defer a message until later.
|
||||
* @see `ContainableBehavior.Defer`
|
||||
* @see `DeferrableMsg`
|
||||
* @param msg the message to defer
|
||||
*/
|
||||
def RepeatMessageLater(msg : Any) : Unit = {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
context.system.scheduler.scheduleOnce(100 milliseconds, self, msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the flag for blocking messages.
|
||||
*/
|
||||
def Wait() : Unit = {
|
||||
waitOnMoveItemOps = math.min(waitOnMoveItemOps + 1, 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the flag for blocking messages.
|
||||
*/
|
||||
def Resume() : Unit = {
|
||||
waitOnMoveItemOps = math.max(0, waitOnMoveItemOps - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop blocking messages.
|
||||
*/
|
||||
def Reset() : Unit = {
|
||||
waitOnMoveItemOps = 0
|
||||
}
|
||||
|
||||
/* Functions (item transfer) */
|
||||
|
||||
private def LocalRemoveItemFromSlot(slot : Int) : Any = {
|
||||
val source = ContainerObject
|
||||
val (outSlot, item) = ContainableBehavior.TryRemoveItemFromSlot(source, slot)
|
||||
item match {
|
||||
case Some(thing) => RemoveItemFromSlotCallback(thing, outSlot.get)
|
||||
case None => ;
|
||||
}
|
||||
Containable.ItemFromSlot(source, item, outSlot)
|
||||
}
|
||||
|
||||
private def LocalRemoveItemFromSlot(item : Equipment) : Any = {
|
||||
val source = ContainerObject
|
||||
val(slot, retItem) = ContainableBehavior.TryRemoveItemFromSlot(source, item)
|
||||
retItem match {
|
||||
case Some(thing) => RemoveItemFromSlotCallback(thing, slot.get)
|
||||
case None => ;
|
||||
}
|
||||
Containable.ItemFromSlot(source, Some(item), slot)
|
||||
}
|
||||
|
||||
private def LocalPutItemInSlot(item : Equipment, dest : Int) : Any = {
|
||||
val destination = ContainerObject
|
||||
ContainableBehavior.TryPutItemInSlot(destination, item, dest) match {
|
||||
case (true, swapItem) =>
|
||||
swapItem match {
|
||||
case Some(thing) => SwapItemCallback(thing)
|
||||
case None => ;
|
||||
}
|
||||
PutItemInSlotCallback(item, dest)
|
||||
Containable.ItemPutInSlot(destination, item, dest, swapItem)
|
||||
case (false, _) =>
|
||||
Containable.CanNotPutItemInSlot(destination, item, dest)
|
||||
}
|
||||
}
|
||||
|
||||
private def LocalPutItemInSlotOnly(item : Equipment, dest : Int) : Any = {
|
||||
val destination = ContainerObject
|
||||
if(ContainableBehavior.TryPutItemInSlotOnly(destination, item, dest)) {
|
||||
PutItemInSlotCallback(item, dest)
|
||||
Containable.ItemPutInSlot(destination, item, dest, None)
|
||||
}
|
||||
else {
|
||||
Containable.CanNotPutItemInSlot(destination, item, dest)
|
||||
}
|
||||
}
|
||||
|
||||
private def LocalPutItemAway(item : Equipment) : Any = {
|
||||
val destination = ContainerObject
|
||||
ContainableBehavior.TryPutItemAway(destination, item) match {
|
||||
case Some(dest) =>
|
||||
PutItemInSlotCallback(item, dest)
|
||||
Containable.ItemPutInSlot(destination, item, dest, None)
|
||||
case _ =>
|
||||
Containable.CanNotPutItemInSlot(destination, item, -1)
|
||||
}
|
||||
}
|
||||
|
||||
private def LocalPutItemInSlotOrAway(item : Equipment, dest : Option[Int]) : Any = {
|
||||
val destination = ContainerObject
|
||||
ContainableBehavior.TryPutItemInSlotOrAway(destination, item, dest) match {
|
||||
case (Some(slot), swapItem) =>
|
||||
swapItem match {
|
||||
case Some(thing) => SwapItemCallback(thing)
|
||||
case None => ;
|
||||
}
|
||||
PutItemInSlotCallback(item, slot)
|
||||
Containable.ItemPutInSlot(destination, item, slot, swapItem)
|
||||
case (None, _) =>
|
||||
Containable.CanNotPutItemInSlot(destination, item, dest.getOrElse(-1))
|
||||
}
|
||||
}
|
||||
|
||||
private def LocalPutItemInSlotOnlyOrAway(item : Equipment, dest : Option[Int]) : Any = {
|
||||
val destination = ContainerObject
|
||||
ContainableBehavior.TryPutItemInSlotOnlyOrAway(destination, item, dest) match {
|
||||
case (Some(slot), None) =>
|
||||
PutItemInSlotCallback(item, slot)
|
||||
Containable.ItemPutInSlot(destination, item, slot, None)
|
||||
case _ =>
|
||||
Containable.CanNotPutItemInSlot(destination, item, dest.getOrElse(-1))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A controlled response where, in certain situations,
|
||||
* it is appropriate to attempt to place an item into a specific container,
|
||||
* first testing a specific slot,
|
||||
* and attempting anywhere available in the container if not that slot,
|
||||
* and, if nowhere is available, then it gets dropped on the ground.
|
||||
* The inserted item is not permitted to swap places with another item in this case.
|
||||
* @param container the container
|
||||
* @param item the item to be inserted
|
||||
* @param slot in which slot the insertion is prioritized (upper left corner of item)
|
||||
* @param to a recipient to redirect the response message
|
||||
* @param timeout how long the request has to complete before expiring
|
||||
*/
|
||||
private def PutItBackOrDropIt(container : PlanetSideServerObject with Container, item : Equipment, slot : Option[Int], to : ActorRef)(implicit timeout : Timeout) : Unit = {
|
||||
val restore = ask(container.Actor, ContainableBehavior.MoveItemPutItemInSlotOrAway(item, slot))
|
||||
restore.onComplete {
|
||||
case Success(_ : Containable.CanNotPutItemInSlot) =>
|
||||
container.Zone.Ground.tell(Zone.Ground.DropItem(item, container.Position, Vector3.z(container.Orientation.z)), to)
|
||||
|
||||
case Failure(_) =>
|
||||
container.Zone.Ground.tell(Zone.Ground.DropItem(item, container.Position, Vector3.z(container.Orientation.z)), to)
|
||||
|
||||
case _ => ; //normal success; //TODO what?
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reaction to the initial deferrence of a message that should handle the visual aspects of not immediately addressing the message.
|
||||
* To be implemented.
|
||||
* @param msg the deferred message
|
||||
*/
|
||||
def MessageDeferredCallback(msg : Any) : Unit
|
||||
|
||||
/**
|
||||
* Reaction to an item being removed a container.
|
||||
* To be implemented.
|
||||
* @param item the item that was removed
|
||||
* @param slot the slot from which is was removed
|
||||
*/
|
||||
def RemoveItemFromSlotCallback(item : Equipment, slot : Int) : Unit
|
||||
|
||||
/**
|
||||
* Reaction to an item being placed into a container.
|
||||
* To be implemented.
|
||||
* @param item the item that was removed
|
||||
* @param slot the slot from which is was removed
|
||||
*/
|
||||
def PutItemInSlotCallback(item : Equipment, slot : Int) : Unit
|
||||
|
||||
/**
|
||||
* Reaction to the existence of a swap item being produced from a container into the environment.
|
||||
* To be implemented.
|
||||
* @param item the item that was removed
|
||||
*/
|
||||
def SwapItemCallback(item : Equipment) : Unit
|
||||
}
|
||||
|
||||
object ContainableBehavior {
|
||||
/** Control message for temporarily blocking some messages to maintain integrity of underlying `Container` object */
|
||||
private case class Wait()
|
||||
/** Control message for unblocking all messages */
|
||||
private case class Resume()
|
||||
/** Internal message for the purpose of refreshing a blocked message in the mailbox */
|
||||
private case class Defer(msg : Any, from : ActorRef)
|
||||
|
||||
/* The same as `PutItemInSlot`, but is not a `DeferrableMsg` for the purposes of completing a `MoveItem` */
|
||||
private case class MoveItemPutItemInSlot(item : Equipment, slot : Int) extends ContainableMsg
|
||||
/* The same as `PutItemInSlotOrAway`, but is not a `DeferrableMsg` for the purposes of completing a `MoveItem` */
|
||||
private case class MoveItemPutItemInSlotOrAway(item : Equipment, slot : Option[Int]) extends ContainableMsg
|
||||
|
||||
/* Functions */
|
||||
|
||||
/**
|
||||
* If the target item can be found in a container, remove the item from the container.
|
||||
* This process can fail if the item can not be found or if it can not be removed for some reason.
|
||||
* @see `Container.Find`
|
||||
* @see `EquipmentSlot.Equipment`
|
||||
* @param source the container in which the `item` is currently located
|
||||
* @param item the item to be removed
|
||||
* @return a `Tuple` of two optional values;
|
||||
* the first is from what index in the container the `item` was removed, if it was removed;
|
||||
* the second is the item again, if it has been removed;
|
||||
* will use `(None, None)` to report failure
|
||||
*/
|
||||
def TryRemoveItemFromSlot(source : PlanetSideServerObject with Container, item : Equipment) : (Option[Int], Option[Equipment]) = {
|
||||
source.Find(item) match {
|
||||
case slot @ Some(index) =>
|
||||
source.Slot(index).Equipment = None
|
||||
if(source.Slot(index).Equipment.isEmpty) {
|
||||
(slot, Some(item))
|
||||
}
|
||||
else {
|
||||
(None, None)
|
||||
}
|
||||
case None =>
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the target slot of a container contains an item, remove that item from the container
|
||||
* fromthe upper left corner position of the item as found in the container.
|
||||
* This process can fail if no item can be found or if it can not be removed for some reason.
|
||||
* @see `Container.Find`
|
||||
* @see `EquipmentSlot.Equipment`
|
||||
* @param source the container in which the `slot` is to be searched
|
||||
* @param slot where the container will be searched
|
||||
* @return a `Tuple` of two values;
|
||||
* the first is from what `slot` in the container an `item` was removed, if any item removed;
|
||||
* the second is the item, if it has been removed;
|
||||
* will use `(None, None)` to report failure
|
||||
*/
|
||||
def TryRemoveItemFromSlot(source : PlanetSideServerObject with Container, slot : Int) : (Option[Int], Option[Equipment]) = {
|
||||
val (item, outSlot) = source.Slot(slot).Equipment match {
|
||||
case Some(thing) => (Some(thing), source.Find(thing))
|
||||
case None => (None, None)
|
||||
}
|
||||
source.Slot(slot).Equipment = None
|
||||
item match {
|
||||
case Some(_) if item.nonEmpty && source.Slot(slot).Equipment.isEmpty =>
|
||||
(outSlot, item)
|
||||
case None =>
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Are the conditions for an item insertion acceptable?
|
||||
* If another item occupies the expected region of insertion (collision of bounding regions),
|
||||
* the insertion can still be permitted with the assumption that
|
||||
* the displaced item ("swap item") will have to be put somewhere else.
|
||||
* @see `ContainableBehavior.PermitEquipmentStow`
|
||||
* @see `Container.Collisions`
|
||||
* @see `InventoryTile`
|
||||
* @param destination the container
|
||||
* @param item the item to be tested for insertion
|
||||
* @param dest the upper left corner of the insertion position
|
||||
* @return the results of the insertion test, if an insertion can be permitted;
|
||||
* `None`, otherwise, and the insertion is not permitted
|
||||
*/
|
||||
def TestPutItemInSlot(destination : PlanetSideServerObject with Container, item : Equipment, dest : Int) : Option[List[InventoryItem]] = {
|
||||
if(ContainableBehavior.PermitEquipmentStow(destination, item)) {
|
||||
val tile = item.Definition.Tile
|
||||
val destinationCollisionTest = destination.Collisions(dest, tile.Width, tile.Height)
|
||||
destinationCollisionTest match {
|
||||
case Success(Nil) => Some(Nil) //no item to swap
|
||||
case Success(out @ List(_)) => Some(out) //one item to swap
|
||||
case _ => None //abort when too many items at destination or other failure case
|
||||
}
|
||||
}
|
||||
else {
|
||||
None //blocked insertion (object type not permitted in container)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put an item in a container at the given position.
|
||||
* The inserted item may swap places with another item.
|
||||
* If the new item can not be inserted, the swap item is kept in its original position.
|
||||
* @param destination the container
|
||||
* @param item the item to be inserted
|
||||
* @param dest in which slot the insertion is expected to occur (upper left corner of item)
|
||||
* @return a `Tuple` of two values;
|
||||
* the first is `true` if the insertion occurred; and, `false`, otherwise
|
||||
* the second is an optional item that was removed from a coincidental position in the container ("swap item")
|
||||
*/
|
||||
def TryPutItemInSlot(destination : PlanetSideServerObject with Container, item : Equipment, dest : Int) : (Boolean, Option[Equipment]) = {
|
||||
ContainableBehavior.TestPutItemInSlot(destination, item, dest) match {
|
||||
case Some(results) =>
|
||||
//insert and swap, if applicable
|
||||
val (swapItem, swapSlot) = results match {
|
||||
case List(InventoryItem(obj, start)) => (Some(obj), start)
|
||||
case _ => (None, dest)
|
||||
}
|
||||
destination.Slot(swapSlot).Equipment = None
|
||||
if((destination.Slot(dest).Equipment = item).contains(item)) {
|
||||
(true, swapItem)
|
||||
}
|
||||
else {
|
||||
//put the swapItem back
|
||||
destination.Slot(swapSlot).Equipment = swapItem
|
||||
(false, None)
|
||||
}
|
||||
case None =>
|
||||
(false, None)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put an item in a container at the given position.
|
||||
* The inserted item is not permitted to swap places with another item in this case.
|
||||
* @param destination the container
|
||||
* @param item the item to be inserted
|
||||
* @param dest in which slot the insertion is expected to occur (upper left corner of item)
|
||||
* @return `true` if the insertion occurred;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def TryPutItemInSlotOnly(destination : PlanetSideServerObject with Container, item : Equipment, dest : Int) : Boolean = {
|
||||
ContainableBehavior.TestPutItemInSlot(destination, item, dest).contains(Nil) && (destination.Slot(dest).Equipment = item).contains(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Put an item in a container in the whatever position it cleanly fits.
|
||||
* The inserted item will not swap places with another item in this case.
|
||||
* @param destination the container
|
||||
* @param item the item to be inserted
|
||||
* @return the slot index of the insertion point;
|
||||
* `None`, if a clean insertion is not possible
|
||||
*/
|
||||
def TryPutItemAway(destination : PlanetSideServerObject with Container, item : Equipment) : Option[Int] = {
|
||||
destination.Fit(item) match {
|
||||
case out @ Some(dest)
|
||||
if ContainableBehavior.PermitEquipmentStow(destination, item) && (destination.Slot(dest).Equipment = item).contains(item) =>
|
||||
out
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to put an item in a container at the given position.
|
||||
* The inserted item may swap places with another item at this time.
|
||||
* If the targeted insertion at this position fails,
|
||||
* attempt to put the item in the container in the whatever position it cleanly fits.
|
||||
* @param destination the container
|
||||
* @param item the item to be inserted
|
||||
* @param dest in which specific slot the insertion is first tested (upper left corner of item)
|
||||
* @return na
|
||||
*/
|
||||
def TryPutItemInSlotOrAway(destination : PlanetSideServerObject with Container, item : Equipment, dest : Option[Int]) : (Option[Int], Option[Equipment]) = {
|
||||
(dest match {
|
||||
case Some(slot) => ContainableBehavior.TryPutItemInSlot(destination, item, slot)
|
||||
case None => (false, None)
|
||||
}) match {
|
||||
case (true, swapItem) =>
|
||||
(dest, swapItem)
|
||||
case _ =>
|
||||
ContainableBehavior.TryPutItemAway(destination, item) match {
|
||||
case out @ Some(_) => (out, None)
|
||||
case None => (None, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to put an item in a container at the given position.
|
||||
* The inserted item may not swap places with another item at this time.
|
||||
* If the targeted insertion at this position fails,
|
||||
* attempt to put the item in the container in the whatever position it cleanly fits.
|
||||
* @param destination the container
|
||||
* @param item the item to be inserted
|
||||
* @param dest in which specific slot the insertion is first tested (upper left corner of item)
|
||||
* @return na
|
||||
*/
|
||||
def TryPutItemInSlotOnlyOrAway(destination : PlanetSideServerObject with Container, item : Equipment, dest : Option[Int]) : (Option[Int], Option[Equipment]) = {
|
||||
(dest match {
|
||||
case Some(slot) if ContainableBehavior.TestPutItemInSlot(destination, item, slot).contains(Nil) => ContainableBehavior.TryPutItemInSlot(destination, item, slot)
|
||||
case None => (false, None)
|
||||
}) match {
|
||||
case (true, swapItem) =>
|
||||
(dest, swapItem)
|
||||
case _ =>
|
||||
ContainableBehavior.TryPutItemAway(destination, item) match {
|
||||
case out @ Some(_) => (out, None)
|
||||
case None => (None, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply incontestable, arbitrary limitations
|
||||
* whereby certain items are denied insertion into certain containers
|
||||
* for vaguely documented but assuredly fantastic excuses on the part of the developer.
|
||||
* @param destination the container
|
||||
* @param equipment the item to be inserted
|
||||
* @return `true`, if the object is allowed to contain the type of equipment object;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def PermitEquipmentStow(destination : PlanetSideServerObject with Container, equipment : Equipment) : Boolean = {
|
||||
import net.psforever.objects.{BoomerTrigger, Player}
|
||||
equipment match {
|
||||
case _ : BoomerTrigger =>
|
||||
//a BoomerTrigger can only be stowed in a player's holsters or inventory
|
||||
//this is only a requirement until they, and their Boomer explosive complement, are cleaned-up properly
|
||||
destination.isInstanceOf[Player]
|
||||
case _ =>
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A predicate used to determine if an `InventoryItem` object contains `Equipment` that should be dropped.
|
||||
* Used to filter through lists of object data before it is placed into a player's inventory.
|
||||
* Drop the item if:<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
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.terminals
|
||||
|
||||
import akka.actor.ActorContext
|
||||
import akka.actor.{ActorContext, ActorRef}
|
||||
import net.psforever.objects.definition.ImplantDefinition
|
||||
import net.psforever.objects.{Default, Player, Vehicle}
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
|
|
@ -92,6 +92,14 @@ class OrderTerminalDefinition(objId : Int) extends TerminalDefinition(objId) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = {
|
||||
tabs.get(msg.msg.item_page) match {
|
||||
case Some(page) =>
|
||||
page.Dispatch(sender, terminal, msg)
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object OrderTerminalDefinition {
|
||||
|
|
@ -100,8 +108,9 @@ object OrderTerminalDefinition {
|
|||
* @see `ItemTransactionMessage`
|
||||
*/
|
||||
sealed trait Tab {
|
||||
def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal()
|
||||
def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange
|
||||
def Sell(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal()
|
||||
def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -119,6 +128,10 @@ object OrderTerminalDefinition {
|
|||
Terminal.NoDeal()
|
||||
}
|
||||
}
|
||||
|
||||
def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = {
|
||||
msg.player.Actor ! msg
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -144,6 +157,13 @@ object OrderTerminalDefinition {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = {
|
||||
msg.response match {
|
||||
case _ : Terminal.BuyExosuit => msg.player.Actor ! msg
|
||||
case _ => sender ! msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -171,6 +191,10 @@ object OrderTerminalDefinition {
|
|||
Terminal.NoDeal()
|
||||
}
|
||||
}
|
||||
|
||||
def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = {
|
||||
sender ! msg
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -187,6 +211,10 @@ object OrderTerminalDefinition {
|
|||
Terminal.NoDeal()
|
||||
}
|
||||
}
|
||||
|
||||
def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = {
|
||||
sender ! msg
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -215,6 +243,10 @@ object OrderTerminalDefinition {
|
|||
Terminal.NoDeal()
|
||||
}
|
||||
}
|
||||
|
||||
def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = {
|
||||
sender ! msg
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -272,6 +304,10 @@ object OrderTerminalDefinition {
|
|||
Terminal.NoDeal()
|
||||
}
|
||||
}
|
||||
|
||||
def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = {
|
||||
msg.player.Actor ! msg
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -300,6 +336,14 @@ object OrderTerminalDefinition {
|
|||
Terminal.NoDeal()
|
||||
}
|
||||
}
|
||||
|
||||
def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = {
|
||||
val player = msg.player
|
||||
player.Zone.GUID(player.VehicleOwned) match {
|
||||
case Some(vehicle : Vehicle) => vehicle.Actor ! msg
|
||||
case _ => sender ! Terminal.TerminalMessage(player, msg.msg, Terminal.NoDeal())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -331,6 +375,10 @@ object OrderTerminalDefinition {
|
|||
Terminal.NoDeal()
|
||||
}
|
||||
}
|
||||
|
||||
def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = {
|
||||
sender ! msg
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
// Copyright (c) 2017-2020 PSForever
|
||||
package net.psforever.objects.vehicles
|
||||
|
||||
import akka.actor.{Actor, ActorRef, Cancellable}
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.ballistics.{ResolvedProjectile, VehicleSource}
|
||||
import net.psforever.objects.equipment.JammableMountedWeapons
|
||||
import net.psforever.objects.equipment.{Equipment, EquipmentSlot, JammableMountedWeapons}
|
||||
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
|
||||
import net.psforever.objects.serverobject.CommonMessages
|
||||
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
|
||||
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
|
||||
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
|
||||
import net.psforever.objects.serverobject.damage.DamageableVehicle
|
||||
import net.psforever.objects.serverobject.deploy.DeploymentBehavior
|
||||
import net.psforever.objects.serverobject.hackable.GenericHackables
|
||||
import net.psforever.objects.serverobject.repair.RepairableVehicle
|
||||
import net.psforever.objects.serverobject.terminals.Terminal
|
||||
import net.psforever.objects.vital.VehicleShieldCharge
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.types.{DriveState, ExoSuitType, PlanetSideGUID, Vector3}
|
||||
import services.{RemoverActor, Service}
|
||||
import net.psforever.types._
|
||||
import services.RemoverActor
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
|
||||
import services.Service
|
||||
import services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
|
@ -36,7 +43,8 @@ class VehicleControl(vehicle : Vehicle) extends Actor
|
|||
with CargoBehavior
|
||||
with DamageableVehicle
|
||||
with RepairableVehicle
|
||||
with JammableMountedWeapons {
|
||||
with JammableMountedWeapons
|
||||
with ContainableBehavior {
|
||||
|
||||
//make control actors belonging to utilities when making control actor belonging to vehicle
|
||||
vehicle.Utilities.foreach({case (_, util) => util.Setup })
|
||||
|
|
@ -48,6 +56,7 @@ class VehicleControl(vehicle : Vehicle) extends Actor
|
|||
def DeploymentObject = vehicle
|
||||
def DamageableObject = vehicle
|
||||
def RepairableObject = vehicle
|
||||
def ContainerObject = vehicle
|
||||
|
||||
/** cheap flag for whether the vehicle is decaying */
|
||||
var decaying : Boolean = false
|
||||
|
|
@ -72,6 +81,7 @@ class VehicleControl(vehicle : Vehicle) extends Actor
|
|||
.orElse(jammableBehavior)
|
||||
.orElse(takesDamage)
|
||||
.orElse(canBeRepairedByNanoDispenser)
|
||||
.orElse(containerBehavior)
|
||||
.orElse {
|
||||
case Vehicle.Ownership(None) =>
|
||||
LoseOwnership()
|
||||
|
|
@ -143,6 +153,52 @@ class VehicleControl(vehicle : Vehicle) extends Actor
|
|||
)
|
||||
}
|
||||
|
||||
case Terminal.TerminalMessage(player, msg, reply) =>
|
||||
reply match {
|
||||
case Terminal.VehicleLoadout(definition, weapons, inventory) =>
|
||||
org.log4s.getLogger(vehicle.Definition.Name).info(s"changing vehicle equipment loadout to ${player.Name}'s option #${msg.unk1 + 1}")
|
||||
//remove old inventory
|
||||
val oldInventory = vehicle.Inventory.Clear().map { case InventoryItem(obj, _) => (obj, obj.GUID) }
|
||||
//"dropped" items are lost; if it doesn't go in the trunk, it vanishes into the nanite cloud
|
||||
val (_, afterInventory) = inventory.partition(ContainableBehavior.DropPredicate(player))
|
||||
val (oldWeapons, newWeapons, finalInventory) = if(vehicle.Definition == definition) {
|
||||
//vehicles are the same type
|
||||
//TODO want to completely swap weapons, but holster icon vanishes temporarily after swap
|
||||
//TODO BFR arms must be swapped properly
|
||||
// //remove old weapons
|
||||
// val oldWeapons = vehicle.Weapons.values.collect { case slot if slot.Equipment.nonEmpty =>
|
||||
// val obj = slot.Equipment.get
|
||||
// slot.Equipment = None
|
||||
// (obj, obj.GUID)
|
||||
// }.toList
|
||||
// (oldWeapons, weapons, afterInventory)
|
||||
//TODO for now, just refill ammo; assume weapons stay the same
|
||||
vehicle.Weapons
|
||||
.collect { case (_, slot : EquipmentSlot) if slot.Equipment.nonEmpty => slot.Equipment.get }
|
||||
.collect { case weapon : Tool =>
|
||||
weapon.AmmoSlots.foreach { ammo => ammo.Box.Capacity = ammo.Box.Definition.Capacity }
|
||||
}
|
||||
(Nil, Nil, afterInventory)
|
||||
}
|
||||
else {
|
||||
//vehicle loadout is not for this vehicle
|
||||
//do not transfer over weapon ammo
|
||||
if(vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset) {
|
||||
(Nil, Nil, afterInventory) //trunk is the same dimensions, however
|
||||
}
|
||||
else {
|
||||
//accommodate as much of inventory as possible
|
||||
val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory)
|
||||
(Nil, Nil, stow)
|
||||
}
|
||||
}
|
||||
finalInventory.foreach { _.obj.Faction = vehicle.Faction }
|
||||
player.Zone.VehicleEvents ! VehicleServiceMessage(player.Zone.Id, VehicleAction.ChangeLoadout(vehicle.GUID, oldWeapons, newWeapons, oldInventory, finalInventory))
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true))
|
||||
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
case Vehicle.Deconstruct(time) =>
|
||||
time match {
|
||||
case Some(delay) =>
|
||||
|
|
@ -261,6 +317,73 @@ class VehicleControl(vehicle : Vehicle) extends Actor
|
|||
case None => ;
|
||||
}
|
||||
}
|
||||
|
||||
def MessageDeferredCallback(msg : Any) : Unit = {
|
||||
msg match {
|
||||
case Containable.MoveItem(_, item, _) =>
|
||||
//momentarily put item back where it was originally
|
||||
val obj = ContainerObject
|
||||
obj.Find(item) match {
|
||||
case Some(slot) =>
|
||||
obj.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||
self.toString,
|
||||
AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectAttachMessage(obj.GUID, item.GUID, slot))
|
||||
)
|
||||
case None => ;
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
|
||||
def RemoveItemFromSlotCallback(item : Equipment, slot : Int) : Unit = {
|
||||
val zone = ContainerObject.Zone
|
||||
zone.VehicleEvents ! VehicleServiceMessage(self.toString, VehicleAction.UnstowEquipment(Service.defaultPlayerGUID, item.GUID))
|
||||
}
|
||||
|
||||
def PutItemInSlotCallback(item : Equipment, slot : Int) : Unit = {
|
||||
val obj = ContainerObject
|
||||
val oguid = obj.GUID
|
||||
val zone = obj.Zone
|
||||
val channel = self.toString
|
||||
val events = zone.VehicleEvents
|
||||
val iguid = item.GUID
|
||||
val definition = item.Definition
|
||||
item.Faction = obj.Faction
|
||||
events ! VehicleServiceMessage(
|
||||
//TODO when a new weapon, the equipment slot ui goes blank, but the weapon functions; remount vehicle to correct it
|
||||
if(obj.VisibleSlots.contains(slot)) zone.Id else channel,
|
||||
VehicleAction.SendResponse(
|
||||
Service.defaultPlayerGUID,
|
||||
ObjectCreateMessage(
|
||||
definition.ObjectId,
|
||||
iguid,
|
||||
ObjectCreateMessageParent(oguid, slot),
|
||||
definition.Packet.ConstructorData(item).get
|
||||
)
|
||||
)
|
||||
)
|
||||
item match {
|
||||
case box : AmmoBox =>
|
||||
events ! VehicleServiceMessage(
|
||||
channel,
|
||||
VehicleAction.InventoryState2(Service.defaultPlayerGUID, iguid, oguid, box.Capacity)
|
||||
)
|
||||
case weapon : Tool =>
|
||||
weapon.AmmoSlots.map { slot => slot.Box }.foreach { box =>
|
||||
events ! VehicleServiceMessage(
|
||||
channel,
|
||||
VehicleAction.InventoryState2(Service.defaultPlayerGUID, iguid, weapon.GUID, box.Capacity)
|
||||
)
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
|
||||
def SwapItemCallback(item : Equipment) : Unit = {
|
||||
val obj = ContainerObject
|
||||
val zone = obj.Zone
|
||||
zone.VehicleEvents ! VehicleServiceMessage(self.toString, VehicleAction.SendResponse(Service.defaultPlayerGUID, ObjectDetachMessage(obj.GUID, item.GUID, Vector3.Zero, 0f)))
|
||||
}
|
||||
}
|
||||
|
||||
object VehicleControl {
|
||||
|
|
|
|||
|
|
@ -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 _ => ;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,23 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player
|
|||
}
|
||||
|
||||
case Zone.Corpse.Add(player) =>
|
||||
if(CorpseAdd(player, corpseList)) {
|
||||
//player can be a corpse if they are in the current zone or are not in any zone
|
||||
//player is "found" if their avatar can be matched by name within this zone and it has a character
|
||||
val (canBeCorpse, playerNotFound) = if(player.Zone == zone) {
|
||||
playerMap.find { case (a, _) => a.name == player.Name } match {
|
||||
case Some((a, Some(p))) if p eq player =>
|
||||
PopulationRelease(a, playerMap)
|
||||
(true, false)
|
||||
case Some((a, None)) =>
|
||||
(true, true)
|
||||
case _ =>
|
||||
(false, false)
|
||||
}
|
||||
}
|
||||
else {
|
||||
(player.Zone == Zone.Nowhere, true)
|
||||
}
|
||||
if(canBeCorpse && CorpseAdd(player, corpseList) && playerNotFound) {
|
||||
player.Actor = context.actorOf(Props(classOf[PlayerControl], player), name = s"corpse_of_${GetPlayerControlName(player, None)}")
|
||||
player.Zone = zone
|
||||
}
|
||||
|
|
@ -219,4 +235,4 @@ object ZonePopulationActor {
|
|||
s"${player.CharId}_${player.GUID.guid}_${System.currentTimeMillis}" //new
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -94,7 +94,7 @@ class AvatarService(zone : Zone) extends Actor {
|
|||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.DestroyDisplay(killer, victim, method, unk))
|
||||
)
|
||||
case AvatarAction.DropItem(player_guid, item, _) =>
|
||||
case AvatarAction.DropItem(player_guid, item) =>
|
||||
val definition = item.Definition
|
||||
val objectData = DroppedItemData(
|
||||
PlacementData(item.Position, item.Orientation),
|
||||
|
|
@ -123,9 +123,9 @@ class AvatarService(zone : Zone) extends Actor {
|
|||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.HitHint(source_guid))
|
||||
)
|
||||
case AvatarAction.Killed(player_guid) =>
|
||||
case AvatarAction.Killed(player_guid, mount_guid) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Killed())
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Killed(mount_guid))
|
||||
)
|
||||
case AvatarAction.LoadPlayer(player_guid, object_id, target_guid, cdata, pdata) =>
|
||||
val pkt = pdata match {
|
||||
|
|
@ -179,21 +179,10 @@ class AvatarService(zone : Zone) extends Actor {
|
|||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ProjectileState(projectile_guid, shot_pos, shot_vel, shot_orient, sequence, end, target))
|
||||
)
|
||||
case AvatarAction.PickupItem(player_guid, _, target, slot, item, unk) =>
|
||||
case AvatarAction.PickupItem(player_guid, item, unk) =>
|
||||
janitor forward RemoverActor.ClearSpecific(List(item), zone)
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, {
|
||||
val itemGUID = item.GUID
|
||||
if(target.VisibleSlots.contains(slot)) {
|
||||
val definition = item.Definition
|
||||
val containerData = ObjectCreateMessageParent(target.GUID, slot)
|
||||
val objectData = definition.Packet.ConstructorData(item).get
|
||||
AvatarResponse.EquipmentInHand(ObjectCreateMessage(definition.ObjectId, itemGUID, containerData, objectData))
|
||||
}
|
||||
else {
|
||||
AvatarResponse.ObjectDelete(itemGUID, unk)
|
||||
}
|
||||
})
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ObjectDelete(item.GUID, unk))
|
||||
)
|
||||
case AvatarAction.PutDownFDU(player_guid) =>
|
||||
AvatarEvents.publish(
|
||||
|
|
@ -238,6 +227,19 @@ class AvatarService(zone : Zone) extends Actor {
|
|||
AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.TeardownConnection())
|
||||
)
|
||||
|
||||
case AvatarAction.TerminalOrderResult(terminal, term_action, result) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.TerminalOrderResult(terminal, term_action, result))
|
||||
)
|
||||
case AvatarAction.ChangeExosuit(target, exosuit, subtype, slot, maxhand, old_holsters, holsters, old_inventory, inventory, drop, delete) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.ChangeExosuit(target, exosuit, subtype, slot, maxhand, old_holsters, holsters, old_inventory, inventory, drop, delete))
|
||||
)
|
||||
case AvatarAction.ChangeLoadout(target, exosuit, subtype, slot, maxhand, old_holsters, holsters, old_inventory, inventory, drop) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.ChangeLoadout(target, exosuit, subtype, slot, maxhand, old_holsters, holsters, old_inventory, inventory, drop))
|
||||
)
|
||||
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
|
|
@ -259,6 +261,7 @@ class AvatarService(zone : Zone) extends Actor {
|
|||
))
|
||||
}
|
||||
*/
|
||||
|
||||
case msg =>
|
||||
log.warn(s"Unhandled message $msg from $sender")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ import net.psforever.objects.{PlanetSideGameObject, Player}
|
|||
import net.psforever.objects.ballistics.{Projectile, SourceEntry}
|
||||
import net.psforever.objects.ce.Deployable
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.objects.inventory.Container
|
||||
import net.psforever.objects.inventory.{Container, InventoryItem}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.PlanetSideGamePacket
|
||||
import net.psforever.packet.game.ImplantAction
|
||||
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent}
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
|
|
@ -36,11 +35,11 @@ object AvatarAction {
|
|||
final case class ActivateImplantSlot(player_guid : PlanetSideGUID, slot : Int) extends Action
|
||||
final case class Destroy(victim : PlanetSideGUID, killer : PlanetSideGUID, weapon : PlanetSideGUID, pos : Vector3) extends Action
|
||||
final case class DestroyDisplay(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int = 121) extends Action
|
||||
final case class DropItem(player_guid : PlanetSideGUID, item : Equipment, zone : Zone) extends Action
|
||||
final case class DropItem(player_guid : PlanetSideGUID, item : Equipment) extends Action
|
||||
final case class EquipmentInHand(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action
|
||||
final case class GenericObjectAction(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, action_code : Int) extends Action
|
||||
final case class HitHint(source_guid : PlanetSideGUID, player_guid : PlanetSideGUID) extends Action
|
||||
final case class Killed(player_guid : PlanetSideGUID) extends Action
|
||||
final case class Killed(player_guid : PlanetSideGUID, mount_guid : Option[PlanetSideGUID]) extends Action
|
||||
final case class LoadPlayer(player_guid : PlanetSideGUID, object_id : Int, target_guid : PlanetSideGUID, cdata : ConstructorData, pdata : Option[ObjectCreateMessageParent]) extends Action
|
||||
final case class LoadProjectile(player_guid : PlanetSideGUID, object_id : Int, projectile_guid : PlanetSideGUID, cdata : ConstructorData) extends Action
|
||||
final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action
|
||||
|
|
@ -49,7 +48,7 @@ object AvatarAction {
|
|||
final case class PlanetsideAttributeToAll(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
|
||||
final case class PlanetsideAttributeSelf(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
|
||||
final case class PlayerState(player_guid : PlanetSideGUID, pos : Vector3, vel : Option[Vector3], facingYaw : Float, facingPitch : Float, facingYawUpper : Float, timestamp : Int, is_crouching : Boolean, is_jumping : Boolean, jump_thrust : Boolean, is_cloaked : Boolean, spectator : Boolean, weaponInHand : Boolean) extends Action
|
||||
final case class PickupItem(player_guid : PlanetSideGUID, zone : Zone, target : PlanetSideGameObject with Container, slot : Int, item : Equipment, unk : Int = 0) extends Action
|
||||
final case class PickupItem(player_guid : PlanetSideGUID, item : Equipment, unk : Int = 0) extends Action
|
||||
final case class ProjectileAutoLockAwareness(mode : Int) extends Action
|
||||
final case class ProjectileExplodes(player_guid : PlanetSideGUID, projectile_guid : PlanetSideGUID, projectile : Projectile) extends Action
|
||||
final case class ProjectileState(player_guid : PlanetSideGUID, projectile_guid : PlanetSideGUID, shot_pos : Vector3, shot_vel : Vector3, shot_orient : Vector3, sequence : Int, end : Boolean, hit_target : PlanetSideGUID) extends Action
|
||||
|
|
@ -64,6 +63,10 @@ object AvatarAction {
|
|||
final case class SendResponse(player_guid: PlanetSideGUID, msg: PlanetSideGamePacket) extends Action
|
||||
final case class SendResponseTargeted(target_guid: PlanetSideGUID, msg: PlanetSideGamePacket) extends Action
|
||||
|
||||
final case class TerminalOrderResult(terminal_guid : PlanetSideGUID, action : TransactionType.Value, result : Boolean) extends Action
|
||||
final case class ChangeExosuit(target_guid : PlanetSideGUID, exosuit : ExoSuitType.Value, subtype : Int, last_drawn_slot : Int, new_max_hand : Boolean, old_holsters : List[(Equipment, PlanetSideGUID)], holsters : List[InventoryItem], old_inventory : List[(Equipment, PlanetSideGUID)], inventory : List[InventoryItem], drop : List[InventoryItem], delete : List[(Equipment, PlanetSideGUID)]) extends Action
|
||||
final case class ChangeLoadout(target_guid : PlanetSideGUID, exosuit : ExoSuitType.Value, subtype : Int, last_drawn_slot : Int, new_max_hand : Boolean, old_holsters : List[(Equipment, PlanetSideGUID)], holsters : List[InventoryItem], old_inventory : List[(Equipment, PlanetSideGUID)], inventory : List[InventoryItem], drop : List[InventoryItem]) extends Action
|
||||
|
||||
final case class TeardownConnection() extends Action
|
||||
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||
// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ package services.avatar
|
|||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.ballistics.{Projectile, SourceEntry}
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.objects.inventory.InventoryItem
|
||||
import net.psforever.packet.PlanetSideGamePacket
|
||||
import net.psforever.packet.game.objectcreate.ConstructorData
|
||||
import net.psforever.packet.game.{ImplantAction, ObjectCreateMessage}
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||
import net.psforever.packet.game.ObjectCreateMessage
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
|
||||
import services.GenericEventBusMsg
|
||||
|
||||
final case class AvatarServiceResponse(toChannel : String,
|
||||
|
|
@ -33,7 +34,7 @@ object AvatarResponse {
|
|||
final case class EquipmentInHand(pkt : ObjectCreateMessage) extends Response
|
||||
final case class GenericObjectAction(object_guid : PlanetSideGUID, action_code : Int) extends Response
|
||||
final case class HitHint(source_guid : PlanetSideGUID) extends Response
|
||||
final case class Killed() extends Response
|
||||
final case class Killed(mount_guid : Option[PlanetSideGUID]) extends Response
|
||||
final case class LoadPlayer(pkt : ObjectCreateMessage) extends Response
|
||||
final case class LoadProjectile(pkt : ObjectCreateMessage) extends Response
|
||||
final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response
|
||||
|
|
@ -56,6 +57,10 @@ object AvatarResponse {
|
|||
final case class SendResponse(msg: PlanetSideGamePacket) extends Response
|
||||
final case class SendResponseTargeted(target_guid : PlanetSideGUID, msg: PlanetSideGamePacket) extends Response
|
||||
|
||||
final case class TerminalOrderResult(terminal_guid : PlanetSideGUID, action : TransactionType.Value, result : Boolean) extends Response
|
||||
final case class ChangeExosuit(target_guid : PlanetSideGUID, exosuit : ExoSuitType.Value, subtype : Int, last_drawn_slot : Int, new_max_hand : Boolean, old_holsters : List[(Equipment, PlanetSideGUID)], holsters : List[InventoryItem], old_inventory : List[(Equipment, PlanetSideGUID)], inventory : List[InventoryItem], drop : List[InventoryItem], delete : List[(Equipment, PlanetSideGUID)]) extends Response
|
||||
final case class ChangeLoadout(target_guid : PlanetSideGUID, exosuit : ExoSuitType.Value, subtype : Int, last_drawn_slot : Int, new_max_hand : Boolean, old_holsters : List[(Equipment, PlanetSideGUID)], holsters : List[InventoryItem], old_inventory : List[(Equipment, PlanetSideGUID)], inventory : List[InventoryItem], drop : List[InventoryItem]) extends Response
|
||||
|
||||
final case class TeardownConnection() extends Response
|
||||
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 _ => ;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,10 +32,12 @@ class PlayerControlHealTest extends ActorTest {
|
|||
player1.Zone = zone
|
||||
player1.Spawn
|
||||
player1.Position = Vector3(2, 0, 0)
|
||||
guid.register(player1.Locker, 5)
|
||||
player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control")
|
||||
val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2
|
||||
player2.Zone = zone
|
||||
player2.Spawn
|
||||
guid.register(player2.Locker, 6)
|
||||
player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control")
|
||||
|
||||
val tool = Tool(GlobalDefinitions.medicalapplicator) //guid=3 & 4
|
||||
|
|
@ -102,6 +104,7 @@ class PlayerControlHealSelfTest extends ActorTest {
|
|||
player1.Zone = zone
|
||||
player1.Spawn
|
||||
player1.Position = Vector3(2, 0, 0)
|
||||
guid.register(player1.Locker, 5)
|
||||
player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control")
|
||||
|
||||
val tool = Tool(GlobalDefinitions.medicalapplicator) //guid=3 & 4
|
||||
|
|
@ -167,10 +170,12 @@ class PlayerControlRepairTest extends ActorTest {
|
|||
player1.Zone = zone
|
||||
player1.Spawn
|
||||
player1.Position = Vector3(2, 0, 0)
|
||||
guid.register(player1.Locker, 5)
|
||||
player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control")
|
||||
val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2
|
||||
player2.Zone = zone
|
||||
player2.Spawn
|
||||
guid.register(player2.Locker, 6)
|
||||
player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control")
|
||||
|
||||
val tool = Tool(GlobalDefinitions.bank) //guid=3 & 4
|
||||
|
|
@ -243,6 +248,7 @@ class PlayerControlRepairSelfTest extends ActorTest {
|
|||
player1.Zone = zone
|
||||
player1.Spawn
|
||||
player1.Position = Vector3(2, 0, 0)
|
||||
guid.register(player1.Locker, 5)
|
||||
player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control")
|
||||
|
||||
val tool = Tool(GlobalDefinitions.bank) //guid=3 & 4
|
||||
|
|
@ -309,10 +315,12 @@ class PlayerControlDamageTest extends ActorTest {
|
|||
player1.Zone = zone
|
||||
player1.Spawn
|
||||
player1.Position = Vector3(2, 0, 0)
|
||||
guid.register(player1.Locker, 5)
|
||||
player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control")
|
||||
val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2
|
||||
player2.Zone = zone
|
||||
player2.Spawn
|
||||
guid.register(player2.Locker, 6)
|
||||
player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control")
|
||||
val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4
|
||||
val projectile = tool.Projectile
|
||||
|
|
@ -385,10 +393,12 @@ class PlayerControlDeathStandingTest extends ActorTest {
|
|||
player1.Zone = zone
|
||||
player1.Spawn
|
||||
player1.Position = Vector3(2,0,0)
|
||||
guid.register(player1.Locker, 5)
|
||||
player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control")
|
||||
val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2
|
||||
player2.Zone = zone
|
||||
player2.Spawn
|
||||
guid.register(player2.Locker, 6)
|
||||
player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control")
|
||||
|
||||
val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4
|
||||
|
|
@ -430,7 +440,7 @@ class PlayerControlDeathStandingTest extends ActorTest {
|
|||
)
|
||||
assert(
|
||||
msg_avatar(1) match {
|
||||
case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2))) => true
|
||||
case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), None)) => true
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
|
|
@ -493,10 +503,12 @@ class PlayerControlDeathSeatedTest extends ActorTest {
|
|||
player1.Zone = zone
|
||||
player1.Spawn
|
||||
player1.Position = Vector3(2,0,0)
|
||||
guid.register(player1.Locker, 6)
|
||||
player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control")
|
||||
val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2
|
||||
player2.Zone = zone
|
||||
player2.Spawn
|
||||
guid.register(player2.Locker, 7)
|
||||
player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control")
|
||||
|
||||
val vehicle = Vehicle(GlobalDefinitions.quadstealth) //guid=5
|
||||
|
|
@ -534,7 +546,7 @@ class PlayerControlDeathSeatedTest extends ActorTest {
|
|||
activityProbe.expectNoMessage(200 milliseconds)
|
||||
assert(
|
||||
msg_avatar.head match {
|
||||
case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2))) => true
|
||||
case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), Some(PlanetSideGUID(5)))) => true
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
|
|
@ -595,5 +607,4 @@ class PlayerControlDeathSeatedTest extends ActorTest {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
object PlayerControlTest { }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ object PsLogin {
|
|||
logger.info("Initializing ServiceManager")
|
||||
val serviceManager = ServiceManager.boot
|
||||
serviceManager ! ServiceManager.Register(Props[AccountIntermediaryService], "accountIntermediary")
|
||||
serviceManager ! ServiceManager.Register(RandomPool(50).props(Props[TaskResolver]), "taskResolver")
|
||||
serviceManager ! ServiceManager.Register(RandomPool(150).props(Props[TaskResolver]), "taskResolver")
|
||||
serviceManager ! ServiceManager.Register(Props[ChatService], "chat")
|
||||
serviceManager ! ServiceManager.Register(Props[GalaxyService], "galaxy")
|
||||
serviceManager ! ServiceManager.Register(Props[SquadService], "squad")
|
||||
|
|
|
|||
603
pslogin/src/main/scala/WorldSession.scala
Normal file
603
pslogin/src/main/scala/WorldSession.scala
Normal 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
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue