Merge psforever/master

This commit is contained in:
Mazo 2018-06-05 20:13:25 +01:00
commit 9707e88924
28 changed files with 1713 additions and 972 deletions

View file

@ -16,6 +16,8 @@ import net.psforever.objects.serverobject.tube.SpawnTubeDefinition
import net.psforever.objects.vehicles.{SeatArmorRestriction, UtilityType} import net.psforever.objects.vehicles.{SeatArmorRestriction, UtilityType}
import net.psforever.types.PlanetSideEmpire import net.psforever.types.PlanetSideEmpire
import scala.concurrent.duration._
object GlobalDefinitions { object GlobalDefinitions {
/* /*
Implants Implants
@ -2716,6 +2718,7 @@ object GlobalDefinitions {
ams.Deployment = true ams.Deployment = true
ams.DeployTime = 2000 ams.DeployTime = 2000
ams.UndeployTime = 2000 ams.UndeployTime = 2000
ams.DeconstructionTime = Some(20 minutes)
ams.AutoPilotSpeeds = (18, 6) ams.AutoPilotSpeeds = (18, 6)
ams.Packet = utilityConverter ams.Packet = utilityConverter
@ -2728,6 +2731,7 @@ object GlobalDefinitions {
router.Deployment = true router.Deployment = true
router.DeployTime = 2000 router.DeployTime = 2000
router.UndeployTime = 2000 router.UndeployTime = 2000
router.DeconstructionTime = Duration(20, "minutes")
router.AutoPilotSpeeds = (16, 6) router.AutoPilotSpeeds = (16, 6)
router.Packet = variantConverter router.Packet = variantConverter

View file

@ -129,7 +129,12 @@ class Player(private val core : Avatar) extends PlanetSideGameObject with Factio
def MaxArmor : Int = exosuit.MaxArmor def MaxArmor : Int = exosuit.MaxArmor
def VisibleSlots : Set[Int] = if(exosuit.SuitType == ExoSuitType.MAX) { Set(0) } else { Set(0,1,2,3,4) } def VisibleSlots : Set[Int] = if(exosuit.SuitType == ExoSuitType.MAX) {
Set(0)
}
else {
(0 to 4).filterNot(index => holsters(index).Size == EquipmentSize.Blocked).toSet
}
override def Slot(slot : Int) : EquipmentSlot = { override def Slot(slot : Int) : EquipmentSlot = {
if(inventory.Offset <= slot && slot <= inventory.LastIndex) { if(inventory.Offset <= slot && slot <= inventory.LastIndex) {

View file

@ -6,6 +6,7 @@ import net.psforever.objects.inventory.InventoryTile
import net.psforever.objects.vehicles.UtilityType import net.psforever.objects.vehicles.UtilityType
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.duration._
/** /**
* An object definition system used to construct and retain the parameters of various vehicles. * An object definition system used to construct and retain the parameters of various vehicles.
@ -29,6 +30,7 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) {
private var canCloak : Boolean = false private var canCloak : Boolean = false
private var canBeOwned : Boolean = true private var canBeOwned : Boolean = true
private var serverVehicleOverrideSpeeds : (Int, Int) = (0, 0) private var serverVehicleOverrideSpeeds : (Int, Int) = (0, 0)
private var deconTime : Option[FiniteDuration] = None
Name = "vehicle" Name = "vehicle"
Packet = VehicleDefinition.converter Packet = VehicleDefinition.converter
@ -83,6 +85,18 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) {
DeployTime DeployTime
} }
def DeconstructionTime : Option[FiniteDuration] = deconTime
def DeconstructionTime_=(time : FiniteDuration) : Option[FiniteDuration] = {
deconTime_=(Some(time))
DeconstructionTime
}
def DeconstructionTime_=(time : Option[FiniteDuration]) : Option[FiniteDuration] = {
deconTime = time
DeconstructionTime
}
def UndeployTime : Int = deploymentTime_Undeploy def UndeployTime : Int = deploymentTime_Undeploy
def UndeployTime_=(dtime : Int) : Int = { def UndeployTime_=(dtime : Int) : Int = {

View file

@ -89,7 +89,7 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
if(accessor == ActorRef.noSender) { if(accessor == ActorRef.noSender) {
implicit val guid : NumberPoolHub = this.guid //passed into builderObject.Build implicitly implicit val guid : NumberPoolHub = this.guid //passed into builderObject.Build implicitly
accessor = context.actorOf(RandomPool(25).props(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(guid))), s"$Id-uns") accessor = context.actorOf(RandomPool(25).props(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(guid))), s"$Id-uns")
ground = context.actorOf(Props(classOf[ZoneGroundActor], equipmentOnGround), s"$Id-ground") ground = context.actorOf(Props(classOf[ZoneGroundActor], this, equipmentOnGround), s"$Id-ground")
transport = context.actorOf(Props(classOf[ZoneVehicleActor], this, vehicles), s"$Id-vehicles") transport = context.actorOf(Props(classOf[ZoneVehicleActor], this, vehicles), s"$Id-vehicles")
population = context.actorOf(Props(classOf[ZonePopulationActor], this, players, corpses), s"$Id-players") population = context.actorOf(Props(classOf[ZonePopulationActor], this, players, corpses), s"$Id-players")
@ -386,27 +386,17 @@ object Zone {
final case class NoValidSpawnPoint(zone_number : Int, spawn_group : Option[Int]) final case class NoValidSpawnPoint(zone_number : Int, spawn_group : Option[Int])
} }
/** object Ground {
* Message to relinguish an item and place in on the ground. final case class DropItem(item : Equipment, pos : Vector3, orient : Vector3)
* @param item the piece of `Equipment` final case class ItemOnGround(item : Equipment, pos : Vector3, orient : Vector3)
* @param pos where it is dropped final case class CanNotDropItem(zone : Zone, item : Equipment, reason : String)
* @param orient in which direction it is facing when dropped
*/
final case class DropItemOnGround(item : Equipment, pos : Vector3, orient : Vector3)
/** final case class PickupItem(item_guid : PlanetSideGUID)
* Message to attempt to acquire an item from the ground (before somoene else?). final case class ItemInHand(item : Equipment)
* @param player who wants the piece of `Equipment` final case class CanNotPickupItem(zone : Zone, item_guid : PlanetSideGUID, reason : String)
* @param item_guid the unique identifier of the piece of `Equipment`
*/
final case class GetItemOnGround(player : Player, item_guid : PlanetSideGUID)
/** final case class RemoveItem(item_guid : PlanetSideGUID)
* Message to give an item from the ground to a specific user. }
* @param player who wants the piece of `Equipment`
* @param item the piece of `Equipment`
*/
final case class ItemFromGround(player : Player, item : Equipment)
object Vehicle { object Vehicle {
final case class Spawn(vehicle : Vehicle) final case class Spawn(vehicle : Vehicle)

View file

@ -50,10 +50,10 @@ class ZoneActor(zone : Zone) extends Actor {
zone.Population forward msg zone.Population forward msg
//frwd to Ground Actor //frwd to Ground Actor
case msg @ Zone.DropItemOnGround => case msg @ Zone.Ground.DropItem =>
zone.Ground forward msg zone.Ground forward msg
case msg @ Zone.GetItemOnGround => case msg @ Zone.Ground.PickupItem =>
zone.Ground forward msg zone.Ground forward msg
//frwd to Vehicle Actor //frwd to Vehicle Actor

View file

@ -12,22 +12,35 @@ import scala.collection.mutable.ListBuffer
* na * na
* @param equipmentOnGround a `List` of items (`Equipment`) dropped by players on the ground and can be collected again * @param equipmentOnGround a `List` of items (`Equipment`) dropped by players on the ground and can be collected again
*/ */
class ZoneGroundActor(equipmentOnGround : ListBuffer[Equipment]) extends Actor { class ZoneGroundActor(zone : Zone, equipmentOnGround : ListBuffer[Equipment]) extends Actor {
//private[this] val log = org.log4s.getLogger //private[this] val log = org.log4s.getLogger
def receive : Receive = { def receive : Receive = {
case Zone.DropItemOnGround(item, pos, orient) => case Zone.Ground.DropItem(item, pos, orient) =>
item.Position = pos sender ! (if(!item.HasGUID) {
item.Orientation = orient Zone.Ground.CanNotDropItem(zone, item, "not registered yet")
equipmentOnGround += item
case Zone.GetItemOnGround(player, item_guid) =>
FindItemOnGround(item_guid) match {
case Some(item) =>
sender ! Zone.ItemFromGround(player, item)
case None =>
org.log4s.getLogger.warn(s"item on ground $item_guid was requested by $player for pickup but was not found")
} }
else if(zone.GUID(item.GUID).isEmpty) {
Zone.Ground.CanNotDropItem(zone, item, "registered to some other zone")
}
else if(equipmentOnGround.contains(item)) {
Zone.Ground.CanNotDropItem(zone, item, "already dropped")
}
else {
equipmentOnGround += item
Zone.Ground.ItemOnGround(item, pos, orient)
})
case Zone.Ground.PickupItem(item_guid) =>
sender ! (FindItemOnGround(item_guid) match {
case Some(item) =>
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
case _ => ; case _ => ;
} }

View file

@ -19,13 +19,18 @@ final case class ActionResultMessage(successful : Boolean,
} }
object ActionResultMessage extends Marshallable[ActionResultMessage] { object ActionResultMessage extends Marshallable[ActionResultMessage] {
def apply() : ActionResultMessage = { /**
ActionResultMessage(true, None) * A message where the result is always a pass.
} * @return an `ActionResultMessage` object
*/
def Pass : ActionResultMessage = ActionResultMessage(true, None)
def apply(error : Long) : ActionResultMessage = { /**
ActionResultMessage(false, Some(error)) * A message where the result is always a failure.
} * @param error the error code
* @return an `ActionResultMessage` object
*/
def Fail(error : Long) : ActionResultMessage = ActionResultMessage(false, Some(error))
implicit val codec : Codec[ActionResultMessage] = ( implicit val codec : Codec[ActionResultMessage] = (
("successful" | bool) >>:~ { res => ("successful" | bool) >>:~ { res =>

View file

@ -40,6 +40,14 @@ final case class ObjectDetachMessage(parent_guid : PlanetSideGUID,
} }
object ObjectDetachMessage extends Marshallable[ObjectDetachMessage] { object ObjectDetachMessage extends Marshallable[ObjectDetachMessage] {
def apply(parent_guid : PlanetSideGUID, child_guid : PlanetSideGUID, pos : Vector3, orient : Vector3) : ObjectDetachMessage = {
ObjectDetachMessage(parent_guid, child_guid, pos, orient.x, orient.y, orient.z)
}
def apply(parent_guid : PlanetSideGUID, child_guid : PlanetSideGUID, pos : Vector3, orient_z : Float) : ObjectDetachMessage = {
ObjectDetachMessage(parent_guid, child_guid, pos, 0, 0, orient_z)
}
implicit val codec : Codec[ObjectDetachMessage] = ( implicit val codec : Codec[ObjectDetachMessage] = (
("parent_guid" | PlanetSideGUID.codec) :: ("parent_guid" | PlanetSideGUID.codec) ::
("child_guid" | PlanetSideGUID.codec) :: ("child_guid" | PlanetSideGUID.codec) ::

View file

@ -38,7 +38,7 @@ class ActionResultMessageTest extends Specification {
} }
"encode (pass, minimal)" in { "encode (pass, minimal)" in {
val msg = ActionResultMessage() val msg = ActionResultMessage.Pass
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_pass pkt mustEqual string_pass
@ -52,7 +52,7 @@ class ActionResultMessageTest extends Specification {
} }
"encode (fail, minimal)" in { "encode (fail, minimal)" in {
val msg = ActionResultMessage(1) val msg = ActionResultMessage.Fail(1)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_fail pkt mustEqual string_fail

View file

@ -26,10 +26,24 @@ class ObjectDetachMessageTest extends Specification {
} }
} }
"encode" in { "encode (1)" in {
val msg = ObjectDetachMessage(PlanetSideGUID(2916), PlanetSideGUID(2502), Vector3(3567.1406f, 2988.0078f, 71.84375f), 0f, 0f, 270f) val msg = ObjectDetachMessage(PlanetSideGUID(2916), PlanetSideGUID(2502), Vector3(3567.1406f, 2988.0078f, 71.84375f), 0f, 0f, 270f)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string pkt mustEqual string
} }
"encode (2)" in {
val msg = ObjectDetachMessage(PlanetSideGUID(2916), PlanetSideGUID(2502), Vector3(3567.1406f, 2988.0078f, 71.84375f), Vector3(0f, 0f, 270f))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
"encode (3)" in {
val msg = ObjectDetachMessage(PlanetSideGUID(2916), PlanetSideGUID(2502), Vector3(3567.1406f, 2988.0078f, 71.84375f), 270f)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
} }

View file

@ -155,7 +155,13 @@ class PlayerTest extends Specification {
"has visible slots" in { "has visible slots" in {
val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
obj.VisibleSlots mustEqual Set(0,2,4) //Standard
obj.ExoSuit = ExoSuitType.Agile
obj.VisibleSlots mustEqual Set(0,1,2,4)
obj.ExoSuit = ExoSuitType.Reinforced
obj.VisibleSlots mustEqual Set(0,1,2,3,4) obj.VisibleSlots mustEqual Set(0,1,2,3,4)
obj.ExoSuit = ExoSuitType.Infiltration
obj.VisibleSlots mustEqual Set(0,4)
obj.ExoSuit = ExoSuitType.MAX obj.ExoSuit = ExoSuitType.MAX
obj.VisibleSlots mustEqual Set(0) obj.VisibleSlots mustEqual Set(0)
} }

View file

@ -18,7 +18,7 @@ import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap}
import net.psforever.objects.Vehicle import net.psforever.objects.Vehicle
import org.specs2.mutable.Specification import org.specs2.mutable.Specification
import scala.concurrent.duration.Duration import scala.concurrent.duration._
class ZoneTest extends Specification { class ZoneTest extends Specification {
def test(a: Int, b : Zone, c : ActorContext) : Building = { Building.NoBuilding } def test(a: Int, b : Zone, c : ActorContext) : Building = { Building.NoBuilding }
@ -464,56 +464,183 @@ class ZonePopulationTest extends ActorTest {
} }
} }
class ZoneGroundTest extends ActorTest { class ZoneGroundDropItemTest extends ActorTest {
val item = AmmoBox(GlobalDefinitions.bullet_9mm) val item = AmmoBox(GlobalDefinitions.bullet_9mm)
item.GUID = PlanetSideGUID(10) val hub = new NumberPoolHub(new LimitedNumberSource(20))
hub.register(item, 10)
val zone = new Zone("test", new ZoneMap("test-map"), 0)
zone.GUID(hub)
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!"
"ZoneGroundActor" should { "DropItem" should {
"drop item on ground" in { "drop item on ground" in {
val zone = new Zone("test", new ZoneMap(""), 0) receiveOne(1 second) //consume
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-item-test") ! "!" assert(!zone.EquipmentOnGround.contains(item))
receiveOne(Duration.create(200, "ms")) //consume zone.Ground ! Zone.Ground.DropItem(item, Vector3(1.1f, 2.2f, 3.3f), Vector3(4.4f, 5.5f, 6.6f))
assert(zone.EquipmentOnGround.isEmpty) val reply = receiveOne(200 milliseconds)
assert(item.Position == Vector3.Zero) assert(reply.isInstanceOf[Zone.Ground.ItemOnGround])
assert(item.Orientation == Vector3.Zero) assert(reply.asInstanceOf[Zone.Ground.ItemOnGround].item == item)
zone.Ground ! Zone.DropItemOnGround(item, Vector3(1.1f, 2.2f, 3.3f), Vector3(4.4f, 5.5f, 6.6f)) assert(reply.asInstanceOf[Zone.Ground.ItemOnGround].pos == Vector3(1.1f, 2.2f, 3.3f))
expectNoMsg(Duration.create(100, "ms")) assert(reply.asInstanceOf[Zone.Ground.ItemOnGround].orient == Vector3(4.4f, 5.5f, 6.6f))
assert(zone.EquipmentOnGround.contains(item))
assert(zone.EquipmentOnGround == List(item))
assert(item.Position == Vector3(1.1f, 2.2f, 3.3f))
assert(item.Orientation == Vector3(4.4f, 5.5f, 6.6f))
} }
}
}
"get item from ground (success)" in { class ZoneGroundCanNotDropItem1Test extends ActorTest {
val zone = new Zone("test", new ZoneMap(""), 0) val item = AmmoBox(GlobalDefinitions.bullet_9mm)
val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) val hub = new NumberPoolHub(new LimitedNumberSource(20))
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "get-item-test-good") ! "!" //hub.register(item, 10) //!important
receiveOne(Duration.create(200, "ms")) //consume val zone = new Zone("test", new ZoneMap("test-map"), 0)
zone.Ground ! Zone.DropItemOnGround(item, Vector3.Zero, Vector3.Zero) zone.GUID(hub)
expectNoMsg(Duration.create(100, "ms")) system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!"
assert(zone.EquipmentOnGround == List(item)) "DropItem" should {
zone.Ground ! Zone.GetItemOnGround(player, PlanetSideGUID(10)) "not drop an item that is not registered" in {
val reply = receiveOne(Duration.create(100, "ms")) receiveOne(1 second) //consume
assert(!zone.EquipmentOnGround.contains(item))
zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero)
assert(zone.EquipmentOnGround.isEmpty) val reply = receiveOne(300 milliseconds)
assert(reply.isInstanceOf[Zone.ItemFromGround]) assert(reply.isInstanceOf[Zone.Ground.CanNotDropItem])
assert(reply.asInstanceOf[Zone.ItemFromGround].player == player) assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].item == item)
assert(reply.asInstanceOf[Zone.ItemFromGround].item == item) assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].zone == zone)
assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].reason == "not registered yet")
assert(!zone.EquipmentOnGround.contains(item))
} }
}
}
"get item from ground (failure)" in { class ZoneGroundCanNotDropItem2Test extends ActorTest {
val zone = new Zone("test", new ZoneMap(""), 0) val item = AmmoBox(GlobalDefinitions.bullet_9mm)
val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) val hub = new NumberPoolHub(new LimitedNumberSource(20))
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "get-item-test-fail") ! "!" hub.register(item, 10) //!important
receiveOne(Duration.create(200, "ms")) //consume val zone = new Zone("test", new ZoneMap("test-map"), 0)
zone.Ground ! Zone.DropItemOnGround(item, Vector3.Zero, Vector3.Zero) //zone.GUID(hub) //!important
expectNoMsg(Duration.create(100, "ms")) system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!"
assert(zone.EquipmentOnGround == List(item)) "DropItem" should {
zone.Ground ! Zone.GetItemOnGround(player, PlanetSideGUID(11)) //wrong guid "not drop an item that is not registered to the zone" in {
expectNoMsg(Duration.create(500, "ms")) receiveOne(1 second) //consume
assert(!zone.EquipmentOnGround.contains(item))
zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero)
val reply = receiveOne(300 milliseconds)
assert(reply.isInstanceOf[Zone.Ground.CanNotDropItem])
assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].item == item)
assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].zone == zone)
assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].reason == "registered to some other zone")
assert(!zone.EquipmentOnGround.contains(item))
}
}
}
class ZoneGroundCanNotDropItem3Test extends ActorTest {
val item = AmmoBox(GlobalDefinitions.bullet_9mm)
val hub = new NumberPoolHub(new LimitedNumberSource(20))
hub.register(item, 10) //!important
val zone = new Zone("test", new ZoneMap("test-map"), 0)
zone.GUID(hub) //!important
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!"
"DropItem" should {
"not drop an item that has already been dropped" in {
receiveOne(1 second) //consume
assert(!zone.EquipmentOnGround.contains(item))
assert(zone.EquipmentOnGround.isEmpty)
zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero)
val reply1 = receiveOne(300 milliseconds)
assert(reply1.isInstanceOf[Zone.Ground.ItemOnGround])
assert(reply1.asInstanceOf[Zone.Ground.ItemOnGround].item == item)
assert(zone.EquipmentOnGround.contains(item))
assert(zone.EquipmentOnGround.size == 1)
zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero)
val reply2 = receiveOne(300 milliseconds)
assert(reply2.isInstanceOf[Zone.Ground.CanNotDropItem])
assert(reply2.asInstanceOf[Zone.Ground.CanNotDropItem].item == item)
assert(reply2.asInstanceOf[Zone.Ground.CanNotDropItem].zone == zone)
assert(reply2.asInstanceOf[Zone.Ground.CanNotDropItem].reason == "already dropped")
assert(zone.EquipmentOnGround.size == 1)
}
}
}
class ZoneGroundPickupItemTest extends ActorTest {
val item = AmmoBox(GlobalDefinitions.bullet_9mm)
val hub = new NumberPoolHub(new LimitedNumberSource(20))
hub.register(item, 10)
val zone = new Zone("test", new ZoneMap("test-map"), 0)
zone.GUID(hub)
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!"
"PickupItem" should {
"pickup an item from ground" in {
receiveOne(1 second) //consume
assert(!zone.EquipmentOnGround.contains(item))
zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero)
val reply1 = receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[Zone.Ground.ItemOnGround])
assert(zone.EquipmentOnGround.contains(item))
zone.Ground ! Zone.Ground.PickupItem(item.GUID)
val reply2 = receiveOne(200 milliseconds)
assert(reply2.isInstanceOf[Zone.Ground.ItemInHand])
assert(reply2.asInstanceOf[Zone.Ground.ItemInHand].item == item)
assert(!zone.EquipmentOnGround.contains(item))
}
}
}
class ZoneGroundCanNotPickupItemTest extends ActorTest {
val item = AmmoBox(GlobalDefinitions.bullet_9mm)
val hub = new NumberPoolHub(new LimitedNumberSource(20))
hub.register(item, 10)
val zone = new Zone("test", new ZoneMap("test-map"), 0)
zone.GUID(hub) //still registered to this zone
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!"
"PickupItem" should {
"not pickup an item if it can not be found" in {
receiveOne(1 second) //consume
assert(!zone.EquipmentOnGround.contains(item))
zone.Ground ! Zone.Ground.PickupItem(item.GUID)
val reply2 = receiveOne(200 milliseconds)
assert(reply2.isInstanceOf[Zone.Ground.CanNotPickupItem])
assert(reply2.asInstanceOf[Zone.Ground.CanNotPickupItem].item_guid == item.GUID)
assert(reply2.asInstanceOf[Zone.Ground.CanNotPickupItem].zone == zone)
assert(reply2.asInstanceOf[Zone.Ground.CanNotPickupItem].reason == "can not find")
}
}
}
class ZoneGroundRemoveItemTest extends ActorTest {
val item = AmmoBox(GlobalDefinitions.bullet_9mm)
val hub = new NumberPoolHub(new LimitedNumberSource(20))
hub.register(item, 10)
val zone = new Zone("test", new ZoneMap("test-map"), 0)
zone.GUID(hub) //still registered to this zone
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!"
"RemoveItem" should {
"remove an item from the ground without callback (even if the item is not found)" in {
receiveOne(1 second)
assert(!zone.EquipmentOnGround.contains(item))
zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero)
receiveOne(200 milliseconds)
assert(zone.EquipmentOnGround.contains(item)) //dropped
zone.Ground ! Zone.Ground.RemoveItem(item.GUID)
expectNoMsg(500 milliseconds)
assert(!zone.EquipmentOnGround.contains(item))
zone.Ground ! Zone.Ground.RemoveItem(item.GUID) //repeat
expectNoMsg(500 milliseconds)
assert(!zone.EquipmentOnGround.contains(item))
} }
} }
} }

