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.types.PlanetSideEmpire
import scala.concurrent.duration._
object GlobalDefinitions {
/*
Implants
@ -2716,6 +2718,7 @@ object GlobalDefinitions {
ams.Deployment = true
ams.DeployTime = 2000
ams.UndeployTime = 2000
ams.DeconstructionTime = Some(20 minutes)
ams.AutoPilotSpeeds = (18, 6)
ams.Packet = utilityConverter
@ -2728,6 +2731,7 @@ object GlobalDefinitions {
router.Deployment = true
router.DeployTime = 2000
router.UndeployTime = 2000
router.DeconstructionTime = Duration(20, "minutes")
router.AutoPilotSpeeds = (16, 6)
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 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 = {
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 scala.collection.mutable
import scala.concurrent.duration._
/**
* 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 canBeOwned : Boolean = true
private var serverVehicleOverrideSpeeds : (Int, Int) = (0, 0)
private var deconTime : Option[FiniteDuration] = None
Name = "vehicle"
Packet = VehicleDefinition.converter
@ -83,6 +85,18 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) {
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_=(dtime : Int) : Int = {

View file

@ -89,7 +89,7 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
if(accessor == ActorRef.noSender) {
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")
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")
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])
}
/**
* Message to relinguish an item and place in on the ground.
* @param item the piece of `Equipment`
* @param pos where it is dropped
* @param orient in which direction it is facing when dropped
*/
final case class DropItemOnGround(item : Equipment, pos : Vector3, orient : Vector3)
object Ground {
final case class DropItem(item : Equipment, pos : Vector3, orient : Vector3)
final case class ItemOnGround(item : Equipment, pos : Vector3, orient : Vector3)
final case class CanNotDropItem(zone : Zone, item : Equipment, reason : String)
/**
* Message to attempt to acquire an item from the ground (before somoene else?).
* @param player who wants the piece of `Equipment`
* @param item_guid the unique identifier of the piece of `Equipment`
*/
final case class GetItemOnGround(player : Player, item_guid : PlanetSideGUID)
final case class PickupItem(item_guid : PlanetSideGUID)
final case class ItemInHand(item : Equipment)
final case class CanNotPickupItem(zone : Zone, item_guid : PlanetSideGUID, reason : String)
/**
* 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)
final case class RemoveItem(item_guid : PlanetSideGUID)
}
object Vehicle {
final case class Spawn(vehicle : Vehicle)

View file

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

View file

@ -12,22 +12,35 @@ import scala.collection.mutable.ListBuffer
* na
* @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
def receive : Receive = {
case Zone.DropItemOnGround(item, pos, orient) =>
item.Position = pos
item.Orientation = orient
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")
case Zone.Ground.DropItem(item, pos, orient) =>
sender ! (if(!item.HasGUID) {
Zone.Ground.CanNotDropItem(zone, item, "not registered yet")
}
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 _ => ;
}

View file

@ -19,13 +19,18 @@ final case class ActionResultMessage(successful : Boolean,
}
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] = (
("successful" | bool) >>:~ { res =>

View file

@ -40,6 +40,14 @@ final case class ObjectDetachMessage(parent_guid : PlanetSideGUID,
}
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] = (
("parent_guid" | PlanetSideGUID.codec) ::
("child_guid" | PlanetSideGUID.codec) ::

View file

@ -38,7 +38,7 @@ class ActionResultMessageTest extends Specification {
}
"encode (pass, minimal)" in {
val msg = ActionResultMessage()
val msg = ActionResultMessage.Pass
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_pass
@ -52,7 +52,7 @@ class ActionResultMessageTest extends Specification {
}
"encode (fail, minimal)" in {
val msg = ActionResultMessage(1)
val msg = ActionResultMessage.Fail(1)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
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 pkt = PacketCoding.EncodePacket(msg).require.toByteVector
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 {
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.ExoSuit = ExoSuitType.Infiltration
obj.VisibleSlots mustEqual Set(0,4)
obj.ExoSuit = ExoSuitType.MAX
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 org.specs2.mutable.Specification
import scala.concurrent.duration.Duration
import scala.concurrent.duration._
class ZoneTest extends Specification {
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)
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 {
val zone = new Zone("test", new ZoneMap(""), 0)
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-item-test") ! "!"
receiveOne(Duration.create(200, "ms")) //consume
receiveOne(1 second) //consume
assert(!zone.EquipmentOnGround.contains(item))
zone.Ground ! Zone.Ground.DropItem(item, Vector3(1.1f, 2.2f, 3.3f), Vector3(4.4f, 5.5f, 6.6f))
assert(zone.EquipmentOnGround.isEmpty)
assert(item.Position == Vector3.Zero)
assert(item.Orientation == Vector3.Zero)
zone.Ground ! Zone.DropItemOnGround(item, Vector3(1.1f, 2.2f, 3.3f), Vector3(4.4f, 5.5f, 6.6f))
expectNoMsg(Duration.create(100, "ms"))
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))
val reply = receiveOne(200 milliseconds)
assert(reply.isInstanceOf[Zone.Ground.ItemOnGround])
assert(reply.asInstanceOf[Zone.Ground.ItemOnGround].item == item)
assert(reply.asInstanceOf[Zone.Ground.ItemOnGround].pos == Vector3(1.1f, 2.2f, 3.3f))
assert(reply.asInstanceOf[Zone.Ground.ItemOnGround].orient == Vector3(4.4f, 5.5f, 6.6f))
assert(zone.EquipmentOnGround.contains(item))
}
}
}
"get item from ground (success)" in {
val zone = new Zone("test", new ZoneMap(""), 0)
val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5))
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "get-item-test-good") ! "!"
receiveOne(Duration.create(200, "ms")) //consume
zone.Ground ! Zone.DropItemOnGround(item, Vector3.Zero, Vector3.Zero)
expectNoMsg(Duration.create(100, "ms"))
class ZoneGroundCanNotDropItem1Test 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)
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!"
assert(zone.EquipmentOnGround == List(item))
zone.Ground ! Zone.GetItemOnGround(player, PlanetSideGUID(10))
val reply = receiveOne(Duration.create(100, "ms"))
"DropItem" should {
"not drop an item that is not registered" in {
receiveOne(1 second) //consume
assert(!zone.EquipmentOnGround.contains(item))
zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero)
assert(zone.EquipmentOnGround.isEmpty)
assert(reply.isInstanceOf[Zone.ItemFromGround])
assert(reply.asInstanceOf[Zone.ItemFromGround].player == player)
assert(reply.asInstanceOf[Zone.ItemFromGround].item == item)
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 == "not registered yet")
assert(!zone.EquipmentOnGround.contains(item))
}
}
}
"get item from ground (failure)" in {
val zone = new Zone("test", new ZoneMap(""), 0)
val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5))
system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "get-item-test-fail") ! "!"
receiveOne(Duration.create(200, "ms")) //consume
zone.Ground ! Zone.DropItemOnGround(item, Vector3.Zero, Vector3.Zero)
expectNoMsg(Duration.create(100, "ms"))
class ZoneGroundCanNotDropItem2Test 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") ! "!"
assert(zone.EquipmentOnGround == List(item))
zone.Ground ! Zone.GetItemOnGround(player, PlanetSideGUID(11)) //wrong guid
expectNoMsg(Duration.create(500, "ms"))
"DropItem" should {
"not drop an item that is not registered to the zone" in {
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.Future
import scala.util.{Failure, Success}
implicit val timeout = Timeout(200 milliseconds)
implicit val timeout = Timeout(500 milliseconds)
val requestVehicleEventBus : Future[ServiceManager.LookupResult] =
(ServiceManager.serviceManager ask ServiceManager.Lookup("vehicle")).mapTo[ServiceManager.LookupResult]
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.packet.game.objectcreate._
import net.psforever.types._
import services._
import services.{RemoverActor, _}
import services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse}
import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse}
import services.vehicle.VehicleAction.UnstowEquipment
import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
import scala.annotation.tailrec
import scala.concurrent.duration._
import scala.util.Success
class WorldSessionActor extends Actor with MDCContextAware {
@ -141,14 +142,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
else { //no items in inventory; leave no corpse
val player_guid = player.GUID
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)
}
case Some(vehicle_guid) =>
val player_guid = player.GUID
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)
DismountVehicleOnLogOut()
}
@ -184,7 +185,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Some(vehicle : Vehicle) =>
vehicle.Seat(vehicle.PassengerInSeat(player).get).get.Occupant = None
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}"))
@ -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) =>
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(
ObjectCreateMessage(
ammo_id,
@ -288,29 +289,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(GenericObjectActionMessage(guid, 36))
}
case AvatarResponse.EquipmentInHand(target, slot, item) =>
case msg @ AvatarResponse.DropItem(pkt) =>
if(tplayer_guid != guid) {
val definition = item.Definition
sendResponse(
ObjectCreateMessage(
definition.ObjectId,
item.GUID,
ObjectCreateMessageParent(target, slot),
definition.Packet.ConstructorData(item).get
)
)
sendResponse(pkt)
}
case msg @ AvatarResponse.EquipmentOnGround(pos, orient, item_id, item_guid, item_data) =>
case AvatarResponse.EquipmentInHand(pkt) =>
if(tplayer_guid != guid) {
log.info(s"now dropping a $msg")
sendResponse(
ObjectCreateMessage(
item_id,
item_guid,
DroppedItemData(PlacementData(pos, Vector3(0f, 0f, orient.z)), item_data)
)
)
sendResponse(pkt)
}
case AvatarResponse.LoadPlayer(pdata) =>
@ -406,7 +392,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
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 {
case LocalResponse.DoorOpens(door_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))
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
// 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))
@ -435,7 +421,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
case LocalResponse.ProximityTerminalEffect(object_guid, effectState) =>
if(player.GUID != guid) {
if(tplayer_guid != guid) {
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) =>
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) =>
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))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
DeploymentActivities(obj)
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
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))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
DeploymentActivities(obj)
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
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 player_guid : PlanetSideGUID = tplayer.GUID
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()
if(seat_num == 0) { //simplistic vehicle ownership management
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))
}
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, _) =>
@ -855,12 +839,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
})
//drop items on ground
val pos = tplayer.Position
val orient = tplayer.Orientation
val orient = Vector3(0,0, tplayer.Orientation.z)
((dropHolsters ++ dropInventory).map(_.obj) ++ drop).foreach(obj => {
continent.Ground ! Zone.DropItemOnGround(obj, pos, Vector3(0f, 0f, orient.z))
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))
//TODO make a sound when dropping stuff
continent.Ground ! Zone.Ground.DropItem(obj, pos, orient)
})
sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true))
}
@ -949,12 +931,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
})
//drop stuff on ground
val pos = tplayer.Position
val orient = tplayer.Orientation
val orient = Vector3(0,0, tplayer.Orientation.z)
((dropHolsters ++ dropInventory).map(_.obj)).foreach(obj => {
continent.Ground ! Zone.DropItemOnGround(obj, pos, Vector3(0f, 0f, orient.z))
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))
continent.Ground ! Zone.Ground.DropItem(obj, pos, orient)
})
case Terminal.VehicleLoadout(definition, weapons, inventory) =>
@ -1175,9 +1154,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
case VehicleSpawnPad.PlayerSeatedInVehicle(vehicle, pad) =>
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, 0, 10))//vehicle.Definition.MaxHealth))
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 68, 0L)) //???
@ -1188,7 +1164,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case VehicleSpawnPad.ServerVehicleOverrideStart(vehicle, pad) =>
val vdef = vehicle.Definition
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)
@ -1366,7 +1342,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
(taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(original)(continent.GUID), zone_id))
}
}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
respawnTimer = context.system.scheduler.scheduleOnce(respawnTime seconds, target, msg)
@ -1386,12 +1361,56 @@ class WorldSessionActor extends Actor with MDCContextAware {
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() =>
StopBundlingPackets()
LivePlayerList.Add(sessionId, avatar)
traveler = new Traveler(self, continent.Id)
//PropertyOverrideMessage
sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1))
sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list
sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil))
@ -1431,7 +1450,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
if(!corpse.isAlive && corpse.HasGUID) {
corpse.VehicleSeated match {
case Some(_) =>
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(corpse))
case None =>
@ -1444,6 +1462,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
val guid = tplayer.GUID
StartBundlingPackets()
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)))
if(spectator) {
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None))
@ -1478,31 +1497,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
//TacticsMessage
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) =>
progressBarUpdate.cancel
if(progressBarValue.isDefined) {
@ -1526,7 +1520,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
else { //continue next tick
tickAction.getOrElse(() => Unit)()
progressBarValue = Some(progressBarVal)
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
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)
//TODO end temp player character auto-loading
self ! ListAccountCharacters
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
clientKeepAlive.cancel
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) =>
log.info("Handling " + msg)
sendResponse(ActionResultMessage(true, None))
sendResponse(ActionResultMessage.Pass)
self ! ListAccountCharacters
case msg @ CharacterRequestMessage(charId, action) =>
log.info("Handling " + msg)
action match {
case CharacterRequestAction.Delete =>
sendResponse(ActionResultMessage(false, Some(1)))
sendResponse(ActionResultMessage.Fail(1))
case CharacterRequestAction.Select =>
//TODO check if can spawn on last continent/location from player?
//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))
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
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)))
}
@ -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 sumReloadValue : Int = originalBoxCapacity + tailReloadValue
val previousBox = tool.AmmoSlot.Box //current magazine in tool
sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3(0f, 0f, 0f), 0f, 0f, 0f))
sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3(0f, 0f, 0f), 0f, 0f, 0f))
sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3.Zero, 0f))
sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3.Zero, 0f))
obj.Inventory -= x.start //remove replacement ammo from inventory
val ammoSlotIndex = tool.FireMode.AmmoSlotIndex
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))
case msg @ DropItemMessage(item_guid) =>
log.info("DropItem: " + msg)
player.FreeHand.Equipment match {
case Some(item) =>
if(item.GUID == item_guid) {
val orient : Vector3 = Vector3(0f, 0f, player.Orientation.z)
player.FreeHand.Equipment = None
continent.Ground ! Zone.DropItemOnGround(item, player.Position, orient)
sendResponse(ObjectDetachMessage(player.GUID, item.GUID, player.Position, 0f, 0f, player.Orientation.z))
val objDef = item.Definition
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, player.Position, orient, objDef.ObjectId, item.GUID, objDef.Packet.ConstructorData(item).get))
}
else {
log.warn(s"item in hand was ${item.GUID} but trying to drop $item_guid; nothing will be dropped")
log.info(s"DropItem: $msg")
continent.GUID(item_guid) match {
case Some(item : Equipment) =>
player.FreeHand.Equipment match {
case Some(_) =>
if(item.GUID == item_guid) {
continent.Ground ! Zone.Ground.DropItem(item, player.Position, player.Orientation)
}
case None =>
log.warn(s"DropItem: $player wanted to drop a $item, but it wasn't at hand")
}
case Some(obj) => //TODO LLU
log.warn(s"DropItem: $player wanted to drop a $obj, but that isn't possible")
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) =>
log.info("PickupItem: " + msg)
continent.Ground ! Zone.GetItemOnGround(player, item_guid)
log.info(s"PickupItem: $msg")
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) =>
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))
|| (player.Faction == vehicle.Faction
&& ((vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Health == 0))) {
vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(object_guid)
vehicleService ! VehicleServiceMessage.RequestDeleteVehicle(vehicle, continent)
log.info(s"RequestDestroy: vehicle $object_guid")
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), continent))
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent, Some(0 seconds)))
log.info(s"RequestDestroy: vehicle $vehicle")
}
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) =>
@ -2331,20 +2331,28 @@ class WorldSessionActor extends Actor with MDCContextAware {
})
match {
case Some((parent, Some(slot))) =>
obj.Position = Vector3.Zero
taskResolver ! RemoveEquipmentFromSlot(parent, obj, slot)
log.info(s"RequestDestroy: equipment $object_guid")
log.info(s"RequestDestroy: equipment $obj")
case _ =>
//TODO search for item on ground
sendResponse(ObjectDeleteMessage(object_guid, 0))
log.warn(s"RequestDestroy: object $object_guid not found")
if(continent.EquipmentOnGround.contains(obj)) {
obj.Position = Vector3.Zero
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 =>
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) =>
@ -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: 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)) {
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 =>
@ -3928,14 +3936,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Some(InventoryItem(item2, destIndex)) => //yes, swap
//cleanly shuffle items around to avoid losing icons
//the next ObjectDetachMessage is necessary to avoid icons being lost, but only as part of this swap
sendResponse(ObjectDetachMessage(source_guid, item_guid, Vector3.Zero, 0f, 0f, 0f))
sendResponse(ObjectDetachMessage(source_guid, item_guid, Vector3.Zero, 0f))
val item2_guid = item2.GUID
destination.Slot(destIndex).Equipment = None //remove the swap item from destination
(indexSlot.Equipment = item2) match {
case Some(_) => //item and item2 swapped places successfully
log.info(s"MoveItem: $item2 swapped to $source @ $index")
//remove item2 from destination
sendResponse(ObjectDetachMessage(destination_guid, item2_guid, Vector3.Zero, 0f, 0f, 0f))
sendResponse(ObjectDetachMessage(destination_guid, item2_guid, Vector3.Zero, 0f))
destination match {
case obj : Vehicle =>
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 sourceOrientZ = source.Orientation.z
val orient : Vector3 = Vector3(0f, 0f, sourceOrientZ)
continent.Actor ! Zone.DropItemOnGround(item2, pos, orient)
sendResponse(ObjectDetachMessage(destination_guid, item2_guid, pos, 0f, 0f, sourceOrientZ)) //ground
continent.Ground ! Zone.Ground.DropItem(item2, pos, orient)
sendResponse(ObjectDetachMessage(destination_guid, item2_guid, pos, sourceOrientZ)) //ground
val objDef = item2.Definition
destination match {
case obj : Vehicle =>
@ -3987,7 +3995,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case _ => ;
//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 => ;
@ -4037,15 +4045,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
* @param item the item
*/
def NormalItemDrop(obj : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : Unit = {
val itemGUID = item.GUID
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))
continent.Ground ! Zone.Ground.DropItem(item, obj.Position, Vector3(0f, 0f, obj.Orientation.z))
}
/**
@ -4348,7 +4348,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
PlayerActionsToCancel()
CancelAllProximityUnits()
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
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 = {
if(WellLootedCorpse(obj)) {
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(1 second, avatarService, AvatarServiceMessage.RemoveSpecificCorpse(List(obj)))
avatarService ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), continent))
true
}
else {
@ -4567,7 +4564,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
def SetDelayedProximityUnitReset(terminal : Terminal with ProximityUnit) : Unit = {
val terminal_guid = terminal.GUID
ClearDelayedProximityUnitReset(terminal_guid)
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
delayedProximityTerminalResets += terminal_guid ->
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
package services.avatar
import net.psforever.objects.Player
import net.psforever.objects.{PlanetSideGameObject, Player}
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.Container
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
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 {
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_Stop(player_guid : PlanetSideGUID, weapon_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 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 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 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 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 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 PlayerStateShift(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 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.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.types.{ExoSuitType, Vector3}
import net.psforever.types.ExoSuitType
object AvatarResponse {
trait Response
@ -16,11 +16,9 @@ object AvatarResponse {
final case class ChangeFireState_Start(weapon_guid : PlanetSideGUID) extends Response
final case class ChangeFireState_Stop(weapon_guid : PlanetSideGUID) extends Response
final case class ConcealPlayer() extends Response
final case class EquipmentInHand(target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Response
final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item_id : Int, item_guid : PlanetSideGUID, item_data : ConstructorData) extends Response
final case class EquipmentInHand(pkt : ObjectCreateMessage) extends Response
final case class DropItem(pkt : ObjectCreateMessage) 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 ObjectHeld(slot : Int) 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 DestroyDisplay(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
import akka.actor.{Actor, ActorRef, Props}
import services.avatar.support.CorpseRemovalActor
import services.{GenericEventBus, Service}
import net.psforever.packet.game.ObjectCreateMessage
import net.psforever.packet.game.objectcreate.{DroppedItemData, ObjectCreateMessageParent, PlacementData}
import services.avatar.support.{CorpseRemovalActor, DroppedItemRemover}
import services.{GenericEventBus, RemoverActor, Service}
class AvatarService extends Actor {
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
@ -62,13 +65,26 @@ class AvatarService extends Actor {
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ConcealPlayer())
)
case AvatarAction.EquipmentInHand(player_guid, target_guid, slot, obj) =>
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.EquipmentInHand(target_guid, slot, obj))
case AvatarAction.DropItem(player_guid, item, zone) =>
val definition = item.Definition
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(
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) =>
AvatarEvents.publish(
@ -90,11 +106,24 @@ class AvatarService extends Actor {
AvatarEvents.publish(
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) =>
undertaker ! (time match {
case Some(t) => CorpseRemovalActor.AddCorpse(player, zone, t)
case None => CorpseRemovalActor.AddCorpse(player, zone)
})
undertaker forward RemoverActor.AddTask(player, zone, time)
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player.GUID, AvatarResponse.Release(player))
)
@ -115,52 +144,13 @@ class AvatarService extends Actor {
}
//message to Undertaker
case AvatarServiceMessage.RemoveSpecificCorpse(corpses) =>
undertaker ! AvatarServiceMessage.RemoveSpecificCorpse( corpses.filter(corpse => {corpse.HasGUID && corpse.isBackpack}) )
case AvatarServiceMessage.Corpse(msg) =>
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) =>
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
if (playerOpt.isDefined) {
@ -185,16 +175,8 @@ class AvatarService extends Actor {
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 =>
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
package services.avatar
import net.psforever.objects.Player
final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action)
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
package services.avatar.support
import akka.actor.{Actor, ActorRef, Cancellable}
import net.psforever.objects.guid.TaskResolver
import net.psforever.objects.{DefaultCancellable, Player}
import net.psforever.objects.zones.Zone
import net.psforever.types.Vector3
import services.{Service, ServiceManager}
import services.ServiceManager.Lookup
import net.psforever.objects.guid.{GUIDTask, TaskResolver}
import net.psforever.objects.Player
import services.{RemoverActor, Service}
import services.avatar.{AvatarAction, AvatarServiceMessage}
import scala.annotation.tailrec
import scala.concurrent.duration._
class CorpseRemovalActor extends Actor {
private var burial : Cancellable = DefaultCancellable.obj
private var corpses : List[CorpseRemovalActor.Entry] = List()
class CorpseRemovalActor extends RemoverActor {
final val FirstStandardDuration : FiniteDuration = 3 minutes
private var decomposition : Cancellable = DefaultCancellable.obj
private var buriedCorpses : List[CorpseRemovalActor.Entry] = List()
final val SecondStandardDuration : FiniteDuration = 500 milliseconds
private var taskResolver : ActorRef = Actor.noSender
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 InclusionTest(entry : RemoverActor.Entry) : Boolean = {
entry.obj.isInstanceOf[Player] && entry.obj.asInstanceOf[Player].isBackpack
}
def receive : Receive = {
case "startup" =>
ServiceManager.serviceManager ! Lookup("taskResolver") //ask for a resolver to deal with the GUID system
def InitialJob(entry : RemoverActor.Entry) : Unit = { }
case ServiceManager.LookupResult("taskResolver", endpoint) =>
//Cart Master: Bring out your dead!
taskResolver = endpoint
context.become(Processing)
case _ => ;
def FirstJob(entry : RemoverActor.Entry) : Unit = {
import net.psforever.objects.zones.Zone
entry.zone.Population ! Zone.Corpse.Remove(entry.obj.asInstanceOf[Player])
context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID))
}
def Processing : Receive = {
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")
}
def ClearanceTest(entry : RemoverActor.Entry) : Boolean = !entry.zone.Corpses.contains(entry.obj)
case AvatarServiceMessage.RemoveSpecificCorpse(targets) =>
if(targets.nonEmpty) {
//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)
}
}
def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = {
GUIDTask.UnregisterPlayer(entry.obj.asInstanceOf[Player])(entry.zone.GUID)
}
}

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

View file

@ -3,16 +3,14 @@ package services.vehicle
import net.psforever.objects.Vehicle
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.PlanetSideGUID
final case class VehicleServiceMessage(forChannel : String, actionMessage : VehicleAction.Action)
object VehicleServiceMessage {
final case class DelayedVehicleDeconstruction(vehicle : Vehicle, continent : Zone, timeAlive : Long)
final case class GiveActorControl(vehicle : Vehicle, actorName : String)
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)
}

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.guid.{GUIDTask, TaskResolver}
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 services.{Service, ServiceManager}
import services.{RemoverActor, Service, ServiceManager}
import services.avatar._
import scala.concurrent.duration._
@ -93,32 +94,59 @@ class ConcealPlayerTest 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 {
"pass EquipmentInHand" in {
ServiceManager.boot(system)
val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName)
service ! Service.Join("test")
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 tool = Tool(toolDef)
tool.AmmoSlots.head.Box.GUID = PlanetSideGUID(1)
val cdata = toolDef.Packet.ConstructorData(tool).get
tool.Position = Vector3(1,2,3)
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 {
"pass EquipmentOnGround" in {
ServiceManager.boot(system)
val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName)
"pass DropItem" in {
service ! Service.Join("test")
service ! AvatarServiceMessage("test", AvatarAction.EquipmentOnGround(PlanetSideGUID(10), Vector3(300f, 200f, 100f), Vector3(450f, 300f, 150f), toolDef.ObjectId, PlanetSideGUID(11), cdata))
expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentOnGround(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.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 {
"AvatarService" should {
"pass Reload" in {
@ -320,14 +387,14 @@ class AvatarReleaseTest extends ActorTest {
taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID)
assert(zone.Corpses.isEmpty)
zone.Population ! Zone.Corpse.Add(obj)
expectNoMsg(100 milliseconds) //spacer
expectNoMsg(200 milliseconds) //spacer
assert(zone.Corpses.size == 1)
assert(obj.HasGUID)
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])
val reply1msg = reply1.asInstanceOf[AvatarServiceResponse]
assert(reply1msg.toChannel == "/test/Avatar")
@ -343,7 +410,7 @@ class AvatarReleaseTest extends ActorTest {
assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete])
assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid)
expectNoMsg(1000 milliseconds)
expectNoMsg(1 seconds)
assert(zone.Corpses.isEmpty)
assert(!obj.HasGUID)
}
@ -369,14 +436,14 @@ class AvatarReleaseEarly1Test extends ActorTest {
taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID)
assert(zone.Corpses.isEmpty)
zone.Population ! Zone.Corpse.Add(obj)
expectNoMsg(100 milliseconds) //spacer
expectNoMsg(200 milliseconds) //spacer
assert(zone.Corpses.size == 1)
assert(obj.HasGUID)
val guid = obj.GUID
service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone)) //3+ minutes!
val reply1 = receiveOne(100 milliseconds)
val reply1 = receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[AvatarServiceResponse])
val reply1msg = reply1.asInstanceOf[AvatarServiceResponse]
assert(reply1msg.toChannel == "/test/Avatar")
@ -384,8 +451,8 @@ class AvatarReleaseEarly1Test extends ActorTest {
assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release])
assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj)
service ! AvatarServiceMessage.RemoveSpecificCorpse(List(obj)) //IMPORTANT: ONE ENTRY
val reply2 = receiveOne(100 milliseconds)
service ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), zone)) //IMPORTANT: ONE ENTRY
val reply2 = receiveOne(200 milliseconds)
assert(reply2.isInstanceOf[AvatarServiceResponse])
val reply2msg = reply2.asInstanceOf[AvatarServiceResponse]
assert(reply2msg.toChannel.equals("/test/Avatar"))
@ -393,7 +460,7 @@ class AvatarReleaseEarly1Test extends ActorTest {
assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete])
assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid)
expectNoMsg(600 milliseconds)
expectNoMsg(1 seconds)
assert(zone.Corpses.isEmpty)
assert(!obj.HasGUID)
}
@ -420,14 +487,14 @@ class AvatarReleaseEarly2Test extends ActorTest {
taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID)
assert(zone.Corpses.isEmpty)
zone.Population ! Zone.Corpse.Add(obj)
expectNoMsg(100 milliseconds) //spacer
expectNoMsg(200 milliseconds) //spacer
assert(zone.Corpses.size == 1)
assert(obj.HasGUID)
val guid = obj.GUID
service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone)) //3+ minutes!
val reply1 = receiveOne(100 milliseconds)
val reply1 = receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[AvatarServiceResponse])
val reply1msg = reply1.asInstanceOf[AvatarServiceResponse]
assert(reply1msg.toChannel == "/test/Avatar")
@ -435,7 +502,7 @@ class AvatarReleaseEarly2Test extends ActorTest {
assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release])
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)
assert(reply2.isInstanceOf[AvatarServiceResponse])
val reply2msg = reply2.asInstanceOf[AvatarServiceResponse]
@ -444,7 +511,7 @@ class AvatarReleaseEarly2Test extends ActorTest {
assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete])
assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid)
expectNoMsg(600 milliseconds)
expectNoMsg(1 seconds)
assert(zone.Corpses.isEmpty)
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)
}
})
}
}
}