View file

@ -217,7 +217,7 @@ object PsLogin {
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future import scala.concurrent.Future
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
implicit val timeout = Timeout(200 milliseconds) implicit val timeout = Timeout(500 milliseconds)
val requestVehicleEventBus : Future[ServiceManager.LookupResult] = val requestVehicleEventBus : Future[ServiceManager.LookupResult] =
(ServiceManager.serviceManager ask ServiceManager.Lookup("vehicle")).mapTo[ServiceManager.LookupResult] (ServiceManager.serviceManager ask ServiceManager.Lookup("vehicle")).mapTo[ServiceManager.LookupResult]
requestVehicleEventBus.onComplete { requestVehicleEventBus.onComplete {

View file

@ -38,13 +38,14 @@ import net.psforever.objects.vehicles.{AccessPermissionGroup, Utility, VehicleLo
import net.psforever.objects.zones.{InterstellarCluster, Zone} import net.psforever.objects.zones.{InterstellarCluster, Zone}
import net.psforever.packet.game.objectcreate._ import net.psforever.packet.game.objectcreate._
import net.psforever.types._ import net.psforever.types._
import services._ import services.{RemoverActor, _}
import services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse} import services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse}
import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse}
import services.vehicle.VehicleAction.UnstowEquipment import services.vehicle.VehicleAction.UnstowEquipment
import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.concurrent.duration._
import scala.util.Success import scala.util.Success
class WorldSessionActor extends Actor with MDCContextAware { class WorldSessionActor extends Actor with MDCContextAware {
@ -141,14 +142,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
else { //no items in inventory; leave no corpse else { //no items in inventory; leave no corpse
val player_guid = player.GUID val player_guid = player.GUID
player.Position = Vector3.Zero //save character before doing this player.Position = Vector3.Zero //save character before doing this
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid))
taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID)
} }
case Some(vehicle_guid) => case Some(vehicle_guid) =>
val player_guid = player.GUID val player_guid = player.GUID
player.Position = Vector3.Zero //save character before doing this player.Position = Vector3.Zero //save character before doing this
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid))
taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID)
DismountVehicleOnLogOut() DismountVehicleOnLogOut()
} }
@ -184,7 +185,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Some(vehicle : Vehicle) => case Some(vehicle : Vehicle) =>
vehicle.Seat(vehicle.PassengerInSeat(player).get).get.Occupant = None vehicle.Seat(vehicle.PassengerInSeat(player).get).get.Occupant = None
if(vehicle.Seats.values.count(_.isOccupied) == 0) { if(vehicle.Seats.values.count(_.isOccupied) == 0) {
vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(vehicle, continent, 600L) //start vehicle decay (10m) vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent), vehicle.Definition.DeconstructionTime) //start vehicle decay
} }
vehicleService ! Service.Leave(Some(s"${vehicle.Actor}")) vehicleService ! Service.Leave(Some(s"${vehicle.Actor}"))
@ -256,7 +257,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) => case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) =>
if(tplayer_guid != guid) { if(tplayer_guid != guid) {
sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3(0,0,0), 0f, 0f, 0f)) sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0))
sendResponse( sendResponse(
ObjectCreateMessage( ObjectCreateMessage(
ammo_id, ammo_id,
@ -288,29 +289,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(GenericObjectActionMessage(guid, 36)) sendResponse(GenericObjectActionMessage(guid, 36))
} }
case AvatarResponse.EquipmentInHand(target, slot, item) => case msg @ AvatarResponse.DropItem(pkt) =>
if(tplayer_guid != guid) { if(tplayer_guid != guid) {
val definition = item.Definition sendResponse(pkt)
sendResponse(
ObjectCreateMessage(
definition.ObjectId,
item.GUID,
ObjectCreateMessageParent(target, slot),
definition.Packet.ConstructorData(item).get
)
)
} }
case msg @ AvatarResponse.EquipmentOnGround(pos, orient, item_id, item_guid, item_data) => case AvatarResponse.EquipmentInHand(pkt) =>
if(tplayer_guid != guid) { if(tplayer_guid != guid) {
log.info(s"now dropping a $msg") sendResponse(pkt)
sendResponse(
ObjectCreateMessage(
item_id,
item_guid,
DroppedItemData(PlacementData(pos, Vector3(0f, 0f, orient.z)), item_data)
)
)
} }
case AvatarResponse.LoadPlayer(pdata) => case AvatarResponse.LoadPlayer(pdata) =>
@ -406,7 +392,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
} }
case LocalServiceResponse(_, guid, reply) => case LocalServiceResponse(_, guid, reply) =>
val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(0) } val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(-1) }
reply match { reply match {
case LocalResponse.DoorOpens(door_guid) => case LocalResponse.DoorOpens(door_guid) =>
if(tplayer_guid != guid) { if(tplayer_guid != guid) {
@ -423,7 +409,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(SetEmpireMessage(target_guid, continent.GUID(target_guid).get.asInstanceOf[FactionAffinity].Faction)) sendResponse(SetEmpireMessage(target_guid, continent.GUID(target_guid).get.asInstanceOf[FactionAffinity].Faction))
case LocalResponse.HackObject(target_guid, unk1, unk2) => case LocalResponse.HackObject(target_guid, unk1, unk2) =>
if(player.GUID != guid && continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction != player.Faction) { if(tplayer_guid != guid && continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction != player.Faction) {
// If the player is not in the faction that hacked this object then send the packet that it's been hacked, so they can either unhack it or use the hacked object // If the player is not in the faction that hacked this object then send the packet that it's been hacked, so they can either unhack it or use the hacked object
// Don't send this to the faction that hacked the object, otherwise it will interfere with the new SetEmpireMessage QoL change that changes the object colour to their faction (but only visible to that faction) // Don't send this to the faction that hacked the object, otherwise it will interfere with the new SetEmpireMessage QoL change that changes the object colour to their faction (but only visible to that faction)
sendResponse(HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)) sendResponse(HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2))
@ -435,7 +421,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
} }
case LocalResponse.ProximityTerminalEffect(object_guid, effectState) => case LocalResponse.ProximityTerminalEffect(object_guid, effectState) =>
if(player.GUID != guid) { if(tplayer_guid != guid) {
sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, effectState)) sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, effectState))
} }
@ -476,7 +462,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
} }
case VehicleResponse.DetachFromRails(vehicle_guid, pad_guid, pad_position, pad_orientation_z) => case VehicleResponse.DetachFromRails(vehicle_guid, pad_guid, pad_position, pad_orientation_z) =>
sendResponse(ObjectDetachMessage(pad_guid, vehicle_guid, pad_position + Vector3(0,0,0.5f), 0, 0, pad_orientation_z)) sendResponse(ObjectDetachMessage(pad_guid, vehicle_guid, pad_position + Vector3(0,0,0.5f), pad_orientation_z))
case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) => case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) =>
if(tplayer_guid != guid) { if(tplayer_guid != guid) {
@ -596,7 +582,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
DeploymentActivities(obj) DeploymentActivities(obj)
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(obj.DeployTime milliseconds, obj.Actor, Deployment.TryDeploy(DriveState.Deployed)) context.system.scheduler.scheduleOnce(obj.DeployTime milliseconds, obj.Actor, Deployment.TryDeploy(DriveState.Deployed))
} }
@ -618,7 +603,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
DeploymentActivities(obj) DeploymentActivities(obj)
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(obj.UndeployTime milliseconds, obj.Actor, Deployment.TryUndeploy(DriveState.Mobile)) context.system.scheduler.scheduleOnce(obj.UndeployTime milliseconds, obj.Actor, Deployment.TryUndeploy(DriveState.Mobile))
} }
@ -671,7 +655,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
val obj_guid : PlanetSideGUID = obj.GUID val obj_guid : PlanetSideGUID = obj.GUID
val player_guid : PlanetSideGUID = tplayer.GUID val player_guid : PlanetSideGUID = tplayer.GUID
log.info(s"MountVehicleMsg: $player_guid mounts $obj_guid @ $seat_num") log.info(s"MountVehicleMsg: $player_guid mounts $obj_guid @ $seat_num")
vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(obj_guid) //clear all deconstruction timers vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) //clear timer
PlayerActionsToCancel() PlayerActionsToCancel()
if(seat_num == 0) { //simplistic vehicle ownership management if(seat_num == 0) { //simplistic vehicle ownership management
obj.Owner match { obj.Owner match {
@ -725,7 +709,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, seat_num, true, obj.GUID)) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, seat_num, true, obj.GUID))
} }
if(obj.Seats.values.count(_.isOccupied) == 0) { if(obj.Seats.values.count(_.isOccupied) == 0) {
vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(obj, continent, 600L) //start vehicle decay (10m) vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, obj.Definition.DeconstructionTime)) //start vehicle decay
} }
case Mountable.CanDismount(obj : Mountable, _) => case Mountable.CanDismount(obj : Mountable, _) =>
@ -855,12 +839,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
}) })
//drop items on ground //drop items on ground
val pos = tplayer.Position val pos = tplayer.Position
val orient = tplayer.Orientation val orient = Vector3(0,0, tplayer.Orientation.z)
((dropHolsters ++ dropInventory).map(_.obj) ++ drop).foreach(obj => { ((dropHolsters ++ dropInventory).map(_.obj) ++ drop).foreach(obj => {
continent.Ground ! Zone.DropItemOnGround(obj, pos, Vector3(0f, 0f, orient.z)) //TODO make a sound when dropping stuff
sendResponse(ObjectDetachMessage(tplayer.GUID, obj.GUID, pos, 0f, 0f, orient.z)) continent.Ground ! Zone.Ground.DropItem(obj, pos, orient)
val objDef = obj.Definition
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, objDef.ObjectId, obj.GUID, objDef.Packet.ConstructorData(obj).get))
}) })
sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true)) sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true))
} }
@ -949,12 +931,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
}) })
//drop stuff on ground //drop stuff on ground
val pos = tplayer.Position val pos = tplayer.Position
val orient = tplayer.Orientation val orient = Vector3(0,0, tplayer.Orientation.z)
((dropHolsters ++ dropInventory).map(_.obj)).foreach(obj => { ((dropHolsters ++ dropInventory).map(_.obj)).foreach(obj => {
continent.Ground ! Zone.DropItemOnGround(obj, pos, Vector3(0f, 0f, orient.z)) continent.Ground ! Zone.Ground.DropItem(obj, pos, orient)
sendResponse(ObjectDetachMessage(tplayer.GUID, obj.GUID, pos, 0f, 0f, orient.z))
val objDef = obj.Definition
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, objDef.ObjectId, obj.GUID, objDef.Packet.ConstructorData(obj).get))
}) })
case Terminal.VehicleLoadout(definition, weapons, inventory) => case Terminal.VehicleLoadout(definition, weapons, inventory) =>
@ -1175,9 +1154,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
case VehicleSpawnPad.PlayerSeatedInVehicle(vehicle, pad) => case VehicleSpawnPad.PlayerSeatedInVehicle(vehicle, pad) =>
val vehicle_guid = vehicle.GUID val vehicle_guid = vehicle.GUID
if(player.VehicleSeated.nonEmpty) {
vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(vehicle_guid)
}
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on? sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on?
//sendResponse(PlanetsideAttributeMessage(vehicle_guid, 0, 10))//vehicle.Definition.MaxHealth)) //sendResponse(PlanetsideAttributeMessage(vehicle_guid, 0, 10))//vehicle.Definition.MaxHealth))
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 68, 0L)) //??? sendResponse(PlanetsideAttributeMessage(vehicle_guid, 68, 0L)) //???
@ -1188,7 +1164,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case VehicleSpawnPad.ServerVehicleOverrideStart(vehicle, pad) => case VehicleSpawnPad.ServerVehicleOverrideStart(vehicle, pad) =>
val vdef = vehicle.Definition val vdef = vehicle.Definition
if(vehicle.Seats(0).isOccupied) { if(vehicle.Seats(0).isOccupied) {
sendResponse(ObjectDetachMessage(pad.GUID, vehicle.GUID, pad.Position + Vector3(0, 0, 0.5f), 0, 0, pad.Orientation.z)) sendResponse(ObjectDetachMessage(pad.GUID, vehicle.GUID, pad.Position + Vector3(0, 0, 0.5f), pad.Orientation.z))
} }
ServerVehicleOverride(vehicle, vdef.AutoPilotSpeed1, GlobalDefinitions.isFlightVehicle(vdef):Int) ServerVehicleOverride(vehicle, vdef.AutoPilotSpeed1, GlobalDefinitions.isFlightVehicle(vdef):Int)
@ -1366,7 +1342,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
(taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(original)(continent.GUID), zone_id)) (taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(original)(continent.GUID), zone_id))
} }
} }
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
respawnTimer = context.system.scheduler.scheduleOnce(respawnTime seconds, target, msg) respawnTimer = context.system.scheduler.scheduleOnce(respawnTime seconds, target, msg)
@ -1386,12 +1361,56 @@ class WorldSessionActor extends Actor with MDCContextAware {
RequestSanctuaryZoneSpawn(player, zone_number) RequestSanctuaryZoneSpawn(player, zone_number)
} }
case Zone.Ground.ItemOnGround(item, pos, orient) =>
item.Position = pos
item.Orientation = Vector3(0,0, orient.z) //dropped items rotate towards the user's standing direction
val exclusionId = player.Find(item) match {
case Some(slotNum) =>
player.Slot(slotNum).Equipment = None
sendResponse(ObjectDetachMessage(player.GUID, item.GUID, pos, orient.z))
sendResponse(ActionResultMessage.Pass)
player.GUID //we're dropping it; don't need to see it dropped again
case None =>
PlanetSideGUID(0) //object is being introduced into the world upon drop
}
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(exclusionId, item, continent))
case Zone.Ground.CanNotDropItem(zone, item, reason) =>
log.warn(s"DropItem: $player tried to drop a $item on the ground, but $reason")
case Zone.Ground.ItemInHand(item) =>
player.Fit(item) match {
case Some(slotNum) =>
val item_guid = item.GUID
val player_guid = player.GUID
player.Slot(slotNum).Equipment = item
val definition = item.Definition
sendResponse(
ObjectCreateDetailedMessage(
definition.ObjectId,
item_guid,
ObjectCreateMessageParent(player_guid, slotNum),
definition.Packet.DetailedConstructorData(item).get
)
)
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PickupItem(player_guid, continent, player, slotNum, item))
case None =>
continent.Ground ! Zone.Ground.DropItem(item, item.Position, item.Orientation) //restore previous state
}
case Zone.Ground.CanNotPickupItem(zone, item_guid, _) =>
zone.GUID(item_guid) match {
case Some(item) =>
log.warn(s"DropItem: finding a $item on the ground was suggested, but $player can not reach it")
case None =>
log.warn(s"DropItem: finding an item ($item_guid) on the ground was suggested, but $player can not see it")
}
case InterstellarCluster.ClientInitializationComplete() => case InterstellarCluster.ClientInitializationComplete() =>
StopBundlingPackets() StopBundlingPackets()
LivePlayerList.Add(sessionId, avatar) LivePlayerList.Add(sessionId, avatar)
traveler = new Traveler(self, continent.Id) traveler = new Traveler(self, continent.Id)
//PropertyOverrideMessage //PropertyOverrideMessage
sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1)) sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1))
sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list
sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil)) sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil))
@ -1431,7 +1450,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
if(!corpse.isAlive && corpse.HasGUID) { if(!corpse.isAlive && corpse.HasGUID) {
corpse.VehicleSeated match { corpse.VehicleSeated match {
case Some(_) => case Some(_) =>
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(corpse)) context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(corpse))
case None => case None =>
@ -1444,6 +1462,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
val guid = tplayer.GUID val guid = tplayer.GUID
StartBundlingPackets() StartBundlingPackets()
sendResponse(SetCurrentAvatarMessage(guid,0,0)) sendResponse(SetCurrentAvatarMessage(guid,0,0))
sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on //TODO once per respawn?
sendResponse(PlayerStateShiftMessage(ShiftState(1, tplayer.Position, tplayer.Orientation.z))) sendResponse(PlayerStateShiftMessage(ShiftState(1, tplayer.Position, tplayer.Orientation.z)))
if(spectator) { if(spectator) {
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None)) sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None))
@ -1478,31 +1497,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
//TacticsMessage //TacticsMessage
StopBundlingPackets() StopBundlingPackets()
case Zone.ItemFromGround(tplayer, item) =>
val obj_guid = item.GUID
val player_guid = tplayer.GUID
tplayer.Fit(item) match {
case Some(slot) =>
tplayer.Slot(slot).Equipment = item
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(player_guid, obj_guid))
val definition = item.Definition
sendResponse(
ObjectCreateDetailedMessage(
definition.ObjectId,
obj_guid,
ObjectCreateMessageParent(player_guid, slot),
definition.Packet.DetailedConstructorData(item).get
)
)
sendResponse(ActionResultMessage())
if(tplayer.VisibleSlots.contains(slot)) {
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentInHand(player_guid, player_guid, slot, item))
}
case None =>
continent.Ground ! Zone.DropItemOnGround(item, item.Position, item.Orientation) //restore
}
case ItemHacking(tplayer, target, tool_guid, delta, completeAction, tickAction) => case ItemHacking(tplayer, target, tool_guid, delta, completeAction, tickAction) =>
progressBarUpdate.cancel progressBarUpdate.cancel
if(progressBarValue.isDefined) { if(progressBarValue.isDefined) {
@ -1526,7 +1520,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
else { //continue next tick else { //continue next tick
tickAction.getOrElse(() => Unit)() tickAction.getOrElse(() => Unit)()
progressBarValue = Some(progressBarVal) progressBarValue = Some(progressBarVal)
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, ItemHacking(tplayer, target, tool_guid, delta, completeAction)) progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, ItemHacking(tplayer, target, tool_guid, delta, completeAction))
} }
@ -1614,22 +1607,20 @@ class WorldSessionActor extends Actor with MDCContextAware {
player.Locker.Inventory += 0 -> SimpleItem(remote_electronics_kit) player.Locker.Inventory += 0 -> SimpleItem(remote_electronics_kit)
//TODO end temp player character auto-loading //TODO end temp player character auto-loading
self ! ListAccountCharacters self ! ListAccountCharacters
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
clientKeepAlive.cancel clientKeepAlive.cancel
clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient()) clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient())
log.warn(PacketCoding.DecodePacket(hex"d2327e7b8a972b95113881003710").toString)
case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) => case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) =>
log.info("Handling " + msg) log.info("Handling " + msg)
sendResponse(ActionResultMessage(true, None)) sendResponse(ActionResultMessage.Pass)
self ! ListAccountCharacters self ! ListAccountCharacters
case msg @ CharacterRequestMessage(charId, action) => case msg @ CharacterRequestMessage(charId, action) =>
log.info("Handling " + msg) log.info("Handling " + msg)
action match { action match {
case CharacterRequestAction.Delete => case CharacterRequestAction.Delete =>
sendResponse(ActionResultMessage(false, Some(1))) sendResponse(ActionResultMessage.Fail(1))
case CharacterRequestAction.Select => case CharacterRequestAction.Select =>
//TODO check if can spawn on last continent/location from player? //TODO check if can spawn on last continent/location from player?
//TODO if yes, get continent guid accessors //TODO if yes, get continent guid accessors
@ -1836,10 +1827,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0))
self ! PacketCoding.CreateGamePacket(0, DismountVehicleMsg(player_guid, BailType.Normal, true)) //let vehicle try to clean up its fields self ! PacketCoding.CreateGamePacket(0, DismountVehicleMsg(player_guid, BailType.Normal, true)) //let vehicle try to clean up its fields
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(player)) context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(player))
//sendResponse(ObjectDetachMessage(vehicle_guid, player.GUID, Vector3.Zero, 0, 0, 0)) //sendResponse(ObjectDetachMessage(vehicle_guid, player.GUID, Vector3.Zero, 0))
//sendResponse(PlayerStateShiftMessage(ShiftState(1, Vector3.Zero, 0))) //sendResponse(PlayerStateShiftMessage(ShiftState(1, Vector3.Zero, 0)))
} }
@ -2000,8 +1990,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) } val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) }
val sumReloadValue : Int = originalBoxCapacity + tailReloadValue val sumReloadValue : Int = originalBoxCapacity + tailReloadValue
val previousBox = tool.AmmoSlot.Box //current magazine in tool val previousBox = tool.AmmoSlot.Box //current magazine in tool
sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3(0f, 0f, 0f), 0f, 0f, 0f)) sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3.Zero, 0f))
sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3(0f, 0f, 0f), 0f, 0f, 0f)) sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3.Zero, 0f))
obj.Inventory -= x.start //remove replacement ammo from inventory obj.Inventory -= x.start //remove replacement ammo from inventory
val ammoSlotIndex = tool.FireMode.AmmoSlotIndex val ammoSlotIndex = tool.FireMode.AmmoSlotIndex
tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool
@ -2169,27 +2159,37 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(EmoteMsg(avatar_guid, emote)) sendResponse(EmoteMsg(avatar_guid, emote))
case msg @ DropItemMessage(item_guid) => case msg @ DropItemMessage(item_guid) =>
log.info("DropItem: " + msg) log.info(s"DropItem: $msg")
player.FreeHand.Equipment match { continent.GUID(item_guid) match {
case Some(item) => case Some(item : Equipment) =>
if(item.GUID == item_guid) { player.FreeHand.Equipment match {
val orient : Vector3 = Vector3(0f, 0f, player.Orientation.z) case Some(_) =>
player.FreeHand.Equipment = None if(item.GUID == item_guid) {
continent.Ground ! Zone.DropItemOnGround(item, player.Position, orient) continent.Ground ! Zone.Ground.DropItem(item, player.Position, player.Orientation)
sendResponse(ObjectDetachMessage(player.GUID, item.GUID, player.Position, 0f, 0f, player.Orientation.z)) }
val objDef = item.Definition case None =>
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, player.Position, orient, objDef.ObjectId, item.GUID, objDef.Packet.ConstructorData(item).get)) log.warn(s"DropItem: $player wanted to drop a $item, but it wasn't at hand")
}
else {
log.warn(s"item in hand was ${item.GUID} but trying to drop $item_guid; nothing will be dropped")
} }
case Some(obj) => //TODO LLU
log.warn(s"DropItem: $player wanted to drop a $obj, but that isn't possible")
case None => case None =>
log.error(s"$player wanted to drop an item, but it was not in hand") log.warn(s"DropItem: $player wanted to drop an item ($item_guid), but it was nowhere to be found")
} }
case msg @ PickupItemMessage(item_guid, player_guid, unk1, unk2) => case msg @ PickupItemMessage(item_guid, player_guid, unk1, unk2) =>
log.info("PickupItem: " + msg) log.info(s"PickupItem: $msg")
continent.Ground ! Zone.GetItemOnGround(player, item_guid) continent.GUID(item_guid) match {
case Some(item : Equipment) =>
player.Fit(item) match {
case Some(_) =>
continent.Ground ! Zone.Ground.PickupItem(item_guid)
case None => //skip
sendResponse(ActionResultMessage.Fail(16)) //error code?
}
case _ =>
log.warn(s"PickupItem: $player requested an item that doesn't exist in this zone; assume client-side garbage data")
sendResponse(ObjectDeleteMessage(item_guid, 0))
}
case msg @ ReloadMessage(item_guid, ammo_clip, unk1) => case msg @ ReloadMessage(item_guid, ammo_clip, unk1) =>
log.info("Reload: " + msg) log.info("Reload: " + msg)
@ -2304,12 +2304,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
if((player.VehicleOwned.contains(object_guid) && vehicle.Owner.contains(player.GUID)) if((player.VehicleOwned.contains(object_guid) && vehicle.Owner.contains(player.GUID))
|| (player.Faction == vehicle.Faction || (player.Faction == vehicle.Faction
&& ((vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Health == 0))) { && ((vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Health == 0))) {
vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(object_guid) vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), continent))
vehicleService ! VehicleServiceMessage.RequestDeleteVehicle(vehicle, continent) vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent, Some(0 seconds)))
log.info(s"RequestDestroy: vehicle $object_guid") log.info(s"RequestDestroy: vehicle $vehicle")
} }
else { else {
log.info(s"RequestDestroy: must own vehicle $object_guid in order to deconstruct it") log.info(s"RequestDestroy: must own vehicle in order to deconstruct it")
} }
case Some(obj : Equipment) => case Some(obj : Equipment) =>
@ -2331,20 +2331,28 @@ class WorldSessionActor extends Actor with MDCContextAware {
}) })
match { match {
case Some((parent, Some(slot))) => case Some((parent, Some(slot))) =>
obj.Position = Vector3.Zero
taskResolver ! RemoveEquipmentFromSlot(parent, obj, slot) taskResolver ! RemoveEquipmentFromSlot(parent, obj, slot)
log.info(s"RequestDestroy: equipment $object_guid") log.info(s"RequestDestroy: equipment $obj")
case _ => case _ =>
//TODO search for item on ground if(continent.EquipmentOnGround.contains(obj)) {
sendResponse(ObjectDeleteMessage(object_guid, 0)) obj.Position = Vector3.Zero
log.warn(s"RequestDestroy: object $object_guid not found") continent.Ground ! Zone.Ground.RemoveItem(object_guid)
avatarService ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), object_guid))
log.info(s"RequestDestroy: equipment $obj on ground")
}
else {
log.warn(s"RequestDestroy: equipment $obj exists, but can not be reached")
}
} }
case Some(thing) =>
log.warn(s"RequestDestroy: not allowed to delete object $thing")
case None => case None =>
log.warn(s"RequestDestroy: object $object_guid not found") log.warn(s"RequestDestroy: object $object_guid not found")
case _ =>
log.warn(s"RequestDestroy: not allowed to delete object $object_guid")
} }
case msg @ ObjectDeleteMessage(object_guid, unk1) => case msg @ ObjectDeleteMessage(object_guid, unk1) =>
@ -2845,7 +2853,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
//todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle //todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle
//todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct. //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct.
if(bailType == BailType.Bailed && seat_num == 0 && GlobalDefinitions.isFlightVehicle(obj.asInstanceOf[Vehicle].Definition)) { if(bailType == BailType.Bailed && seat_num == 0 && GlobalDefinitions.isFlightVehicle(obj.asInstanceOf[Vehicle].Definition)) {
vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(obj.asInstanceOf[Vehicle], continent, 0L) // Immediately deconstruct vehicle vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, Some(0 seconds))) // Immediately deconstruct vehicle
} }
case None => case None =>
@ -3928,14 +3936,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Some(InventoryItem(item2, destIndex)) => //yes, swap case Some(InventoryItem(item2, destIndex)) => //yes, swap
//cleanly shuffle items around to avoid losing icons //cleanly shuffle items around to avoid losing icons
//the next ObjectDetachMessage is necessary to avoid icons being lost, but only as part of this swap //the next ObjectDetachMessage is necessary to avoid icons being lost, but only as part of this swap
sendResponse(ObjectDetachMessage(source_guid, item_guid, Vector3.Zero, 0f, 0f, 0f)) sendResponse(ObjectDetachMessage(source_guid, item_guid, Vector3.Zero, 0f))
val item2_guid = item2.GUID val item2_guid = item2.GUID
destination.Slot(destIndex).Equipment = None //remove the swap item from destination destination.Slot(destIndex).Equipment = None //remove the swap item from destination
(indexSlot.Equipment = item2) match { (indexSlot.Equipment = item2) match {
case Some(_) => //item and item2 swapped places successfully case Some(_) => //item and item2 swapped places successfully
log.info(s"MoveItem: $item2 swapped to $source @ $index") log.info(s"MoveItem: $item2 swapped to $source @ $index")
//remove item2 from destination //remove item2 from destination
sendResponse(ObjectDetachMessage(destination_guid, item2_guid, Vector3.Zero, 0f, 0f, 0f)) sendResponse(ObjectDetachMessage(destination_guid, item2_guid, Vector3.Zero, 0f))
destination match { destination match {
case obj : Vehicle => case obj : Vehicle =>
vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item2_guid)) vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item2_guid))
@ -3978,8 +3986,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
val pos = source.Position val pos = source.Position
val sourceOrientZ = source.Orientation.z val sourceOrientZ = source.Orientation.z
val orient : Vector3 = Vector3(0f, 0f, sourceOrientZ) val orient : Vector3 = Vector3(0f, 0f, sourceOrientZ)
continent.Actor ! Zone.DropItemOnGround(item2, pos, orient) continent.Ground ! Zone.Ground.DropItem(item2, pos, orient)
sendResponse(ObjectDetachMessage(destination_guid, item2_guid, pos, 0f, 0f, sourceOrientZ)) //ground sendResponse(ObjectDetachMessage(destination_guid, item2_guid, pos, sourceOrientZ)) //ground
val objDef = item2.Definition val objDef = item2.Definition
destination match { destination match {
case obj : Vehicle => case obj : Vehicle =>
@ -3987,7 +3995,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case _ => ; case _ => ;
//Player does not require special case; the act of dropping forces the item and icon to change //Player does not require special case; the act of dropping forces the item and icon to change
} }
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentOnGround(player_guid, pos, orient, objDef.ObjectId, item2_guid, objDef.Packet.ConstructorData(item2).get)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(player_guid, item2, continent))
} }
case None => ; case None => ;
@ -4037,15 +4045,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
* @param item the item * @param item the item
*/ */
def NormalItemDrop(obj : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : Unit = { def NormalItemDrop(obj : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : Unit = {
val itemGUID = item.GUID continent.Ground ! Zone.Ground.DropItem(item, obj.Position, Vector3(0f, 0f, obj.Orientation.z))
val ang = obj.Orientation.z
val pos = obj.Position
val orient = Vector3(0f, 0f, ang)
item.Position = pos
item.Orientation = orient
zone.Ground ! Zone.DropItemOnGround(item, pos, orient)
val itemDef = item.Definition
service ! AvatarServiceMessage(zone.Id, AvatarAction.EquipmentOnGround(Service.defaultPlayerGUID, pos, orient, itemDef.ObjectId, itemGUID, itemDef.Packet.ConstructorData(item).get))
} }
/** /**
@ -4348,7 +4348,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
PlayerActionsToCancel() PlayerActionsToCancel()
CancelAllProximityUnits() CancelAllProximityUnits()
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, galaxy, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(tplayer.Faction), tplayer, 7)) reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, galaxy, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(tplayer.Faction), tplayer, 7))
} }
@ -4489,9 +4488,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
*/ */
def TryDisposeOfLootedCorpse(obj : Player) : Boolean = { def TryDisposeOfLootedCorpse(obj : Player) : Boolean = {
if(WellLootedCorpse(obj)) { if(WellLootedCorpse(obj)) {
import scala.concurrent.duration._ avatarService ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), continent))
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(1 second, avatarService, AvatarServiceMessage.RemoveSpecificCorpse(List(obj)))
true true
} }
else { else {
@ -4567,7 +4564,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
def SetDelayedProximityUnitReset(terminal : Terminal with ProximityUnit) : Unit = { def SetDelayedProximityUnitReset(terminal : Terminal with ProximityUnit) : Unit = {
val terminal_guid = terminal.GUID val terminal_guid = terminal.GUID
ClearDelayedProximityUnitReset(terminal_guid) ClearDelayedProximityUnitReset(terminal_guid)
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
delayedProximityTerminalResets += terminal_guid -> delayedProximityTerminalResets += terminal_guid ->
context.system.scheduler.scheduleOnce(3000 milliseconds, self, DelayedProximityUnitStop(terminal)) context.system.scheduler.scheduleOnce(3000 milliseconds, self, DelayedProximityUnitStop(terminal))

View file

@ -0,0 +1,502 @@
// Copyright (c) 2017 PSForever
package services
import akka.actor.{Actor, ActorRef, Cancellable}
import net.psforever.objects.guid.TaskResolver
import net.psforever.objects.zones.Zone
import net.psforever.objects.{DefaultCancellable, PlanetSideGameObject}
import net.psforever.types.Vector3
import scala.annotation.tailrec
import scala.concurrent.duration._
/**
* The base class for a type of "destruction `Actor`" intended to be used for delaying object cleanup activity.
* Objects submitted to this process should be registered to a global unique identified system for a given region
* as is specified in their submission.<br>
* <br>
* Two waiting lists are used to pool the objects being removed.
* The first list is a basic pooling list that precludes any proper removal actions
* and is almost expressly for delaying the process.
* Previously-submitted tasks can be removed from this list so long as a matching object can be found.
* Tasks in this list can also be expedited into the second list without having to consider delays.
* After being migrated to the secondary list, the object is considered beyond the point of no return.
* Followup activity will lead to its inevitable unregistering and removal.<br>
* <br>
* Functions have been provided for `override` in order to interject the appropriate cleanup operations.
* The activity itself is typically removing the object in question from a certain list,
* dismissing it with a mass distribution of `ObjectDeleteMessage` packets,
* and finally unregistering it.
* Some types of object have (de-)implementation variations which should be made explicit through the overrides.
*/
abstract class RemoverActor extends Actor {
/**
* The timer that checks whether entries in the first pool are still eligible for that pool.
*/
var firstTask : Cancellable = DefaultCancellable.obj
/**
* The first pool of objects waiting to be processed for removal.
*/
var firstHeap : List[RemoverActor.Entry] = List()
/**
* The timer that checks whether entries in the second pool are still eligible for that pool.
*/
var secondTask : Cancellable = DefaultCancellable.obj
/**
* The second pool of objects waiting to be processed for removal.
*/
var secondHeap : List[RemoverActor.Entry] = List()
private var taskResolver : ActorRef = Actor.noSender
private[this] val log = org.log4s.getLogger
def trace(msg : String) : Unit = log.trace(msg)
def debug(msg : String) : Unit = log.debug(msg)
/**
* Send the initial message that requests a task resolver for assisting in the removal process.
*/
override def preStart() : Unit = {
super.preStart()
self ! RemoverActor.Startup()
}
/**
* Sufficiently clean up the current contents of these waiting removal jobs.
* Cancel all timers, rush all entries in the lists through their individual steps, then empty the lists.
* This is an improved `HurryAll`, but still faster since it also railroads entries through the second queue as well.
*/
override def postStop() = {
super.postStop()
firstTask.cancel
secondTask.cancel
firstHeap.foreach(entry => {
FirstJob(entry)
SecondJob(entry)
})
secondHeap.foreach { SecondJob }
firstHeap = Nil
secondHeap = Nil
taskResolver = ActorRef.noSender
}
def receive : Receive = {
case RemoverActor.Startup() =>
ServiceManager.serviceManager ! ServiceManager.Lookup("taskResolver") //ask for a resolver to deal with the GUID system
case ServiceManager.LookupResult("taskResolver", endpoint) =>
taskResolver = endpoint
context.become(Processing)
case msg =>
log.error(s"received message $msg before being properly initialized")
}
def Processing : Receive = {
case RemoverActor.AddTask(obj, zone, duration) =>
val entry = RemoverActor.Entry(obj, zone, duration.getOrElse(FirstStandardDuration).toNanos)
if(InclusionTest(entry) && !secondHeap.exists(test => RemoverActor.Similarity(test, entry) )) {
InitialJob(entry)
if(firstHeap.isEmpty) {
//we were the only entry so the event must be started from scratch
firstHeap = List(entry)
trace(s"a remover task has been added: $entry")
RetimeFirstTask()
}
else {
//unknown number of entries; append, sort, then re-time tasking
val oldHead = firstHeap.head
if(!firstHeap.exists(test => RemoverActor.Similarity(test, entry))) {
firstHeap = (firstHeap :+ entry).sortBy(_.duration)
trace(s"a remover task has been added: $entry")
if(oldHead != firstHeap.head) {
RetimeFirstTask()
}
}
else {
trace(s"$obj is already queued for removal")
}
}
}
else {
trace(s"$obj either does not qualify for this Remover or is already queued")
}
case RemoverActor.HurrySpecific(targets, zone) =>
HurrySpecific(targets, zone)
case RemoverActor.HurryAll() =>
HurryAll()
case RemoverActor.ClearSpecific(targets, zone) =>
ClearSpecific(targets, zone)
case RemoverActor.ClearAll() =>
ClearAll()
//private messages from RemoverActor to RemoverActor
case RemoverActor.StartDelete() =>
firstTask.cancel
secondTask.cancel
val now : Long = System.nanoTime
val (in, out) = firstHeap.partition(entry => { now - entry.time >= entry.duration })
firstHeap = out
secondHeap = secondHeap ++ in.map { RepackageEntry }
in.foreach { FirstJob }
RetimeFirstTask()
if(secondHeap.nonEmpty) {
import scala.concurrent.ExecutionContext.Implicits.global
secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete())
}
trace(s"item removal task has found ${in.size} items to remove")
case RemoverActor.TryDelete() =>
secondTask.cancel
val (in, out) = secondHeap.partition { ClearanceTest }
secondHeap = out
in.foreach { SecondJob }
if(out.nonEmpty) {
import scala.concurrent.ExecutionContext.Implicits.global
secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete())
}
trace(s"item removal task has removed ${in.size} items")
case RemoverActor.FailureToWork(entry, ex) =>
log.error(s"${entry.obj} from ${entry.zone} not properly deleted - $ex")
case _ => ;
}
/**
* Expedite some entries from the first pool into the second.
* @param targets a list of objects to pick
* @param zone the zone in which these objects must be discovered;
* all targets must be in this zone, with the assumption that this is the zone where they were registered
*/
def HurrySpecific(targets : List[PlanetSideGameObject], zone : Zone) : Unit = {
CullTargetsFromFirstHeap(targets, zone) match {
case Nil =>
debug(s"no tasks matching the targets $targets have been hurried")
case list =>
debug(s"the following tasks have been hurried: $list")
secondTask.cancel
list.foreach { FirstJob }
secondHeap = secondHeap ++ list.map { RepackageEntry }
import scala.concurrent.ExecutionContext.Implicits.global
secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete())
}
}
/**
* Expedite all entries from the first pool into the second.
*/
def HurryAll() : Unit = {
trace("all tasks have been hurried")
firstTask.cancel
firstHeap.foreach { FirstJob }
secondHeap = secondHeap ++ firstHeap.map { RepackageEntry }
firstHeap = Nil
secondTask.cancel
import scala.concurrent.ExecutionContext.Implicits.global
secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete())
}
/**
* Remove specific entries from the first pool.
*/
def ClearSpecific(targets : List[PlanetSideGameObject], zone : Zone) : Unit = {
CullTargetsFromFirstHeap(targets, zone) match {
case Nil =>
debug(s"no tasks matching the targets $targets have been cleared")
case list =>
debug(s"the following tasks have been cleared: $list")
}
}
/**
* No entries in the first pool.
*/
def ClearAll() : Unit = {
firstTask.cancel
firstHeap = Nil
}
/**
* Retime an individual entry by recreating it.
* @param entry an existing entry
* @return a new entry, containing the same object and zone information;
* this new entry is always set to last for the duration of the second pool
*/
private def RepackageEntry(entry : RemoverActor.Entry) : RemoverActor.Entry = {
RemoverActor.Entry(entry.obj, entry.zone, SecondStandardDuration.toNanos)
}
/**
* Search the first pool of entries awaiting removal processing.
* If any entry has the same object as one of the targets and belongs to the same zone, remove it from the first pool.
* If no targets are selected (an empty list), all discovered targets within the appropriate zone are removed.
* @param targets a list of objects to pick
* @param zone the zone in which these objects must be discovered;
* all targets must be in this zone, with the assumption that this is the zone where they were registered
* @return all of the discovered entries
*/
private def CullTargetsFromFirstHeap(targets : List[PlanetSideGameObject], zone : Zone) : List[RemoverActor.Entry] = {
val culledEntries = if(targets.nonEmpty) {
if(targets.size == 1) {
debug(s"a target submitted: ${targets.head}")
//simple selection
RemoverActor.recursiveFind(firstHeap.iterator, RemoverActor.Entry(targets.head, zone, 0)) match {
case None => ;
Nil
case Some(index) =>
val entry = firstHeap(index)
firstHeap = (firstHeap.take(index) ++ firstHeap.drop(index + 1)).sortBy(_.duration)
List(entry)
}
}
else {
debug(s"multiple targets submitted: $targets")
//cumbersome partition
//a - find targets from entries
val locatedTargets = for {
a <- targets.map(RemoverActor.Entry(_, zone, 0))
b <- firstHeap//.filter(entry => entry.zone == zone)
if b.obj.HasGUID && a.obj.HasGUID && RemoverActor.Similarity(b, a)
} yield b
if(locatedTargets.nonEmpty) {
//b - entries, after the found targets are removed (cull any non-GUID entries while at it)
firstHeap = (for {
a <- locatedTargets
b <- firstHeap
if b.obj.HasGUID && a.obj.HasGUID && !RemoverActor.Similarity(b, a)
} yield b).sortBy(_.duration)
locatedTargets
}
else {
Nil
}
}
}
else {
debug(s"all targets within the specified zone $zone will be submitted")
//no specific targets; split on all targets in the given zone instead
val (in, out) = firstHeap.partition(entry => entry.zone == zone)
firstHeap = out.sortBy(_.duration)
in
}
if(culledEntries.nonEmpty) {
RetimeFirstTask()
culledEntries
}
else {
Nil
}
}
/**
* Common function to reset the first task's delayed execution.
* Cancels the scheduled timer and will only restart the timer if there is at least one entry in the first pool.
* @param now the time (in nanoseconds);
* defaults to the current time (in nanoseconds)
*/
def RetimeFirstTask(now : Long = System.nanoTime) : Unit = {
firstTask.cancel
if(firstHeap.nonEmpty) {
val short_timeout : FiniteDuration = math.max(1, firstHeap.head.duration - (now - firstHeap.head.time)) nanoseconds
import scala.concurrent.ExecutionContext.Implicits.global
firstTask = context.system.scheduler.scheduleOnce(short_timeout, self, RemoverActor.StartDelete())
}
}
def SecondJob(entry : RemoverActor.Entry) : Unit = {
entry.obj.Position = Vector3.Zero //somewhere it will not disturb anything
taskResolver ! FinalTask(entry)
}
def FinalTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = {
import net.psforever.objects.guid.Task
TaskResolver.GiveTask (
new Task() {
private val localEntry = entry
private val localAnnounce = self
override def isComplete : Task.Resolution.Value = if(!localEntry.obj.HasGUID) {
Task.Resolution.Success
}
else {
Task.Resolution.Incomplete
}
def Execute(resolver : ActorRef) : Unit = {
resolver ! scala.util.Success(this)
}
override def onFailure(ex : Throwable): Unit = {
localAnnounce ! RemoverActor.FailureToWork(localEntry, ex)
}
}, List(DeletionTask(entry))
)
}
/**
* Default time for entries waiting in the first list.
* Override.
* @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds)
*/
def FirstStandardDuration : FiniteDuration
/**
* Default time for entries waiting in the second list.
* Override.
* @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds)
*/
def SecondStandardDuration : FiniteDuration
/**
* Determine whether or not the resulting entry is valid for this removal process.
* The primary purpose of this function should be to determine if the appropriate type of object is being submitted.
* Override.
* @param entry the entry
* @return `true`, if it can be processed; `false`, otherwise
*/
def InclusionTest(entry : RemoverActor.Entry) : Boolean
/**
* Performed when the entry is initially added to the first list.
* Override.
* @param entry the entry
*/
def InitialJob(entry : RemoverActor.Entry) : Unit
/**
* Performed when the entry is shifted from the first list to the second list.
* Override.
* @param entry the entry
*/
def FirstJob(entry : RemoverActor.Entry) : Unit
/**
* Performed to determine when an entry can be shifted off from the second list.
* Override.
* @param entry the entry
*/
def ClearanceTest(entry : RemoverActor.Entry) : Boolean
/**
* The specific action that is necessary to complete the removal process.
* Override.
* @see `GUIDTask`
* @param entry the entry
*/
def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask
}
object RemoverActor {
/**
* All information necessary to apply to the removal process to produce an effect.
* Internally, all entries have a "time created" field.
* @param obj the target
* @param zone the zone in which this target is registered
* @param duration how much longer the target will exist in its current state (in nanoseconds)
*/
case class Entry(obj : PlanetSideGameObject, zone : Zone, duration : Long) {
/** The time when this entry was created (in nanoseconds) */
val time : Long = System.nanoTime
}
/**
* A message that prompts the retrieval of a `TaskResolver` for us in the removal process.
*/
case class Startup()
/**
* Message to submit an object to the removal process.
* @see `FirstStandardDuration`
* @param obj the target
* @param zone the zone in which this target is registered
* @param duration how much longer the target will exist in its current state (in nanoseconds);
* a default time duration is provided by implementation
*/
case class AddTask(obj : PlanetSideGameObject, zone : Zone, duration : Option[FiniteDuration] = None)
/**
* "Hurrying" shifts entries with the discovered objects (in the same `zone`)
* through their first task and into the second pool.
* If the list of targets is empty, all discovered objects in the given zone will be considered targets.
* @param targets a list of objects to match
* @param zone the zone in which these objects exist;
* the assumption is that all these target objects are registered to this zone
*/
case class HurrySpecific(targets : List[PlanetSideGameObject], zone : Zone)
/**
* "Hurrying" shifts all entries through their first task and into the second pool.
*/
case class HurryAll()
/**
* "Clearing" cancels entries with the discovered objects (in the same `zone`)
* if they are discovered in the first pool of objects.
* Those entries will no longer be affected by any actions performed by the removal process until re-submitted.
* If the list of targets is empty, all discovered objects in the given zone will be considered targets.
* @param targets a list of objects to match
* @param zone the zone in which these objects exist;
* the assumption is that all these target objects are registered to this zone
*/
case class ClearSpecific(targets : List[PlanetSideGameObject], zone : Zone)
/**
* "Clearing" cancels all entries if they are discovered in the first pool of objects.
* Those entries will no longer be affected by any actions performed by the removal process until re-submitted.
*/
case class ClearAll()
/**
* Message that indicates that the final stage of the remover process has failed.
* Since the last step is generally unregistering the object, it could be a critical error.
* @param entry the entry that was not properly removed
* @param ex the reason the last entry was not properly removed
*/
protected final case class FailureToWork(entry : RemoverActor.Entry, ex : Throwable)
/**
* Internal message to flag operations by data in the first list if it has been in that list long enough.
*/
private final case class StartDelete()
/**
* Internal message to flag operations by data in the second list if it has been in that list long enough.
*/
private final case class TryDelete()
/**
* Match two entries by object and by zone information.
* @param entry1 the first entry
* @param entry2 the second entry
* @return if they match
*/
private def Similarity(entry1 : RemoverActor.Entry, entry2 : RemoverActor.Entry) : Boolean = {
entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID
}
/**
* Get the index of an entry in the list of entries.
* @param iter an `Iterator` of entries
* @param target the specific entry to be found
* @param index the incrementing index value
* @return the index of the entry in the list, if a match to the target is found
*/
@tailrec private def recursiveFind(iter : Iterator[RemoverActor.Entry], target : RemoverActor.Entry, index : Int = 0) : Option[Int] = {
if(!iter.hasNext) {
None
}
else {
val entry = iter.next
if(entry.obj.HasGUID && target.obj.HasGUID && Similarity(entry, target)) {
Some(index)
}
else {
recursiveFind(iter, target, index + 1)
}
}
}
}

View file

@ -1,12 +1,15 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package services.avatar package services.avatar
import net.psforever.objects.Player import net.psforever.objects.{PlanetSideGameObject, Player}
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.Container
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.objectcreate.ConstructorData
import net.psforever.types.{ExoSuitType, Vector3} import net.psforever.types.ExoSuitType
import scala.concurrent.duration.FiniteDuration
object AvatarAction { object AvatarAction {
trait Action trait Action
@ -17,21 +20,19 @@ object AvatarAction {
final case class ChangeFireState_Start(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class ChangeFireState_Start(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action
final case class ChangeFireState_Stop(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class ChangeFireState_Stop(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action
final case class ConcealPlayer(player_guid : PlanetSideGUID) extends Action final case class ConcealPlayer(player_guid : PlanetSideGUID) extends Action
final case class DropItem(player_guid : PlanetSideGUID, item : Equipment, zone : Zone) extends Action
final case class EquipmentInHand(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action final case class EquipmentInHand(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action
final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item_id : Int, item_guid : PlanetSideGUID, item_data : ConstructorData) extends Action
final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action
// final case class LoadMap(msg : PlanetSideGUID) extends Action
// final case class unLoadMap(msg : PlanetSideGUID) extends Action
final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action
final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action
final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action
final case class Release(player : Player, zone : Zone, time : Option[Long] = None) 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 Release(player : Player, zone : Zone, time : Option[FiniteDuration] = None) extends Action
final case class Reload(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class Reload(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action
final case class StowEquipment(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action final case class StowEquipment(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action
final case class WeaponDryFire(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class WeaponDryFire(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action
} }

View file

@ -3,9 +3,9 @@ package services.avatar
import net.psforever.objects.Player import net.psforever.objects.Player
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID, PlayerStateMessageUpstream}
import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.objectcreate.ConstructorData
import net.psforever.types.{ExoSuitType, Vector3} import net.psforever.types.ExoSuitType
object AvatarResponse { object AvatarResponse {
trait Response trait Response
@ -16,11 +16,9 @@ object AvatarResponse {
final case class ChangeFireState_Start(weapon_guid : PlanetSideGUID) extends Response final case class ChangeFireState_Start(weapon_guid : PlanetSideGUID) extends Response
final case class ChangeFireState_Stop(weapon_guid : PlanetSideGUID) extends Response final case class ChangeFireState_Stop(weapon_guid : PlanetSideGUID) extends Response
final case class ConcealPlayer() extends Response final case class ConcealPlayer() extends Response
final case class EquipmentInHand(target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Response final case class EquipmentInHand(pkt : ObjectCreateMessage) extends Response
final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item_id : Int, item_guid : PlanetSideGUID, item_data : ConstructorData) extends Response final case class DropItem(pkt : ObjectCreateMessage) extends Response
final case class LoadPlayer(pdata : ConstructorData) extends Response final case class LoadPlayer(pdata : ConstructorData) extends Response
// final case class unLoadMap() extends Response
// final case class LoadMap() extends Response
final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response
final case class ObjectHeld(slot : Int) extends Response final case class ObjectHeld(slot : Int) extends Response
final case class PlanetsideAttribute(attribute_type : Int, attribute_value : Long) extends Response final case class PlanetsideAttribute(attribute_type : Int, attribute_value : Long) extends Response
@ -32,5 +30,4 @@ object AvatarResponse {
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response // final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response // final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response
// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response // final case class HitHintReturn(itemID : PlanetSideGUID) extends Response
// final case class ChangeWeapon(facingYaw : Int) extends Response
} }

View file

@ -2,12 +2,15 @@
package services.avatar package services.avatar
import akka.actor.{Actor, ActorRef, Props} import akka.actor.{Actor, ActorRef, Props}
import services.avatar.support.CorpseRemovalActor import net.psforever.packet.game.ObjectCreateMessage
import services.{GenericEventBus, Service} import net.psforever.packet.game.objectcreate.{DroppedItemData, ObjectCreateMessageParent, PlacementData}
import services.avatar.support.{CorpseRemovalActor, DroppedItemRemover}
import services.{GenericEventBus, RemoverActor, Service}
class AvatarService extends Actor { class AvatarService extends Actor {
private val undertaker : ActorRef = context.actorOf(Props[CorpseRemovalActor], "corpse-removal-agent") private val undertaker : ActorRef = context.actorOf(Props[CorpseRemovalActor], "corpse-removal-agent")
undertaker ! "startup" private val janitor = context.actorOf(Props[DroppedItemRemover], "item-remover-agent")
//undertaker ! "startup"
private [this] val log = org.log4s.getLogger private [this] val log = org.log4s.getLogger
@ -62,13 +65,26 @@ class AvatarService extends Actor {
AvatarEvents.publish( AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ConcealPlayer()) AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ConcealPlayer())
) )
case AvatarAction.EquipmentInHand(player_guid, target_guid, slot, obj) => case AvatarAction.DropItem(player_guid, item, zone) =>
AvatarEvents.publish( val definition = item.Definition
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.EquipmentInHand(target_guid, slot, obj)) val objectData = DroppedItemData(
PlacementData(item.Position, item.Orientation),
definition.Packet.ConstructorData(item).get
) )
case AvatarAction.EquipmentOnGround(player_guid, pos, orient, item_id, item_guid, item_data) => janitor forward RemoverActor.AddTask(item, zone)
AvatarEvents.publish( AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.EquipmentOnGround(pos, orient, item_id, item_guid, item_data)) AvatarServiceResponse(s"/$forChannel/Avatar", player_guid,
AvatarResponse.DropItem(ObjectCreateMessage(definition.ObjectId, item.GUID, objectData))
)
)
case AvatarAction.EquipmentInHand(player_guid, target_guid, slot, item) =>
val definition = item.Definition
val containerData = ObjectCreateMessageParent(target_guid, slot)
val objectData = definition.Packet.ConstructorData(item).get
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid,
AvatarResponse.EquipmentInHand(ObjectCreateMessage(definition.ObjectId, item.GUID, containerData, objectData))
)
) )
case AvatarAction.LoadPlayer(player_guid, pdata) => case AvatarAction.LoadPlayer(player_guid, pdata) =>
AvatarEvents.publish( AvatarEvents.publish(
@ -90,11 +106,24 @@ class AvatarService extends Actor {
AvatarEvents.publish( AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarResponse.PlayerState(msg, spectator, weapon)) AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarResponse.PlayerState(msg, spectator, weapon))
) )
case AvatarAction.PickupItem(player_guid, zone, target, slot, 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)
}
})
)
case AvatarAction.Release(player, zone, time) => case AvatarAction.Release(player, zone, time) =>
undertaker ! (time match { undertaker forward RemoverActor.AddTask(player, zone, time)
case Some(t) => CorpseRemovalActor.AddCorpse(player, zone, t)
case None => CorpseRemovalActor.AddCorpse(player, zone)
})
AvatarEvents.publish( AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player.GUID, AvatarResponse.Release(player)) AvatarServiceResponse(s"/$forChannel/Avatar", player.GUID, AvatarResponse.Release(player))
) )
@ -115,52 +144,13 @@ class AvatarService extends Actor {
} }
//message to Undertaker //message to Undertaker
case AvatarServiceMessage.RemoveSpecificCorpse(corpses) => case AvatarServiceMessage.Corpse(msg) =>
undertaker ! AvatarServiceMessage.RemoveSpecificCorpse( corpses.filter(corpse => {corpse.HasGUID && corpse.isBackpack}) ) undertaker forward msg
case AvatarServiceMessage.Ground(msg) =>
janitor forward msg
/* /*
case AvatarService.PlayerStateMessage(msg) =>
// log.info(s"NEW: ${m}")
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.avatar_guid)
if (playerOpt.isDefined) {
val player: PlayerAvatar = playerOpt.get
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, msg.avatar_guid,
AvatarServiceReply.PlayerStateMessage(msg.pos, msg.vel, msg.facingYaw, msg.facingPitch, msg.facingYawUpper, msg.is_crouching, msg.is_jumping, msg.jump_thrust, msg.is_cloaked)
))
}
case AvatarService.LoadMap(msg) =>
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
if (playerOpt.isDefined) {
val player: PlayerAvatar = playerOpt.get
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
AvatarServiceReply.LoadMap()
))
}
case AvatarService.unLoadMap(msg) =>
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
if (playerOpt.isDefined) {
val player: PlayerAvatar = playerOpt.get
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
AvatarServiceReply.unLoadMap()
))
}
case AvatarService.ObjectHeld(msg) =>
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
if (playerOpt.isDefined) {
val player: PlayerAvatar = playerOpt.get
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
AvatarServiceReply.ObjectHeld()
))
}
case AvatarService.PlanetsideAttribute(guid, attribute_type, attribute_value) =>
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
if (playerOpt.isDefined) {
val player: PlayerAvatar = playerOpt.get
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid,
AvatarServiceReply.PlanetSideAttribute(attribute_type, attribute_value)
))
}
case AvatarService.PlayerStateShift(killer, guid) => case AvatarService.PlayerStateShift(killer, guid) =>
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid) val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
if (playerOpt.isDefined) { if (playerOpt.isDefined) {
@ -185,16 +175,8 @@ class AvatarService extends Actor {
AvatarServiceReply.DestroyDisplay(source_guid) AvatarServiceReply.DestroyDisplay(source_guid)
)) ))
} }
case AvatarService.ChangeWeapon(unk1, sessionId) =>
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(sessionId)
if (playerOpt.isDefined) {
val player: PlayerAvatar = playerOpt.get
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(player.guid),
AvatarServiceReply.ChangeWeapon(unk1)
))
}
*/ */
case msg => case msg =>
log.info(s"Unhandled message $msg from $sender") log.warn(s"Unhandled message $msg from $sender")
} }
} }

View file

@ -1,10 +1,9 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package services.avatar package services.avatar
import net.psforever.objects.Player
final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action) final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action)
object AvatarServiceMessage { object AvatarServiceMessage {
final case class RemoveSpecificCorpse(corpse : List[Player]) final case class Corpse(msg : Any)
final case class Ground(msg : Any)
} }

View file

@ -1,245 +1,33 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package services.avatar.support package services.avatar.support
import akka.actor.{Actor, ActorRef, Cancellable} import net.psforever.objects.guid.{GUIDTask, TaskResolver}
import net.psforever.objects.guid.TaskResolver import net.psforever.objects.Player
import net.psforever.objects.{DefaultCancellable, Player} import services.{RemoverActor, Service}
import net.psforever.objects.zones.Zone
import net.psforever.types.Vector3
import services.{Service, ServiceManager}
import services.ServiceManager.Lookup
import services.avatar.{AvatarAction, AvatarServiceMessage} import services.avatar.{AvatarAction, AvatarServiceMessage}
import scala.annotation.tailrec
import scala.concurrent.duration._ import scala.concurrent.duration._
class CorpseRemovalActor extends Actor { class CorpseRemovalActor extends RemoverActor {
private var burial : Cancellable = DefaultCancellable.obj final val FirstStandardDuration : FiniteDuration = 3 minutes
private var corpses : List[CorpseRemovalActor.Entry] = List()
private var decomposition : Cancellable = DefaultCancellable.obj final val SecondStandardDuration : FiniteDuration = 500 milliseconds
private var buriedCorpses : List[CorpseRemovalActor.Entry] = List()
private var taskResolver : ActorRef = Actor.noSender def InclusionTest(entry : RemoverActor.Entry) : Boolean = {
entry.obj.isInstanceOf[Player] && entry.obj.asInstanceOf[Player].isBackpack
private[this] val log = org.log4s.getLogger
override def postStop() = {
//Cart Master: See you on Thursday.
super.postStop()
burial.cancel
decomposition.cancel
corpses.foreach(corpse => {
BurialTask(corpse)
LastRitesTask(corpse)
})
buriedCorpses.foreach { LastRitesTask }
} }
def receive : Receive = { def InitialJob(entry : RemoverActor.Entry) : Unit = { }
case "startup" =>
ServiceManager.serviceManager ! Lookup("taskResolver") //ask for a resolver to deal with the GUID system
case ServiceManager.LookupResult("taskResolver", endpoint) => def FirstJob(entry : RemoverActor.Entry) : Unit = {
//Cart Master: Bring out your dead! import net.psforever.objects.zones.Zone
taskResolver = endpoint entry.zone.Population ! Zone.Corpse.Remove(entry.obj.asInstanceOf[Player])
context.become(Processing) context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID))
case _ => ;
} }
def Processing : Receive = { def ClearanceTest(entry : RemoverActor.Entry) : Boolean = !entry.zone.Corpses.contains(entry.obj)
case CorpseRemovalActor.AddCorpse(corpse, zone, time) =>
import CorpseRemovalActor.SimilarCorpses
if(corpse.isBackpack && !buriedCorpses.exists(entry => SimilarCorpses(entry.corpse, corpse) )) {
if(corpses.isEmpty) {
//we were the only entry so the event must be started from scratch
corpses = List(CorpseRemovalActor.Entry(corpse, zone, time))
RetimeFirstTask()
}
else {
//unknown number of entries; append, sort, then re-time tasking
val oldHead = corpses.head
if(!corpses.exists(entry => SimilarCorpses(entry.corpse, corpse))) {
corpses = (corpses :+ CorpseRemovalActor.Entry(corpse, zone, time)).sortBy(_.timeAlive)
if(oldHead != corpses.head) {
RetimeFirstTask()
}
}
}
}
else {
//Cart Master: 'Ere. He says he's not dead!
log.warn(s"$corpse does not qualify as a corpse; ignored queueing request")
}
case AvatarServiceMessage.RemoveSpecificCorpse(targets) => def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = {
if(targets.nonEmpty) { GUIDTask.UnregisterPlayer(entry.obj.asInstanceOf[Player])(entry.zone.GUID)
//Cart Master: No, I've got to go to the Robinsons'. They've lost nine today.
burial.cancel
if(targets.size == 1) {
log.debug(s"a target corpse submitted for early cleanup: ${targets.head}")
//simple selection
CorpseRemovalActor.recursiveFindCorpse(corpses.iterator, targets.head) match {
case None => ;
case Some(index) =>
decomposition.cancel
BurialTask(corpses(index))
buriedCorpses = buriedCorpses :+ corpses(index)
corpses = (corpses.take(index) ++ corpses.drop(index + 1)).sortBy(_.timeAlive)
import scala.concurrent.ExecutionContext.Implicits.global
decomposition = context.system.scheduler.scheduleOnce(500 milliseconds, self, CorpseRemovalActor.TryDelete())
}
}
else {
log.debug(s"multiple target corpses submitted for early cleanup: $targets")
import CorpseRemovalActor.SimilarCorpses
decomposition.cancel
//cumbersome partition
//a - find targets from corpses
val locatedTargets = for {
a <- targets
b <- corpses
if b.corpse.HasGUID && a.HasGUID && SimilarCorpses(b.corpse, a)
} yield b
if(locatedTargets.nonEmpty) {
decomposition.cancel
locatedTargets.foreach { BurialTask }
buriedCorpses = locatedTargets ++ buriedCorpses
import scala.concurrent.ExecutionContext.Implicits.global
decomposition = context.system.scheduler.scheduleOnce(500 milliseconds, self, CorpseRemovalActor.TryDelete())
//b - corpses, after the found targets are removed (cull any non-GUID entries while at it)
corpses = (for {
a <- locatedTargets
b <- corpses
if b.corpse.HasGUID && a.corpse.HasGUID && !SimilarCorpses(b.corpse, a.corpse)
} yield b).sortBy(_.timeAlive)
}
}
RetimeFirstTask()
}
case CorpseRemovalActor.StartDelete() =>
burial.cancel
decomposition.cancel
val now : Long = System.nanoTime
val (buried, rotting) = corpses.partition(entry => { now - entry.time >= entry.timeAlive })
corpses = rotting
buriedCorpses = buriedCorpses ++ buried
buried.foreach { BurialTask }
RetimeFirstTask()
if(buriedCorpses.nonEmpty) {
import scala.concurrent.ExecutionContext.Implicits.global
burial = context.system.scheduler.scheduleOnce(500 milliseconds, self, CorpseRemovalActor.TryDelete())
}
case CorpseRemovalActor.TryDelete() =>
decomposition.cancel
val (decomposed, rotting) = buriedCorpses.partition(entry => {
!entry.zone.Corpses.contains(entry.corpse)
})
buriedCorpses = rotting
decomposed.foreach { LastRitesTask }
if(rotting.nonEmpty) {
import scala.concurrent.ExecutionContext.Implicits.global
decomposition = context.system.scheduler.scheduleOnce(500 milliseconds, self, CorpseRemovalActor.TryDelete())
}
case CorpseRemovalActor.FailureToWork(target, zone, ex) =>
//Cart Master: Oh, I can't take him like that. It's against regulations.
log.error(s"corpse $target from $zone not properly unregistered - $ex")
case _ => ;
}
def RetimeFirstTask(now : Long = System.nanoTime) : Unit = {
//Cart Master: Thursday.
burial.cancel
if(corpses.nonEmpty) {
val short_timeout : FiniteDuration = math.max(1, corpses.head.timeAlive - (now - corpses.head.time)) nanoseconds
import scala.concurrent.ExecutionContext.Implicits.global
burial = context.system.scheduler.scheduleOnce(short_timeout, self, CorpseRemovalActor.StartDelete())
}
}
def BurialTask(entry : CorpseRemovalActor.Entry) : Unit = {
val target = entry.corpse
entry.zone.Population ! Zone.Corpse.Remove(target)
context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, target.GUID))
}
def LastRitesTask(entry : CorpseRemovalActor.Entry) : Unit = {
//Cart master: Nine pence.
val target = entry.corpse
target.Position = Vector3.Zero //somewhere it will not disturb anything
taskResolver ! LastRitesTask(target, entry.zone)
}
def LastRitesTask(corpse : Player, zone : Zone) : TaskResolver.GiveTask = {
import net.psforever.objects.guid.{GUIDTask, Task}
TaskResolver.GiveTask (
new Task() {
private val localCorpse = corpse
private val localZone = zone
private val localAnnounce = self
override def isComplete : Task.Resolution.Value = if(!localCorpse.HasGUID) {
Task.Resolution.Success
}
else {
Task.Resolution.Incomplete
}
def Execute(resolver : ActorRef) : Unit = {
resolver ! scala.util.Success(this)
}
override def onFailure(ex : Throwable): Unit = {
localAnnounce ! CorpseRemovalActor.FailureToWork(localCorpse, localZone, ex)
}
}, List(GUIDTask.UnregisterPlayer(corpse)(zone.GUID))
)
}
}
object CorpseRemovalActor {
final val time : Long = 180000000000L //3 min (180s)
final case class AddCorpse(corpse : Player, zone : Zone, time : Long = CorpseRemovalActor.time)
final case class Entry(corpse : Player, zone : Zone, timeAlive : Long = CorpseRemovalActor.time, time : Long = System.nanoTime())
private final case class FailureToWork(corpse : Player, zone : Zone, ex : Throwable)
private final case class StartDelete()
private final case class TryDelete()
private def SimilarCorpses(obj1 : Player, obj2 : Player) : Boolean = {
obj1 == obj2 && obj1.Continent.equals(obj2.Continent) && obj1.GUID == obj2.GUID
}
/**
* A recursive function that finds and removes a specific player from a list of players.
* @param iter an `Iterator` of `CorpseRemovalActor.Entry` objects
* @param player the target `Player`
* @param index the index of the discovered `Player` object
* @return the index of the `Player` object in the list to be removed;
* `None`, otherwise
*/
@tailrec final def recursiveFindCorpse(iter : Iterator[CorpseRemovalActor.Entry], player : Player, index : Int = 0) : Option[Int] = {
if(!iter.hasNext) {
None
}
else {
val corpse = iter.next.corpse
if(corpse.HasGUID && player.HasGUID && SimilarCorpses(corpse, player)) {
Some(index)
}
else {
recursiveFindCorpse(iter, player, index + 1)
}
}
} }
} }

View file

@ -0,0 +1,33 @@
// Copyright (c) 2017 PSForever
package services.avatar.support
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.guid.{GUIDTask, TaskResolver}
import services.avatar.{AvatarAction, AvatarServiceMessage}
import services.{RemoverActor, Service}
import scala.concurrent.duration._
class DroppedItemRemover extends RemoverActor {
final val FirstStandardDuration : FiniteDuration = 3 minutes
final val SecondStandardDuration : FiniteDuration = 500 milliseconds
def InclusionTest(entry : RemoverActor.Entry) : Boolean = {
entry.obj.isInstanceOf[Equipment]
}
def InitialJob(entry : RemoverActor.Entry) : Unit = { }
def FirstJob(entry : RemoverActor.Entry) : Unit = {
import net.psforever.objects.zones.Zone
entry.zone.Ground ! Zone.Ground.PickupItem(entry.obj.GUID)
context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID))
}
def ClearanceTest(entry : RemoverActor.Entry) : Boolean = !entry.zone.EquipmentOnGround.contains(entry.obj)
def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = {
GUIDTask.UnregisterEquipment(entry.obj.asInstanceOf[Equipment])(entry.zone.GUID)
}
}

View file

@ -4,15 +4,14 @@ package services.vehicle
import akka.actor.{Actor, ActorRef, Props} import akka.actor.{Actor, ActorRef, Props}
import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.pad.VehicleSpawnPad
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import services.vehicle.support.{DeconstructionActor, DelayedDeconstructionActor} import services.vehicle.support.VehicleRemover
import net.psforever.types.DriveState import net.psforever.types.DriveState
import services.{GenericEventBus, RemoverActor, Service}
import services.{GenericEventBus, Service} import scala.concurrent.duration._
class VehicleService extends Actor { class VehicleService extends Actor {
private val vehicleDecon : ActorRef = context.actorOf(Props[DeconstructionActor], "vehicle-decon-agent") private val vehicleDecon : ActorRef = context.actorOf(Props[VehicleRemover], "vehicle-decon-agent")
private val vehicleDelayedDecon : ActorRef = context.actorOf(Props[DelayedDeconstructionActor], "vehicle-delayed-decon-agent")
vehicleDecon ! DeconstructionActor.RequestTaskResolver
private [this] val log = org.log4s.getLogger private [this] val log = org.log4s.getLogger
override def preStart = { override def preStart = {
@ -87,6 +86,11 @@ class VehicleService extends Actor {
VehicleEvents.publish( VehicleEvents.publish(
VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.StowEquipment(vehicle_guid, slot, definition.ObjectId, item.GUID, definition.Packet.DetailedConstructorData(item).get)) VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.StowEquipment(vehicle_guid, slot, definition.ObjectId, item.GUID, definition.Packet.DetailedConstructorData(item).get))
) )
case VehicleAction.UnloadVehicle(player_guid, continent, vehicle) =>
vehicleDecon ! RemoverActor.ClearSpecific(List(vehicle), continent) //precaution
VehicleEvents.publish(
VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.UnloadVehicle(vehicle.GUID))
)
case VehicleAction.UnstowEquipment(player_guid, item_guid) => case VehicleAction.UnstowEquipment(player_guid, item_guid) =>
VehicleEvents.publish( VehicleEvents.publish(
VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.UnstowEquipment(item_guid)) VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.UnstowEquipment(item_guid))
@ -102,23 +106,9 @@ class VehicleService extends Actor {
case _ => ; case _ => ;
} }
//message to DeconstructionActor //message to VehicleRemover
case VehicleServiceMessage.RequestDeleteVehicle(vehicle, continent) => case VehicleServiceMessage.Decon(msg) =>
vehicleDecon ! DeconstructionActor.RequestDeleteVehicle(vehicle, continent) vehicleDecon forward msg
//message to DelayedDeconstructionActor
case VehicleServiceMessage.DelayedVehicleDeconstruction(vehicle, zone, timeAlive) =>
vehicleDelayedDecon ! DelayedDeconstructionActor.ScheduleDeconstruction(vehicle, zone, timeAlive)
//message to DelayedDeconstructionActor
case VehicleServiceMessage.UnscheduleDeconstruction(vehicle_guid) =>
vehicleDelayedDecon ! DelayedDeconstructionActor.UnscheduleDeconstruction(vehicle_guid)
//response from DeconstructionActor
case DeconstructionActor.DeleteVehicle(vehicle_guid, zone_id) =>
VehicleEvents.publish(
VehicleServiceResponse(s"/$zone_id/Vehicle", Service.defaultPlayerGUID, VehicleResponse.UnloadVehicle(vehicle_guid))
)
//from VehicleSpawnControl //from VehicleSpawnControl
case VehicleSpawnPad.ConcealPlayer(player_guid, zone_id) => case VehicleSpawnPad.ConcealPlayer(player_guid, zone_id) =>
@ -159,13 +149,12 @@ class VehicleService extends Actor {
VehicleEvents.publish( VehicleEvents.publish(
VehicleServiceResponse(s"/${zone.Id}/Vehicle", Service.defaultPlayerGUID, VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata)) VehicleServiceResponse(s"/${zone.Id}/Vehicle", Service.defaultPlayerGUID, VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata))
) )
vehicleDelayedDecon ! DelayedDeconstructionActor.UnscheduleDeconstruction(vguid) //avoid unattended vehicle spawning blocking the pad; user should mount (and does so normally) to reset decon timer
vehicleDelayedDecon ! DelayedDeconstructionActor.ScheduleDeconstruction(vehicle, zone, 600L) //10min vehicleDecon forward RemoverActor.AddTask(vehicle, zone, Some(30 seconds))
//from VehicleSpawnControl //from VehicleSpawnControl
case VehicleSpawnPad.DisposeVehicle(vehicle, zone) => case VehicleSpawnPad.DisposeVehicle(vehicle, zone) =>
vehicleDelayedDecon ! DelayedDeconstructionActor.UnscheduleDeconstruction(vehicle.GUID) vehicleDecon forward RemoverActor.HurrySpecific(List(vehicle), zone)
vehicleDecon ! DeconstructionActor.RequestDeleteVehicle(vehicle, zone)
//correspondence from WorldSessionActor //correspondence from WorldSessionActor
case VehicleServiceMessage.AMSDeploymentChange(zone) => case VehicleServiceMessage.AMSDeploymentChange(zone) =>

View file

@ -3,16 +3,14 @@ package services.vehicle
import net.psforever.objects.Vehicle import net.psforever.objects.Vehicle
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game.PlanetSideGUID
final case class VehicleServiceMessage(forChannel : String, actionMessage : VehicleAction.Action) final case class VehicleServiceMessage(forChannel : String, actionMessage : VehicleAction.Action)
object VehicleServiceMessage { object VehicleServiceMessage {
final case class DelayedVehicleDeconstruction(vehicle : Vehicle, continent : Zone, timeAlive : Long)
final case class GiveActorControl(vehicle : Vehicle, actorName : String) final case class GiveActorControl(vehicle : Vehicle, actorName : String)
final case class RevokeActorControl(vehicle : Vehicle) final case class RevokeActorControl(vehicle : Vehicle)
final case class RequestDeleteVehicle(vehicle : Vehicle, continent : Zone)
final case class UnscheduleDeconstruction(vehicle_guid : PlanetSideGUID) final case class Decon(msg : Any)
final case class AMSDeploymentChange(zone : Zone) final case class AMSDeploymentChange(zone : Zone)
} }

View file

@ -1,276 +0,0 @@
// Copyright (c) 2017 PSForever
package services.vehicle.support
import akka.actor.{Actor, ActorRef, Cancellable}
import net.psforever.objects.{DefaultCancellable, GlobalDefinitions, Vehicle}
import net.psforever.objects.guid.TaskResolver
import net.psforever.objects.vehicles.Seat
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.types.Vector3
import services.ServiceManager
import services.ServiceManager.Lookup
import services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.annotation.tailrec
import scala.concurrent.duration._
/**
* Manage a previously-functioning vehicle as it is being deconstructed.<br>
* <br>
* A reference to a vehicle should be passed to this object as soon as it is going to be cleaned-up from the game world.
* Once accepted, only a few seconds will remain before the vehicle is deleted.
* To ensure that no players are lost in the deletion, all occupants of the vehicle are kicked out.
* Furthermore, the vehicle is rendered "dead" and inaccessible right up to the point where it is removed.<br>
* <br>
* This `Actor` is intended to sit on top of the event system that handles broadcast messaging.
*/
class DeconstructionActor extends Actor {
/** The periodic `Executor` that scraps the next vehicle on the list */
private var scrappingProcess : Cancellable = DefaultCancellable.obj
/** A `List` of currently doomed vehicles */
private var vehicles : List[DeconstructionActor.VehicleEntry] = Nil
/** The periodic `Executor` that cleans up the next vehicle on the list */
private var heapEmptyProcess : Cancellable = DefaultCancellable.obj
/** A `List` of vehicles that have been removed from the game world and are awaiting deconstruction. */
private var vehicleScrapHeap : List[DeconstructionActor.VehicleEntry] = Nil
/** The manager that helps unregister the vehicle from its current GUID scope */
private var taskResolver : ActorRef = Actor.noSender
//private[this] val log = org.log4s.getLogger
override def postStop() : Unit = {
super.postStop()
scrappingProcess.cancel
heapEmptyProcess.cancel
vehicles.foreach(entry => {
RetirementTask(entry)
DestructionTask(entry)
})
vehicleScrapHeap.foreach { DestructionTask }
}
def receive : Receive = {
/*
ask for a resolver to deal with the GUID system
when the TaskResolver is finally delivered, switch over to a behavior that actually deals with submitted vehicles
*/
case DeconstructionActor.RequestTaskResolver =>
ServiceManager.serviceManager ! Lookup("taskResolver")
case ServiceManager.LookupResult("taskResolver", endpoint) =>
taskResolver = endpoint
context.become(Processing)
case _ => ;
}
def Processing : Receive = {
case DeconstructionActor.RequestDeleteVehicle(vehicle, zone, time) =>
if(!vehicles.exists(_.vehicle == vehicle) && !vehicleScrapHeap.exists(_.vehicle == vehicle)) {
vehicles = vehicles :+ DeconstructionActor.VehicleEntry(vehicle, zone, time)
vehicle.Actor ! Vehicle.PrepareForDeletion
//kick everyone out; this is a no-blocking manual form of MountableBehavior ! Mountable.TryDismount
vehicle.Definition.MountPoints.values.foreach(seat_num => {
val zone_id : String = zone.Id
val seat : Seat = vehicle.Seat(seat_num).get
seat.Occupant match {
case Some(tplayer) =>
seat.Occupant = None
tplayer.VehicleSeated = None
if(tplayer.HasGUID) {
context.parent ! VehicleServiceMessage(zone_id, VehicleAction.KickPassenger(tplayer.GUID, 4, false, vehicle.GUID))
}
case None => ;
}
})
if(vehicles.size == 1) {
//we were the only entry so the event must be started from scratch
import scala.concurrent.ExecutionContext.Implicits.global
scrappingProcess = context.system.scheduler.scheduleOnce(DeconstructionActor.timeout, self, DeconstructionActor.StartDeleteVehicle())
}
}
case DeconstructionActor.StartDeleteVehicle() =>
scrappingProcess.cancel
heapEmptyProcess.cancel
val now : Long = System.nanoTime
val (vehiclesToScrap, vehiclesRemain) = PartitionEntries(vehicles, now)
vehicles = vehiclesRemain
vehicleScrapHeap = vehicleScrapHeap ++ vehiclesToScrap //may include existing entries
vehiclesToScrap.foreach(entry => {
val vehicle = entry.vehicle
val zone = entry.zone
RetirementTask(entry)
if(vehicle.Definition == GlobalDefinitions.ams) {
import net.psforever.types.DriveState
vehicle.DeploymentState = DriveState.Mobile //internally undeployed //TODO this should be temporary?
context.parent ! VehicleServiceMessage.AMSDeploymentChange(zone)
}
taskResolver ! DeconstructionTask(vehicle, zone)
})
if(vehiclesRemain.nonEmpty) {
val short_timeout : FiniteDuration = math.max(1, DeconstructionActor.timeout_time - (now - vehiclesRemain.head.time)) nanoseconds
import scala.concurrent.ExecutionContext.Implicits.global
scrappingProcess = context.system.scheduler.scheduleOnce(short_timeout, self, DeconstructionActor.StartDeleteVehicle())
}
if(vehicleScrapHeap.nonEmpty) {
import scala.concurrent.ExecutionContext.Implicits.global
heapEmptyProcess = context.system.scheduler.scheduleOnce(500 milliseconds, self, DeconstructionActor.TryDeleteVehicle())
}
case DeconstructionActor.TryDeleteVehicle() =>
heapEmptyProcess.cancel
val (vehiclesToScrap, vehiclesRemain) = vehicleScrapHeap.partition(entry => !entry.zone.Vehicles.contains(entry.vehicle))
vehicleScrapHeap = vehiclesRemain
vehiclesToScrap.foreach { DestructionTask }
if(vehiclesRemain.nonEmpty) {
import scala.concurrent.ExecutionContext.Implicits.global
heapEmptyProcess = context.system.scheduler.scheduleOnce(500 milliseconds, self, DeconstructionActor.TryDeleteVehicle())
}
case DeconstructionActor.FailureToDeleteVehicle(localVehicle, localZone, ex) =>
org.log4s.getLogger.error(s"vehicle deconstruction: $localVehicle failed to be properly cleaned up from zone $localZone - $ex")
case _ => ;
}
def RetirementTask(entry : DeconstructionActor.VehicleEntry) : Unit = {
val vehicle = entry.vehicle
val zone = entry.zone
vehicle.Position = Vector3.Zero //somewhere it will not disturb anything
zone.Transport ! Zone.Vehicle.Despawn(vehicle)
context.parent ! DeconstructionActor.DeleteVehicle(vehicle.GUID, zone.Id) //call up to the main event system
}
def DestructionTask(entry : DeconstructionActor.VehicleEntry) : Unit = {
val vehicle = entry.vehicle
val zone = entry.zone
taskResolver ! DeconstructionTask(vehicle, zone)
}
/**
* Construct a middleman `Task` intended to return error messages to the `DeconstructionActor`.
* @param vehicle the `Vehicle` object
* @param zone the `Zone` in which the vehicle resides
* @return a `TaskResolver.GiveTask` message
*/
def DeconstructionTask(vehicle : Vehicle, zone : Zone) : TaskResolver.GiveTask = {
import net.psforever.objects.guid.{GUIDTask, Task}
TaskResolver.GiveTask (
new Task() {
private val localVehicle = vehicle
private val localZone = zone
private val localAnnounce = self
override def isComplete : Task.Resolution.Value = Task.Resolution.Success
def Execute(resolver : ActorRef) : Unit = {
resolver ! scala.util.Success(this)
}
override def onFailure(ex : Throwable): Unit = {
localAnnounce ! DeconstructionActor.FailureToDeleteVehicle(localVehicle, localZone, ex)
}
}, List(GUIDTask.UnregisterVehicle(vehicle)(zone.GUID))
)
}
/**
* Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered.
* Separate the original `List` into two:
* a `List` of elements that have exceeded the time limit,
* and a `List` of elements that still satisfy the time limit.
* As newer entries to the `List` will always resolve later than old ones,
* and newer entries are always added to the end of the main `List`,
* processing in order is always correct.
* @param list the `List` of entries to divide
* @param now the time right now (in nanoseconds)
* @see `List.partition`
* @return a `Tuple` of two `Lists`, whose qualifications are explained above
*/
private def PartitionEntries(list : List[DeconstructionActor.VehicleEntry], now : Long) : (List[DeconstructionActor.VehicleEntry], List[DeconstructionActor.VehicleEntry]) = {
val n : Int = recursivePartitionEntries(list.iterator, now)
(list.take(n), list.drop(n)) //take and drop so to always return new lists
}
/**
* Mark the index where the `List` of elements can be divided into two:
* a `List` of elements that have exceeded the time limit,
* and a `List` of elements that still satisfy the time limit.
* @param iter the `Iterator` of entries to divide
* @param now the time right now (in nanoseconds)
* @param index a persistent record of the index where list division should occur;
* defaults to 0
* @return the index where division will occur
*/
@tailrec private def recursivePartitionEntries(iter : Iterator[DeconstructionActor.VehicleEntry], now : Long, index : Int = 0) : Int = {
if(!iter.hasNext) {
index
}
else {
val entry = iter.next()
if(now - entry.time >= DeconstructionActor.timeout_time) {
recursivePartitionEntries(iter, now, index + 1)
}
else {
index
}
}
}
}
object DeconstructionActor {
/** The wait before completely deleting a vehicle; as a Long for calculation simplicity */
private final val timeout_time : Long = 5000000000L //nanoseconds (5s)
/** The wait before completely deleting a vehicle; as a `FiniteDuration` for `Executor` simplicity */
private final val timeout : FiniteDuration = timeout_time nanoseconds
final case class RequestTaskResolver()
/**
* Message that carries information about a vehicle to be deconstructed.
* @param vehicle the `Vehicle` object
* @param zone the `Zone` in which the vehicle resides
* @param time when the vehicle was doomed
* @see `VehicleEntry`
*/
final case class RequestDeleteVehicle(vehicle : Vehicle, zone : Zone, time : Long = System.nanoTime())
/**
* Message that carries information about a vehicle to be deconstructed.
* Prompting, as compared to `RequestDeleteVehicle` which is reactionary.
* @param vehicle_guid the vehicle
* @param zone_id the `Zone` in which the vehicle resides
*/
final case class DeleteVehicle(vehicle_guid : PlanetSideGUID, zone_id : String)
/**
* Internal message used to signal a test of the queued vehicle information.
* Remove all deconstructing vehicles from the game world.
*/
private final case class StartDeleteVehicle()
/**
* Internal message used to signal a test of the queued vehicle information.
* Remove all deconstructing vehicles from the zone's globally unique identifier system.
*/
private final case class TryDeleteVehicle()
/**
* Error-passing message carrying information out of the final deconstruction GUID unregistering task.
* @param vehicle the `Vehicle` object
* @param zone the `Zone` in which the vehicle may or may not reside
* @param ex information regarding what happened
*/
private final case class FailureToDeleteVehicle(vehicle : Vehicle, zone : Zone, ex : Throwable)
/**
* Entry of vehicle information.
* The `zone` is maintained separately as a necessity, required to complete the deletion of the vehicle
* via unregistering of the vehicle and all related, registered objects.
* @param vehicle the `Vehicle` object
* @param zone the `Zone` in which the vehicle resides
* @param time when the vehicle was doomed
* @see `RequestDeleteVehicle`
*/
private final case class VehicleEntry(vehicle : Vehicle, zone : Zone, time : Long)
}

View file

@ -1,114 +0,0 @@
// Copyright (c) 2017 PSForever
package services.vehicle.support
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.{DefaultCancellable, Vehicle}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.PlanetSideGUID
import services.vehicle.VehicleServiceMessage
import scala.concurrent.duration._
/**
* Maintain and curate a list of timed `vehicle` object deconstruction tasks.<br>
* <br>
* These tasks are queued or dismissed by player activity but they are executed independent of player activity.
* A common disconnected cause of deconstruction is neglect for an extended period of time.
* At that point, the original owner of the vehicle no longer matters.
* Deconstruction neglect, however, is averted by having someone become seated.
* A realized deconstruction is entirely based on a fixed interval after an unresolved request has been received.
* The actual process of deconstructing the vehicle and cleaning up its resources is performed by an external agent.<br>
* <br>
* This `Actor` is intended to sit on top of the event system that handles broadcast messaging.
*/
class DelayedDeconstructionActor extends Actor {
/** The periodic `Executor` that scraps the next vehicle on the list */
private var monitor : Cancellable = DefaultCancellable.obj
/** A `List` of currently doomed vehicles */
private var vehicles : List[DelayedDeconstructionActor.VehicleEntry] = Nil
private[this] val log = org.log4s.getLogger
private[this] def trace(msg : String) : Unit = log.trace(msg)
def receive : Receive = {
case DelayedDeconstructionActor.ScheduleDeconstruction(vehicle, zone, timeAlive) =>
trace(s"delayed deconstruction order for $vehicle in $timeAlive")
val oldHead = vehicles.headOption
val now : Long = System.nanoTime
vehicles = (vehicles :+ DelayedDeconstructionActor.VehicleEntry(vehicle, zone, timeAlive * 1000000000L))
.sortBy(entry => entry.survivalTime - (now - entry.logTime))
if(vehicles.size == 1 || oldHead != vehicles.headOption) { //we were the only entry so the event must be started from scratch
RetimePeriodicTest()
}
case DelayedDeconstructionActor.UnscheduleDeconstruction(vehicle_guid) =>
//all tasks for this vehicle are cleared from the queue
//clear any task that is no longer valid by determination of unregistered GUID
val before = vehicles.length
val now : Long = System.nanoTime
vehicles = vehicles.filter(entry => { entry.vehicle.HasGUID && entry.vehicle.GUID != vehicle_guid })
.sortBy(entry => entry.survivalTime - (now - entry.logTime))
trace(s"attempting to clear deconstruction order for vehicle $vehicle_guid, found ${before - vehicles.length}")
RetimePeriodicTest()
case DelayedDeconstructionActor.PeriodicTaskCulling =>
//filter the list of deconstruction tasks for any that are need to be triggered
monitor.cancel
val now : Long = System.nanoTime
val (vehiclesToDecon, vehiclesRemain) = vehicles.partition(entry => { now - entry.logTime >= entry.survivalTime })
vehicles = vehiclesRemain.sortBy(_.survivalTime)
trace(s"vehicle culling - ${vehiclesToDecon.length} deconstruction tasks found; ${vehiclesRemain.length} tasks remain")
vehiclesToDecon.foreach(entry => { context.parent ! VehicleServiceMessage.RequestDeleteVehicle(entry.vehicle, entry.zone) })
RetimePeriodicTest()
case _ => ;
}
def RetimePeriodicTest() : Unit = {
monitor.cancel
vehicles.headOption match {
case None => ;
case Some(entry) =>
val retime = math.max(1, entry.survivalTime - (System.nanoTime - entry.logTime)) nanoseconds
import scala.concurrent.ExecutionContext.Implicits.global
monitor = context.system.scheduler.scheduleOnce(retime, self, DelayedDeconstructionActor.PeriodicTaskCulling)
}
}
}
object DelayedDeconstructionActor {
/**
* Timer for the repeating executor.
*/
private final val periodicTest : FiniteDuration = 5000000000L nanoseconds //5s
/**
* Queue a future vehicle deconstruction action.
* @param vehicle the `Vehicle` object
* @param zone the `Zone` that the vehicle currently occupies
* @param survivalTime how long until the vehicle will be deconstructed in seconds
*/
final case class ScheduleDeconstruction(vehicle : Vehicle, zone : Zone, survivalTime : Long)
/**
* Dequeue a vehicle from being deconstructed.
* @param vehicle_guid the vehicle
*/
final case class UnscheduleDeconstruction(vehicle_guid : PlanetSideGUID)
/**
* A message the `Actor` sends to itself.
* The trigger for the periodic deconstruction task.
*/
private final case class PeriodicTaskCulling()
/**
* An entry that stores vehicle deconstruction tasks.
* @param vehicle the `Vehicle` object
* @param zone the `Zone` that the vehicle currently occupies
* @param survivalTime how long until the vehicle will be deconstructed in nanoseconds
* @param logTime when this deconstruction request was initially created in nanoseconds;
* initialized by default to a "now"
*/
private final case class VehicleEntry(vehicle : Vehicle, zone : Zone, survivalTime : Long, logTime : Long = System.nanoTime())
}

View file

@ -0,0 +1,56 @@
// Copyright (c) 2017 PSForever
package services.vehicle.support
import net.psforever.objects.Vehicle
import net.psforever.objects.guid.{GUIDTask, TaskResolver}
import net.psforever.objects.zones.Zone
import services.vehicle.{VehicleAction, VehicleServiceMessage}
import services.{RemoverActor, Service}
import scala.concurrent.duration._
class VehicleRemover extends RemoverActor {
final val FirstStandardDuration : FiniteDuration = 5 minutes
final val SecondStandardDuration : FiniteDuration = 5 seconds
def InclusionTest(entry : RemoverActor.Entry) : Boolean = {
entry.obj.isInstanceOf[Vehicle]
}
def InitialJob(entry : RemoverActor.Entry) : Unit = { }
def FirstJob(entry : RemoverActor.Entry) : Unit = {
val vehicle = entry.obj.asInstanceOf[Vehicle]
val vehicleGUID = vehicle.GUID
val zoneId = entry.zone.Id
vehicle.Actor ! Vehicle.PrepareForDeletion
//kick out all passengers
vehicle.Definition.MountPoints.values.foreach(mount => {
val seat = vehicle.Seat(mount).get
seat.Occupant match {
case Some(tplayer) =>
seat.Occupant = None
tplayer.VehicleSeated = None
if(tplayer.HasGUID) {
context.parent ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(tplayer.GUID, 4, false, vehicleGUID))
}
case None => ;
}
})
}
override def SecondJob(entry : RemoverActor.Entry) : Unit = {
val vehicle = entry.obj.asInstanceOf[Vehicle]
val zone = entry.zone
zone.Transport ! Zone.Vehicle.Despawn(vehicle)
context.parent ! VehicleServiceMessage(zone.Id, VehicleAction.UnloadVehicle(Service.defaultPlayerGUID, zone, vehicle))
super.SecondJob(entry)
}
def ClearanceTest(entry : RemoverActor.Entry) : Boolean = entry.obj.asInstanceOf[Vehicle].Seats.values.count(_.isOccupied) == 0
def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = {
GUIDTask.UnregisterVehicle(entry.obj.asInstanceOf[Vehicle])(entry.zone.GUID)
}
}

View file

@ -4,9 +4,10 @@ import akka.routing.RandomPool
import net.psforever.objects._ import net.psforever.objects._
import net.psforever.objects.guid.{GUIDTask, TaskResolver} import net.psforever.objects.guid.{GUIDTask, TaskResolver}
import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap} import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap}
import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.packet.game.objectcreate.{DroppedItemData, ObjectCreateMessageParent, PlacementData}
import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID, PlayerStateMessageUpstream}
import net.psforever.types.{CharacterGender, ExoSuitType, PlanetSideEmpire, Vector3} import net.psforever.types.{CharacterGender, ExoSuitType, PlanetSideEmpire, Vector3}
import services.{Service, ServiceManager} import services.{RemoverActor, Service, ServiceManager}
import services.avatar._ import services.avatar._
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -93,32 +94,59 @@ class ConcealPlayerTest extends ActorTest {
} }
class EquipmentInHandTest extends ActorTest { class EquipmentInHandTest extends ActorTest {
val tool = Tool(GlobalDefinitions.beamer) ServiceManager.boot(system) ! ServiceManager.Register(RandomPool(1).props(Props[TaskResolver]), "taskResolver")
val service = system.actorOf(Props[AvatarService], "release-test-service")
val zone = new Zone("test", new ZoneMap("test-map"), 0)
val taskResolver = system.actorOf(Props[TaskResolver], "release-test-resolver")
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(11), 2),
toolDef.Packet.ConstructorData(tool).get
)
"AvatarService" should { "AvatarService" should {
"pass EquipmentInHand" in { "pass EquipmentInHand" in {
ServiceManager.boot(system)
val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName)
service ! Service.Join("test") service ! Service.Join("test")
service ! AvatarServiceMessage("test", AvatarAction.EquipmentInHand(PlanetSideGUID(10), PlanetSideGUID(11), 2, tool)) service ! AvatarServiceMessage("test", AvatarAction.EquipmentInHand(PlanetSideGUID(10), PlanetSideGUID(11), 2, tool))
expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentInHand(PlanetSideGUID(11), 2, tool))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentInHand(pkt)))
} }
} }
} }
class EquipmentOnGroundTest extends ActorTest { class DroptItemTest extends ActorTest {
ServiceManager.boot(system) ! ServiceManager.Register(RandomPool(1).props(Props[TaskResolver]), "taskResolver")
val service = system.actorOf(Props[AvatarService], "release-test-service")
val zone = new Zone("test", new ZoneMap("test-map"), 0)
val taskResolver = system.actorOf(Props[TaskResolver], "drop-item-test-resolver")
zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-item-test-zone")
zone.Actor ! Zone.Init()
val toolDef = GlobalDefinitions.beamer val toolDef = GlobalDefinitions.beamer
val tool = Tool(toolDef) val tool = Tool(toolDef)
tool.AmmoSlots.head.Box.GUID = PlanetSideGUID(1) tool.Position = Vector3(1,2,3)
val cdata = toolDef.Packet.ConstructorData(tool).get tool.Orientation = Vector3(4,5,6)
tool.GUID = PlanetSideGUID(40)
tool.AmmoSlots.head.Box.GUID = PlanetSideGUID(41)
val pkt = ObjectCreateMessage(
toolDef.ObjectId,
tool.GUID,
DroppedItemData(
PlacementData(tool.Position, tool.Orientation),
toolDef.Packet.ConstructorData(tool).get
)
)
"AvatarService" should { "AvatarService" should {
"pass EquipmentOnGround" in { "pass DropItem" in {
ServiceManager.boot(system)
val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName)
service ! Service.Join("test") service ! Service.Join("test")
service ! AvatarServiceMessage("test", AvatarAction.EquipmentOnGround(PlanetSideGUID(10), Vector3(300f, 200f, 100f), Vector3(450f, 300f, 150f), toolDef.ObjectId, PlanetSideGUID(11), cdata)) service ! AvatarServiceMessage("test", AvatarAction.DropItem(PlanetSideGUID(10), tool, zone))
expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentOnGround(Vector3(300f, 200f, 100f), Vector3(450f, 300f, 150f), toolDef.ObjectId, PlanetSideGUID(11), cdata))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.DropItem(pkt)))
} }
} }
} }
@ -193,6 +221,45 @@ class PlayerStateTest extends ActorTest {
} }
} }
class PickupItemATest extends ActorTest {
val obj = Player(Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1))
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[AvatarService], AvatarServiceTest.TestName)
service ! Service.Join("test")
service ! AvatarServiceMessage("test", AvatarAction.PickupItem(PlanetSideGUID(10), Zone.Nowhere, obj, 0, tool))
expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentInHand(pkt)))
}
}
class PickupItemBTest extends ActorTest {
val obj = Player(Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1))
val tool = Tool(GlobalDefinitions.beamer)
tool.GUID = PlanetSideGUID(40)
"pass PickUpItem as ObjectDelete (not visible inventory space)" in {
ServiceManager.boot(system)
val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName)
service ! Service.Join("test")
service ! AvatarServiceMessage("test", AvatarAction.PickupItem(PlanetSideGUID(10), Zone.Nowhere, obj, 6, tool))
expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectDelete(tool.GUID, 0)))
}
}
class ReloadTest extends ActorTest { class ReloadTest extends ActorTest {
"AvatarService" should { "AvatarService" should {
"pass Reload" in { "pass Reload" in {
@ -320,14 +387,14 @@ class AvatarReleaseTest extends ActorTest {
taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID)
assert(zone.Corpses.isEmpty) assert(zone.Corpses.isEmpty)
zone.Population ! Zone.Corpse.Add(obj) zone.Population ! Zone.Corpse.Add(obj)
expectNoMsg(100 milliseconds) //spacer expectNoMsg(200 milliseconds) //spacer
assert(zone.Corpses.size == 1) assert(zone.Corpses.size == 1)
assert(obj.HasGUID) assert(obj.HasGUID)
val guid = obj.GUID val guid = obj.GUID
service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone, Some(1000000000))) //alive for one second service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone, Some(1 second))) //alive for one second
val reply1 = receiveOne(100 milliseconds) val reply1 = receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[AvatarServiceResponse]) assert(reply1.isInstanceOf[AvatarServiceResponse])
val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] val reply1msg = reply1.asInstanceOf[AvatarServiceResponse]
assert(reply1msg.toChannel == "/test/Avatar") assert(reply1msg.toChannel == "/test/Avatar")
@ -343,7 +410,7 @@ class AvatarReleaseTest extends ActorTest {
assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete])
assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid)
expectNoMsg(1000 milliseconds) expectNoMsg(1 seconds)
assert(zone.Corpses.isEmpty) assert(zone.Corpses.isEmpty)
assert(!obj.HasGUID) assert(!obj.HasGUID)
} }
@ -369,14 +436,14 @@ class AvatarReleaseEarly1Test extends ActorTest {
taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID)
assert(zone.Corpses.isEmpty) assert(zone.Corpses.isEmpty)
zone.Population ! Zone.Corpse.Add(obj) zone.Population ! Zone.Corpse.Add(obj)
expectNoMsg(100 milliseconds) //spacer expectNoMsg(200 milliseconds) //spacer
assert(zone.Corpses.size == 1) assert(zone.Corpses.size == 1)
assert(obj.HasGUID) assert(obj.HasGUID)
val guid = obj.GUID val guid = obj.GUID
service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone)) //3+ minutes! service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone)) //3+ minutes!
val reply1 = receiveOne(100 milliseconds) val reply1 = receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[AvatarServiceResponse]) assert(reply1.isInstanceOf[AvatarServiceResponse])
val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] val reply1msg = reply1.asInstanceOf[AvatarServiceResponse]
assert(reply1msg.toChannel == "/test/Avatar") assert(reply1msg.toChannel == "/test/Avatar")
@ -384,8 +451,8 @@ class AvatarReleaseEarly1Test extends ActorTest {
assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release]) assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release])
assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj) assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj)
service ! AvatarServiceMessage.RemoveSpecificCorpse(List(obj)) //IMPORTANT: ONE ENTRY service ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), zone)) //IMPORTANT: ONE ENTRY
val reply2 = receiveOne(100 milliseconds) val reply2 = receiveOne(200 milliseconds)
assert(reply2.isInstanceOf[AvatarServiceResponse]) assert(reply2.isInstanceOf[AvatarServiceResponse])
val reply2msg = reply2.asInstanceOf[AvatarServiceResponse] val reply2msg = reply2.asInstanceOf[AvatarServiceResponse]
assert(reply2msg.toChannel.equals("/test/Avatar")) assert(reply2msg.toChannel.equals("/test/Avatar"))
@ -393,7 +460,7 @@ class AvatarReleaseEarly1Test extends ActorTest {
assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete])
assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid)
expectNoMsg(600 milliseconds) expectNoMsg(1 seconds)
assert(zone.Corpses.isEmpty) assert(zone.Corpses.isEmpty)
assert(!obj.HasGUID) assert(!obj.HasGUID)
} }
@ -420,14 +487,14 @@ class AvatarReleaseEarly2Test extends ActorTest {
taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID)
assert(zone.Corpses.isEmpty) assert(zone.Corpses.isEmpty)
zone.Population ! Zone.Corpse.Add(obj) zone.Population ! Zone.Corpse.Add(obj)
expectNoMsg(100 milliseconds) //spacer expectNoMsg(200 milliseconds) //spacer
assert(zone.Corpses.size == 1) assert(zone.Corpses.size == 1)
assert(obj.HasGUID) assert(obj.HasGUID)
val guid = obj.GUID val guid = obj.GUID
service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone)) //3+ minutes! service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone)) //3+ minutes!
val reply1 = receiveOne(100 milliseconds) val reply1 = receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[AvatarServiceResponse]) assert(reply1.isInstanceOf[AvatarServiceResponse])
val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] val reply1msg = reply1.asInstanceOf[AvatarServiceResponse]
assert(reply1msg.toChannel == "/test/Avatar") assert(reply1msg.toChannel == "/test/Avatar")
@ -435,7 +502,7 @@ class AvatarReleaseEarly2Test extends ActorTest {
assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release]) assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release])
assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj) assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj)
service ! AvatarServiceMessage.RemoveSpecificCorpse(List(objAlt, obj)) //IMPORTANT: TWO ENTRIES service ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(objAlt, obj), zone)) //IMPORTANT: TWO ENTRIES
val reply2 = receiveOne(100 milliseconds) val reply2 = receiveOne(100 milliseconds)
assert(reply2.isInstanceOf[AvatarServiceResponse]) assert(reply2.isInstanceOf[AvatarServiceResponse])
val reply2msg = reply2.asInstanceOf[AvatarServiceResponse] val reply2msg = reply2.asInstanceOf[AvatarServiceResponse]
@ -444,7 +511,7 @@ class AvatarReleaseEarly2Test extends ActorTest {
assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete])
assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid)
expectNoMsg(600 milliseconds) expectNoMsg(1 seconds)
assert(zone.Corpses.isEmpty) assert(zone.Corpses.isEmpty)
assert(!obj.HasGUID) assert(!obj.HasGUID)
} }

View file

@ -0,0 +1,537 @@
// Copyright (c) 2017 PSForever
import akka.actor.{ActorRef, Props}
import akka.routing.RandomPool
import akka.testkit.TestProbe
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{EquipmentDefinition, ObjectDefinition}
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.guid.TaskResolver
import net.psforever.objects.zones.{Zone, ZoneMap}
import net.psforever.packet.game.PlanetSideGUID
import services.{RemoverActor, ServiceManager}
import scala.concurrent.duration._
class StandardRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
"RemoverActor" should {
"handle a simple task" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere)
val reply1 = probe.receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert])
val reply2 = probe.receiveOne(200 milliseconds)
assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert])
probe.expectNoMsg(1 seconds) //delay
val reply3 = probe.receiveOne(300 milliseconds)
assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert])
val reply4 = probe.receiveOne(300 milliseconds)
assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
val reply5 = probe.receiveOne(300 milliseconds)
assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert])
val reply6 = probe.receiveOne(500 milliseconds)
assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
val reply7 = probe.receiveOne(500 milliseconds)
assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
}
}
}
class DelayedRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
"RemoverActor" should {
"handle a simple task (timed)" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(100 milliseconds))
val reply1 = probe.receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert])
val reply2 = probe.receiveOne(200 milliseconds)
assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert])
//no delay
val reply3 = probe.receiveOne(300 milliseconds)
assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert])
val reply4 = probe.receiveOne(300 milliseconds)
assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
val reply5 = probe.receiveOne(300 milliseconds)
assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert])
val reply6 = probe.receiveOne(300 milliseconds)
assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
val reply7 = probe.receiveOne(300 milliseconds)
assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
}
}
}
class ExcludedRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
val AlternateTestObject = new PlanetSideGameObject() { def Definition = new ObjectDefinition(0) { } }
"RemoverActor" should {
"allow only specific objects" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(AlternateTestObject, Zone.Nowhere)
val reply1 = probe.receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert])
expectNoMsg(2 seconds)
//RemoverActor is stalled because it received an object that it was not allowed to act upon
}
}
}
class MultipleRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } }
"RemoverActor" should {
"work on parallel tasks" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere)
remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere)
val replies = probe.receiveN(14, 5 seconds)
var ita : Int = 0
var ija : Int = 0
var fja : Int = 0
var cta : Int = 0
var sja : Int = 0
var dta : Int = 0
var dtr : Int = 0
replies.collect {
case RemoverActorTest.InclusionTestAlert() => ita += 1
case RemoverActorTest.InitialJobAlert() => ija += 1
case RemoverActorTest.FirstJobAlert() => fja += 1
case RemoverActorTest.ClearanceTestAlert() => cta += 1
case RemoverActorTest.SecondJobAlert() => sja += 1
case RemoverActorTest.DeletionTaskAlert() => dta += 1
case RemoverActorTest.DeletionTaskRunAlert() => dtr += 1
case msg => assert(false, s"$msg")
}
assert(ita == 2 && ija == 2 && fja == 2 && cta == 2 && sja == 2 && dta == 2 && dtr == 2)
}
}
}
class HurrySpecificRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
"RemoverActor" should {
"be able to hurry certain tasks" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(10 minutes)) //TEN MINUTE WAIT
val reply1 = probe.receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert])
val reply2 = probe.receiveOne(200 milliseconds)
assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert])
probe.expectNoMsg(3 seconds) //long delay, longer than standard but not yet 10 minutes
remover ! RemoverActor.HurrySpecific(List(RemoverActorTest.TestObject), Zone.Nowhere) //hurried
val reply3 = probe.receiveOne(300 milliseconds)
assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert])
val reply4 = probe.receiveOne(300 milliseconds)
assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
val reply5 = probe.receiveOne(300 milliseconds)
assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert])
val reply6 = probe.receiveOne(500 milliseconds)
assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
val reply7 = probe.receiveOne(500 milliseconds)
assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
}
}
}
class HurrySelectionRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } }
"RemoverActor" should {
"be able to hurry certain tasks, but let others finish normally" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds))
remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(10 seconds))
val replies = probe.receiveN(4, 5 seconds)
var ita : Int = 0
var ija : Int = 0
replies.collect {
case RemoverActorTest.InclusionTestAlert() => ita += 1
case RemoverActorTest.InitialJobAlert() => ija += 1
case msg => assert(false, s"$msg")
}
assert(ita == 2 && ija == 2)
probe.expectNoMsg(3 seconds) //long delay, longer than standard but not yet 5 seconds
remover ! RemoverActor.HurrySpecific(List(RemoverActorTest.TestObject), Zone.Nowhere) //hurried
//first
val reply3a = probe.receiveOne(300 milliseconds)
assert(reply3a.isInstanceOf[RemoverActorTest.FirstJobAlert])
val reply4a = probe.receiveOne(300 milliseconds)
assert(reply4a.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
val reply5a = probe.receiveOne(300 milliseconds)
assert(reply5a.isInstanceOf[RemoverActorTest.SecondJobAlert])
val reply6a = probe.receiveOne(500 milliseconds)
assert(reply6a.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
val reply7a = probe.receiveOne(500 milliseconds)
assert(reply7a.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
//second
remover ! RemoverActor.HurrySpecific(List(TestObject2), Zone.Nowhere) //hurried
val reply3b = probe.receiveOne(300 milliseconds)
assert(reply3b.isInstanceOf[RemoverActorTest.FirstJobAlert])
val reply4b = probe.receiveOne(300 milliseconds)
assert(reply4b.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
val reply5b = probe.receiveOne(300 milliseconds)
assert(reply5b.isInstanceOf[RemoverActorTest.SecondJobAlert])
val reply6b = probe.receiveOne(500 milliseconds)
assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
val reply7b = probe.receiveOne(500 milliseconds)
assert(reply7b.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
}
}
}
class HurryMultipleRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } }
final val TestObject3 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(3) } }
"RemoverActor" should {
"be able to hurry certain tasks, but only valid ones" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds))
remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds))
val replies = probe.receiveN(4, 5 seconds)
var ita : Int = 0
var ija : Int = 0
replies.collect {
case RemoverActorTest.InclusionTestAlert() => ita += 1
case RemoverActorTest.InitialJobAlert() => ija += 1
case msg => assert(false, s"$msg")
}
assert(ita == 2 && ija == 2)
probe.expectNoMsg(3 seconds) //long delay, longer than standard but not yet 5 seconds
remover ! RemoverActor.HurrySpecific(List(RemoverActorTest.TestObject, TestObject3), Zone.Nowhere) //multiple hurried, only one valid
//first
val reply3a = probe.receiveOne(300 milliseconds)
assert(reply3a.isInstanceOf[RemoverActorTest.FirstJobAlert])
val reply4a = probe.receiveOne(300 milliseconds)
assert(reply4a.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
val reply5a = probe.receiveOne(300 milliseconds)
assert(reply5a.isInstanceOf[RemoverActorTest.SecondJobAlert])
val reply6a = probe.receiveOne(500 milliseconds)
assert(reply6a.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
val reply7a = probe.receiveOne(500 milliseconds)
assert(reply7a.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
//second
remover ! RemoverActor.HurrySpecific(List(TestObject2), Zone.Nowhere) //hurried
val reply3b = probe.receiveOne(300 milliseconds)
assert(reply3b.isInstanceOf[RemoverActorTest.FirstJobAlert])
val reply4b = probe.receiveOne(300 milliseconds)
assert(reply4b.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
val reply5b = probe.receiveOne(300 milliseconds)
assert(reply5b.isInstanceOf[RemoverActorTest.SecondJobAlert])
val reply6b = probe.receiveOne(500 milliseconds)
assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
val reply7b = probe.receiveOne(500 milliseconds)
assert(reply7b.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
}
}
}
class HurryByZoneRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } }
final val TestObject3 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(3) } }
final val zone = new Zone("test", new ZoneMap("test-map"), 11)
"RemoverActor" should {
"be able to hurry certain tasks by their zone" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds))
remover ! RemoverActor.AddTask(TestObject2, zone, Some(5 seconds))
remover ! RemoverActor.AddTask(TestObject3, Zone.Nowhere, Some(5 seconds))
val replies1 = probe.receiveN(6, 5 seconds)
var ita : Int = 0
var ija : Int = 0
replies1.collect {
case RemoverActorTest.InclusionTestAlert() => ita += 1
case RemoverActorTest.InitialJobAlert() => ija += 1
case msg => assert(false, s"$msg")
}
assert(ita == 3 && ija == 3)
probe.expectNoMsg(3 seconds) //long delay, longer than standard but not yet 5 seconds
remover ! RemoverActor.HurrySpecific(List(), Zone.Nowhere) //multiple hurried, only the two entries with Zone.Nowhere
//
val replies2 = probe.receiveN(10, 5 seconds)
var fja : Int = 0
var cta : Int = 0
var sja : Int = 0
var dta : Int = 0
var dtr : Int = 0
replies2.collect {
case RemoverActorTest.InclusionTestAlert() => ita += 1
case RemoverActorTest.InitialJobAlert() => ija += 1
case RemoverActorTest.FirstJobAlert() => fja += 1
case RemoverActorTest.ClearanceTestAlert() => cta += 1
case RemoverActorTest.SecondJobAlert() => sja += 1
case RemoverActorTest.DeletionTaskAlert() => dta += 1
case RemoverActorTest.DeletionTaskRunAlert() => dtr += 1
case msg => assert(false, s"$msg")
}
assert(fja == 2 && cta == 2 && sja == 2 && dta == 2 && dtr == 2)
//final
remover ! RemoverActor.HurrySpecific(List(), zone) //hurried
val reply3b = probe.receiveOne(300 milliseconds)
assert(reply3b.isInstanceOf[RemoverActorTest.FirstJobAlert])
val reply4b = probe.receiveOne(300 milliseconds)
assert(reply4b.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
val reply5b = probe.receiveOne(300 milliseconds)
assert(reply5b.isInstanceOf[RemoverActorTest.SecondJobAlert])
val reply6b = probe.receiveOne(500 milliseconds)
assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
val reply7b = probe.receiveOne(500 milliseconds)
assert(reply7b.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
}
}
}
class HurryAllRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } }
final val TestObject3 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(3) } }
"RemoverActor" should {
"be able to hurry all tasks to completion" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(20 seconds))
remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(15 seconds))
remover ! RemoverActor.AddTask(TestObject3, Zone.Nowhere, Some(10 seconds))
val replies1 = probe.receiveN(6, 5 seconds)
var ita : Int = 0
var ija : Int = 0
replies1.collect {
case RemoverActorTest.InclusionTestAlert() => ita += 1
case RemoverActorTest.InitialJobAlert() => ija += 1
case msg => assert(false, s"$msg")
}
assert(ita == 3 && ija == 3)
probe.expectNoMsg(3 seconds) //long delay, longer than standard but not yet longer than any of the tasks
remover ! RemoverActor.HurryAll() //all hurried
//
val replies2 = probe.receiveN(15, 5 seconds)
var fja : Int = 0
var cta : Int = 0
var sja : Int = 0
var dta : Int = 0
var dtr : Int = 0
replies2.collect {
case RemoverActorTest.InclusionTestAlert() => ita += 1
case RemoverActorTest.InitialJobAlert() => ija += 1
case RemoverActorTest.FirstJobAlert() => fja += 1
case RemoverActorTest.ClearanceTestAlert() => cta += 1
case RemoverActorTest.SecondJobAlert() => sja += 1
case RemoverActorTest.DeletionTaskAlert() => dta += 1
case RemoverActorTest.DeletionTaskRunAlert() => dtr += 1
case msg => assert(false, s"$msg")
}
assert(fja == 3 && cta == 3 && sja == 3 && dta == 3 && dtr == 3)
}
}
}
class ClearSelectionRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } }
"RemoverActor" should {
"be able to clear certain tasks" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds))
remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds))
val replies = probe.receiveN(4, 5 seconds)
var ita : Int = 0
var ija : Int = 0
replies.collect {
case RemoverActorTest.InclusionTestAlert() => ita += 1
case RemoverActorTest.InitialJobAlert() => ija += 1
case msg => assert(false, s"$msg")
}
assert(ita == 2 && ija == 2)
probe.expectNoMsg(4 seconds) //long delay, longer than standard but not yet 5 seconds
remover ! RemoverActor.ClearSpecific(List(RemoverActorTest.TestObject), Zone.Nowhere) //cleared
//
val reply3 = probe.receiveOne(2 seconds)
assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert])
val reply4 = probe.receiveOne(300 milliseconds)
assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
val reply5 = probe.receiveOne(300 milliseconds)
assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert])
val reply6 = probe.receiveOne(500 milliseconds)
assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
val reply7 = probe.receiveOne(500 milliseconds)
assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
//wait
probe.expectNoMsg(2 seconds) //nothing more to do
}
}
}
class ClearAllRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } }
"RemoverActor" should {
"be able to clear all tasks, with no more work on them" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds))
remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds))
val replies = probe.receiveN(4, 5 seconds)
var ita : Int = 0
var ija : Int = 0
replies.collect {
case RemoverActorTest.InclusionTestAlert() => ita += 1
case RemoverActorTest.InitialJobAlert() => ija += 1
case msg => assert(false, s"$msg")
}
assert(ita == 2 && ija == 2)
probe.expectNoMsg(4 seconds) //long delay, longer than standard but not yet 5 seconds
remover ! RemoverActor.ClearAll() //cleared
//wait
probe.expectNoMsg(3 seconds) //nothing more to do
}
}
}
class EarlyDeathRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } }
"RemoverActor" should {
"be able to hurry certain tasks" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds))
remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds))
val replies = probe.receiveN(4, 5 seconds)
var ita : Int = 0
var ija : Int = 0
replies.collect {
case RemoverActorTest.InclusionTestAlert() => ita += 1
case RemoverActorTest.InitialJobAlert() => ija += 1
case msg => assert(false, s"$msg")
}
assert(ita == 2 && ija == 2)
probe.expectNoMsg(2 seconds)
remover ! akka.actor.PoisonPill
//
val replies2 = probe.receiveN(8, 5 seconds)
var fja : Int = 0
var cta : Int = 0
var sja : Int = 0
var dta : Int = 0
var dtr : Int = 0
replies2.collect {
case RemoverActorTest.FirstJobAlert() => fja += 1
case RemoverActorTest.ClearanceTestAlert() => cta += 1
case RemoverActorTest.SecondJobAlert() => sja += 1
case RemoverActorTest.DeletionTaskAlert() => dta += 1
case RemoverActorTest.DeletionTaskRunAlert() => dtr += 1
case msg => assert(false, s"$msg")
}
assert(fja == 2 && cta == 0 && sja == 2 && dta == 2 && dtr == 2) //no clearance tests
}
}
}
object RemoverActorTest {
final val TestObject = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(1) } }
final case class InclusionTestAlert()
final case class InitialJobAlert()
final case class FirstJobAlert()
final case class SecondJobAlert()
final case class ClearanceTestAlert()
final case class DeletionTaskAlert()
final case class DeletionTaskRunAlert()
class TestRemover(probe : TestProbe) extends RemoverActor {
import net.psforever.objects.guid.{Task, TaskResolver}
val FirstStandardDuration = 1 seconds
val SecondStandardDuration = 100 milliseconds
def InclusionTest(entry : RemoverActor.Entry) : Boolean = {
probe.ref ! InclusionTestAlert()
entry.obj.isInstanceOf[Equipment]
}
def InitialJob(entry : RemoverActor.Entry) : Unit = {
probe.ref ! InitialJobAlert()
}
def FirstJob(entry : RemoverActor.Entry) : Unit = {
probe.ref ! FirstJobAlert()
}
override def SecondJob(entry : RemoverActor.Entry) : Unit = {
probe.ref ! SecondJobAlert()
super.SecondJob(entry)
}
def ClearanceTest(entry : RemoverActor.Entry) : Boolean = {
probe.ref ! ClearanceTestAlert()
true
}
def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = {
probe.ref ! DeletionTaskAlert()
TaskResolver.GiveTask(new Task() {
private val localProbe = probe
override def isComplete = Task.Resolution.Success
def Execute(resolver : ActorRef) : Unit = {
localProbe.ref ! DeletionTaskRunAlert()
resolver ! scala.util.Success(this)
}
})
}
}
}