From f6f7ad561715cac2790bddadaacf2b802ce5a3f8 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 22 May 2018 19:13:59 -0400 Subject: [PATCH 1/9] unified code paths for dropping an item, and reinforced code path for picking an item back up; LocalService will handle some of the work now; ActionResultMessage embraces its simplistic nature (we don't have enough error messages) --- .../net/psforever/objects/zones/Zone.scala | 28 +-- .../psforever/objects/zones/ZoneActor.scala | 4 +- .../objects/zones/ZoneGroundActor.scala | 26 +-- .../packet/game/ActionResultMessage.scala | 17 +- .../packet/game/ObjectDetachMessage.scala | 8 + .../scala/game/ActionResultMessageTest.scala | 4 +- .../scala/game/ObjectDetachMessageTest.scala | 16 +- common/src/test/scala/objects/ZoneTest.scala | 15 +- .../src/main/scala/WorldSessionActor.scala | 184 ++++++++++-------- .../scala/services/local/LocalAction.scala | 2 + .../scala/services/local/LocalResponse.scala | 2 + .../scala/services/local/LocalService.scala | 12 ++ 12 files changed, 192 insertions(+), 126 deletions(-) diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala index 1c25abaa..4ac4d109 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -386,27 +386,15 @@ 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(item : Equipment) - /** - * 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) - - /** - * 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 PickupItem(item_guid : PlanetSideGUID) + final case class ItemInHand(item : Equipment) + final case class CanNotPickupItem(item_guid : PlanetSideGUID) + } object Vehicle { final case class Spawn(vehicle : Vehicle) diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala index 4d6f0c4f..80f6a547 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala @@ -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 diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala index 5a001e47..b44be2c3 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala @@ -16,18 +16,22 @@ class ZoneGroundActor(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 Zone.Ground.DropItem(item, pos, orient) => + sender ! (FindItemOnGround(item.GUID) match { case None => - org.log4s.getLogger.warn(s"item on ground $item_guid was requested by $player for pickup but was not found") - } + equipmentOnGround += item + Zone.Ground.ItemOnGround(item, pos, orient) + case Some(_) => + Zone.Ground.CanNotDropItem(item) + }) + + case Zone.Ground.PickupItem(item_guid) => + sender ! (FindItemOnGround(item_guid) match { + case Some(item) => + Zone.Ground.ItemInHand(item) + case None => + Zone.Ground.CanNotPickupItem(item_guid) + }) case _ => ; } diff --git a/common/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala b/common/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala index fe7f0b44..b8b7d9c5 100644 --- a/common/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala @@ -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 => diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala index a68fa1fb..5ab2d7b2 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala @@ -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) :: diff --git a/common/src/test/scala/game/ActionResultMessageTest.scala b/common/src/test/scala/game/ActionResultMessageTest.scala index a6bce43c..29c9d950 100644 --- a/common/src/test/scala/game/ActionResultMessageTest.scala +++ b/common/src/test/scala/game/ActionResultMessageTest.scala @@ -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 diff --git a/common/src/test/scala/game/ObjectDetachMessageTest.scala b/common/src/test/scala/game/ObjectDetachMessageTest.scala index f7ee8b52..a5266513 100644 --- a/common/src/test/scala/game/ObjectDetachMessageTest.scala +++ b/common/src/test/scala/game/ObjectDetachMessageTest.scala @@ -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 + } } diff --git a/common/src/test/scala/objects/ZoneTest.scala b/common/src/test/scala/objects/ZoneTest.scala index 6137b993..84868f23 100644 --- a/common/src/test/scala/objects/ZoneTest.scala +++ b/common/src/test/scala/objects/ZoneTest.scala @@ -477,7 +477,7 @@ class ZoneGroundTest extends ActorTest { 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)) + zone.Ground ! Zone.Ground.DropItem(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)) @@ -490,17 +490,16 @@ class ZoneGroundTest extends ActorTest { 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) + zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) expectNoMsg(Duration.create(100, "ms")) assert(zone.EquipmentOnGround == List(item)) - zone.Ground ! Zone.GetItemOnGround(player, PlanetSideGUID(10)) + zone.Ground ! Zone.Ground.PickupItem(PlanetSideGUID(10)) val reply = receiveOne(Duration.create(100, "ms")) assert(zone.EquipmentOnGround.isEmpty) - assert(reply.isInstanceOf[Zone.ItemFromGround]) - assert(reply.asInstanceOf[Zone.ItemFromGround].player == player) - assert(reply.asInstanceOf[Zone.ItemFromGround].item == item) + assert(reply.isInstanceOf[Zone.Ground.ItemInHand]) + assert(reply.asInstanceOf[Zone.Ground.ItemInHand].item == item) } "get item from ground (failure)" in { @@ -508,11 +507,11 @@ class ZoneGroundTest extends ActorTest { 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) + zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) expectNoMsg(Duration.create(100, "ms")) assert(zone.EquipmentOnGround == List(item)) - zone.Ground ! Zone.GetItemOnGround(player, PlanetSideGUID(11)) //wrong guid + zone.Ground ! Zone.Ground.PickupItem(PlanetSideGUID(11)) //wrong guid expectNoMsg(Duration.create(500, "ms")) } } diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 8ec79f16..4ad2820d 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -255,7 +255,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, @@ -405,7 +405,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) { @@ -415,16 +415,21 @@ class WorldSessionActor extends Actor with MDCContextAware { case LocalResponse.DoorCloses(door_guid) => //door closes for everyone sendResponse(GenericObjectStateMsg(door_guid, 17)) + case LocalResponse.DropItem(id, item_guid, data) => + if(tplayer_guid != guid) { + sendResponse(ObjectCreateMessage(id, item_guid, data)) + } + case LocalResponse.HackClear(target_guid, unk1, unk2) => sendResponse(HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2)) case LocalResponse.HackObject(target_guid, unk1, unk2) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)) } case LocalResponse.ProximityTerminalEffect(object_guid, effectState) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, effectState)) } @@ -465,7 +470,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) { @@ -844,12 +849,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)) } @@ -938,12 +941,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) => @@ -1177,7 +1177,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) @@ -1375,6 +1375,62 @@ 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) //only one kind of rotation is important + 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 + } + localService ! LocalServiceMessage(continent.Id, LocalAction.DropItem(exclusionId, item)) + + case Zone.Ground.CanNotDropItem(item) => + log.warn(s"DropItem: $player tried to drop a $item on the ground, but he missed") + player.Find(item) match { + case None => //item in limbo + taskResolver ! GUIDTask.UnregisterEquipment(item)(continent.GUID) + case Some(_) => ; + } + + 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, if(player.VisibleSlots.contains(slotNum)) { + AvatarAction.EquipmentInHand(player_guid, player_guid, slotNum, item) + } + else { + AvatarAction.ObjectDelete(player_guid, item_guid) + }) + sendResponse(ActionResultMessage.Pass) + case None => + continent.Ground ! Zone.Ground.DropItem(item, item.Position, item.Orientation) //restore previous state + } + + case Zone.Ground.CanNotPickupItem(item_guid) => + continent.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) @@ -1467,31 +1523,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) { @@ -1607,18 +1638,17 @@ class WorldSessionActor extends Actor with MDCContextAware { 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 @@ -1828,7 +1858,7 @@ class WorldSessionActor extends Actor with MDCContextAware { 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))) } @@ -1989,8 +2019,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 @@ -2158,27 +2188,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) @@ -3847,14 +3887,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)) @@ -3897,8 +3937,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 => @@ -3956,15 +3996,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)) } /** diff --git a/pslogin/src/main/scala/services/local/LocalAction.scala b/pslogin/src/main/scala/services/local/LocalAction.scala index 1c04f2e7..2674e517 100644 --- a/pslogin/src/main/scala/services/local/LocalAction.scala +++ b/pslogin/src/main/scala/services/local/LocalAction.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever package services.local +import net.psforever.objects.equipment.Equipment import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.zones.Zone @@ -12,6 +13,7 @@ object LocalAction { final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action + final case class DropItem(player_guid : PlanetSideGUID, item : Equipment) extends Action final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action final case class ProximityTerminalEffect(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, effectState : Boolean) extends Action diff --git a/pslogin/src/main/scala/services/local/LocalResponse.scala b/pslogin/src/main/scala/services/local/LocalResponse.scala index fdc2aa37..4313db72 100644 --- a/pslogin/src/main/scala/services/local/LocalResponse.scala +++ b/pslogin/src/main/scala/services/local/LocalResponse.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever package services.local +import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound} import net.psforever.types.Vector3 @@ -9,6 +10,7 @@ object LocalResponse { final case class DoorOpens(door_guid : PlanetSideGUID) extends Response final case class DoorCloses(door_guid : PlanetSideGUID) extends Response + final case class DropItem(item_id : Int, item_guid : PlanetSideGUID, item_data : ConstructorData) extends Response final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response final case class ProximityTerminalEffect(object_guid : PlanetSideGUID, effectState : Boolean) extends Response diff --git a/pslogin/src/main/scala/services/local/LocalService.scala b/pslogin/src/main/scala/services/local/LocalService.scala index 5c01ee1a..01df3c74 100644 --- a/pslogin/src/main/scala/services/local/LocalService.scala +++ b/pslogin/src/main/scala/services/local/LocalService.scala @@ -2,6 +2,7 @@ package services.local import akka.actor.{Actor, Props} +import net.psforever.packet.game.objectcreate.{DroppedItemData, PlacementData} import services.local.support.{DoorCloseActor, HackClearActor} import services.{GenericEventBus, Service} @@ -46,6 +47,17 @@ class LocalService extends Actor { LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.DoorCloses(door_guid)) ) + case LocalAction.DropItem(player_guid, item) => + val definition = item.Definition + val objectData = DroppedItemData( + PlacementData(item.Position, item.Orientation), + definition.Packet.ConstructorData(item).get + ) + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", player_guid, + LocalResponse.DropItem(definition.ObjectId, item.GUID, objectData) + ) + ) case LocalAction.HackClear(player_guid, target, unk1, unk2) => LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.HackClear(target.GUID, unk1, unk2)) From d35536da062bf41f87006674a7c473c61ad54e5f Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 23 May 2018 23:53:50 -0400 Subject: [PATCH 2/9] created a generic base model for automated object deletion that isn't user driven; the first instance is the DroppedItemRemover for LocalService --- .../src/main/scala/WorldSessionActor.scala | 20 +- .../main/scala/services/RemoverActor.scala | 284 ++++++++++++++++++ .../scala/services/local/LocalAction.scala | 1 + .../scala/services/local/LocalResponse.scala | 1 + .../scala/services/local/LocalService.scala | 15 +- .../local/support/DroppedItemRemover.scala | 33 ++ 6 files changed, 341 insertions(+), 13 deletions(-) create mode 100644 pslogin/src/main/scala/services/RemoverActor.scala create mode 100644 pslogin/src/main/scala/services/local/support/DroppedItemRemover.scala diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 4ad2820d..e30f07cc 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -37,13 +37,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 { @@ -428,6 +429,11 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)) } + case LocalResponse.ObjectDelete(item_guid, unk) => + if(tplayer_guid != guid) { + sendResponse(ObjectDeleteMessage(item_guid, unk)) + } + case LocalResponse.ProximityTerminalEffect(object_guid, effectState) => if(tplayer_guid != guid) { sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, effectState)) @@ -590,7 +596,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)) } @@ -612,7 +617,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)) } @@ -1355,7 +1359,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) @@ -1387,6 +1390,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case None => PlanetSideGUID(0) //object is being introduced into the world upon drop } + localService ! RemoverActor.AddTask(item, continent, Some(20 seconds)) localService ! LocalServiceMessage(continent.Id, LocalAction.DropItem(exclusionId, item)) case Zone.Ground.CanNotDropItem(item) => @@ -1400,6 +1404,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case Zone.Ground.ItemInHand(item) => player.Fit(item) match { case Some(slotNum) => + localService ! RemoverActor.ClearSpecific(List(item), continent) val item_guid = item.GUID val player_guid = player.GUID player.Slot(slotNum).Equipment = item @@ -1476,7 +1481,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 => @@ -1546,7 +1550,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)) } @@ -1634,7 +1637,6 @@ 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()) @@ -1855,7 +1857,6 @@ 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)) @@ -4299,7 +4300,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)) } @@ -4440,7 +4440,6 @@ 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))) true @@ -4518,7 +4517,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)) diff --git a/pslogin/src/main/scala/services/RemoverActor.scala b/pslogin/src/main/scala/services/RemoverActor.scala new file mode 100644 index 00000000..656d49f1 --- /dev/null +++ b/pslogin/src/main/scala/services/RemoverActor.scala @@ -0,0 +1,284 @@ +// 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._ + +abstract class RemoverActor extends Actor { + protected var firstTask : Cancellable = DefaultCancellable.obj + protected var firstHeap : List[RemoverActor.Entry] = List() + + protected var secondTask : Cancellable = DefaultCancellable.obj + protected var secondHeap : List[RemoverActor.Entry] = List() + + protected var taskResolver : ActorRef = Actor.noSender + + protected[this] val log = org.log4s.getLogger + + override def preStart() : Unit = { + super.preStart() + self ! RemoverActor.Startup() + } + + override def postStop() = { + super.postStop() + firstTask.cancel + secondTask.cancel + + firstHeap.foreach(entry => { + FirstJob(entry) + SecondJob(entry) + }) + secondHeap.foreach { SecondJob } + } + + 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) + 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) + if(oldHead != firstHeap.head) { + RetimeFirstTask() + } + } + else { + log.trace(s"$obj is already queued for removal") + } + } + } + else { + log.trace(s"$obj either does not qualify for this Remover or is already queued") + } + + case RemoverActor.HurrySpecific(targets, zone) => + CullTargetsFromFirstHeap(targets, zone) match { + case Nil => ; + case list => + secondTask.cancel + list.foreach { FirstJob } + secondHeap = list ++ secondHeap + import scala.concurrent.ExecutionContext.Implicits.global + secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) + } + + case RemoverActor.HurryAll() => + firstTask.cancel + firstHeap.foreach { FirstJob } + secondHeap = secondHeap ++ firstHeap + firstHeap = Nil + secondTask.cancel + import scala.concurrent.ExecutionContext.Implicits.global + secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) + + case RemoverActor.ClearSpecific(targets, zone) => + CullTargetsFromFirstHeap(targets, zone) + + case RemoverActor.ClearAll() => + firstTask.cancel + firstHeap = Nil + + //private messages + 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 + in.foreach { FirstJob } + RetimeFirstTask() + if(secondHeap.nonEmpty) { + import scala.concurrent.ExecutionContext.Implicits.global + secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) + } + log.trace(s"item removal task has found ${secondHeap.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()) + } + log.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 unregistered - $ex") + } + + private def CullTargetsFromFirstHeap(targets : List[PlanetSideGameObject], zone : Zone) : List[RemoverActor.Entry] = { + if(targets.nonEmpty) { + firstTask.cancel + val culledEntries = if(targets.size == 1) { + log.debug(s"a target submitted for early cleanup: ${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 { + log.trace(s"multiple targets submitted for early cleanup: $targets") + //cumbersome partition + //a - find targets from entries + val locatedTargets = for { + a <- targets.map(RemoverActor.Entry(_, zone, 0)) + b <- firstHeap + 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 + } + } + RetimeFirstTask() + culledEntries + } + else { + Nil + } + } + + 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)) + ) + } + + def FirstStandardDuration : FiniteDuration + + def SecondStandardDuration : FiniteDuration + + def InclusionTest(entry : RemoverActor.Entry) : Boolean + + def InitialJob(entry : RemoverActor.Entry) : Unit + + def FirstJob(entry : RemoverActor.Entry) : Unit + + def ClearanceTest(entry : RemoverActor.Entry) : Boolean + + def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask +} + +object RemoverActor { + /** + * na + * @param obj the target + * @param zone the zone in which this target is registered + * @param duration how much longer the target will exist (in nanoseconds) + * @param time when this entry was created (in nanoseconds) + */ + case class Entry(obj : PlanetSideGameObject, zone : Zone, duration : Long, time : Long = System.nanoTime) + + case class Startup() + + case class AddTask(obj : PlanetSideGameObject, zone : Zone, duration : Option[FiniteDuration] = None) + + case class HurrySpecific(targets : List[PlanetSideGameObject], zone : Zone) + + case class HurryAll() + + case class ClearSpecific(targets : List[PlanetSideGameObject], zone : Zone) + + case class ClearAll() + + protected final case class FailureToWork(entry : RemoverActor.Entry, ex : Throwable) + + private final case class StartDelete() + + private final case class TryDelete() + + private def Similarity(entry1 : RemoverActor.Entry, entry2 : RemoverActor.Entry) : Boolean = { + entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID + } + + @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) + } + } + } +} diff --git a/pslogin/src/main/scala/services/local/LocalAction.scala b/pslogin/src/main/scala/services/local/LocalAction.scala index 2674e517..800f86f0 100644 --- a/pslogin/src/main/scala/services/local/LocalAction.scala +++ b/pslogin/src/main/scala/services/local/LocalAction.scala @@ -16,6 +16,7 @@ object LocalAction { final case class DropItem(player_guid : PlanetSideGUID, item : Equipment) extends Action final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action + final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action final case class ProximityTerminalEffect(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, effectState : Boolean) extends Action final case class TriggerSound(player_guid : PlanetSideGUID, sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Action } diff --git a/pslogin/src/main/scala/services/local/LocalResponse.scala b/pslogin/src/main/scala/services/local/LocalResponse.scala index 4313db72..b6b9329b 100644 --- a/pslogin/src/main/scala/services/local/LocalResponse.scala +++ b/pslogin/src/main/scala/services/local/LocalResponse.scala @@ -13,6 +13,7 @@ object LocalResponse { final case class DropItem(item_id : Int, item_guid : PlanetSideGUID, item_data : ConstructorData) extends Response final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response + final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response final case class ProximityTerminalEffect(object_guid : PlanetSideGUID, effectState : Boolean) extends Response final case class TriggerSound(sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Response } diff --git a/pslogin/src/main/scala/services/local/LocalService.scala b/pslogin/src/main/scala/services/local/LocalService.scala index 01df3c74..2d20f1e7 100644 --- a/pslogin/src/main/scala/services/local/LocalService.scala +++ b/pslogin/src/main/scala/services/local/LocalService.scala @@ -3,12 +3,13 @@ package services.local import akka.actor.{Actor, Props} import net.psforever.packet.game.objectcreate.{DroppedItemData, PlacementData} -import services.local.support.{DoorCloseActor, HackClearActor} -import services.{GenericEventBus, Service} +import services.local.support.{DoorCloseActor, DroppedItemRemover, HackClearActor} +import services.{GenericEventBus, RemoverActor, Service} class LocalService extends Actor { private val doorCloser = context.actorOf(Props[DoorCloseActor], "local-door-closer") private val hackClearer = context.actorOf(Props[HackClearActor], "local-hack-clearer") + private val janitor = context.actorOf(Props[DroppedItemRemover], "local-item-remover") private [this] val log = org.log4s.getLogger override def preStart = { @@ -58,6 +59,10 @@ class LocalService extends Actor { LocalResponse.DropItem(definition.ObjectId, item.GUID, objectData) ) ) + case LocalAction.ObjectDelete(player_guid, item_guid, unk) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.ObjectDelete(item_guid, unk)) + ) case LocalAction.HackClear(player_guid, target, unk1, unk2) => LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.HackClear(target.GUID, unk1, unk2)) @@ -78,6 +83,12 @@ class LocalService extends Actor { case _ => ; } + //messages to DroppedItemRemover + case msg @ (RemoverActor.AddTask | + RemoverActor.HurrySpecific | RemoverActor.HurryAll | + RemoverActor.ClearSpecific | RemoverActor.ClearAll) => + janitor ! msg + //response from DoorCloseActor case DoorCloseActor.CloseTheDoor(door_guid, zone_id) => LocalEvents.publish( diff --git a/pslogin/src/main/scala/services/local/support/DroppedItemRemover.scala b/pslogin/src/main/scala/services/local/support/DroppedItemRemover.scala new file mode 100644 index 00000000..c9c24a39 --- /dev/null +++ b/pslogin/src/main/scala/services/local/support/DroppedItemRemover.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2017 PSForever +package services.local.support + +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.guid.{GUIDTask, TaskResolver} +import services.{RemoverActor, Service} +import services.local.{LocalAction, LocalServiceMessage} + +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 ! LocalServiceMessage(entry.zone.Id, LocalAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID)) + } + + def ClearanceTest(entry : RemoverActor.Entry) : Boolean = true + + def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = { + GUIDTask.UnregisterEquipment(entry.obj.asInstanceOf[Equipment])(entry.zone.GUID) + } +} From 36b9d81e6c7917089e01376de10a9236b5ce68ce Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 25 May 2018 21:11:25 -0400 Subject: [PATCH 3/9] moved current object dropping functionality over to AvatarService entirely; adjusting special support actor messaging for AvatarService; modified calls for DroppedItemRemover and CorpseRemoverActor; Player now has a more sensible check for its VisibleSlots --- .../scala/net/psforever/objects/Player.scala | 7 +- .../src/main/scala/WorldSessionActor.scala | 54 +--- .../main/scala/services/RemoverActor.scala | 288 +++++++++++++++--- .../scala/services/avatar/AvatarAction.scala | 15 +- .../services/avatar/AvatarResponse.scala | 11 +- .../scala/services/avatar/AvatarService.scala | 112 +++---- .../avatar/AvatarServiceMessage.scala | 5 +- .../avatar/support/CorpseRemovalActor.scala | 244 ++------------- .../support/DroppedItemRemover.scala | 6 +- .../scala/services/local/LocalAction.scala | 3 - .../scala/services/local/LocalResponse.scala | 3 - .../scala/services/local/LocalService.scala | 27 +- .../src/test/scala/AvatarServiceTest.scala | 121 ++++++-- 13 files changed, 440 insertions(+), 456 deletions(-) rename pslogin/src/main/scala/services/{local => avatar}/support/DroppedItemRemover.scala (81%) diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index 19cf484b..4300ac40 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -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) { diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index e30f07cc..fb16f896 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -141,14 +141,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() } @@ -288,29 +288,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) => @@ -416,11 +401,6 @@ class WorldSessionActor extends Actor with MDCContextAware { case LocalResponse.DoorCloses(door_guid) => //door closes for everyone sendResponse(GenericObjectStateMsg(door_guid, 17)) - case LocalResponse.DropItem(id, item_guid, data) => - if(tplayer_guid != guid) { - sendResponse(ObjectCreateMessage(id, item_guid, data)) - } - case LocalResponse.HackClear(target_guid, unk1, unk2) => sendResponse(HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2)) @@ -429,11 +409,6 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)) } - case LocalResponse.ObjectDelete(item_guid, unk) => - if(tplayer_guid != guid) { - sendResponse(ObjectDeleteMessage(item_guid, unk)) - } - case LocalResponse.ProximityTerminalEffect(object_guid, effectState) => if(tplayer_guid != guid) { sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, effectState)) @@ -1390,8 +1365,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case None => PlanetSideGUID(0) //object is being introduced into the world upon drop } - localService ! RemoverActor.AddTask(item, continent, Some(20 seconds)) - localService ! LocalServiceMessage(continent.Id, LocalAction.DropItem(exclusionId, item)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(exclusionId, item, continent)) case Zone.Ground.CanNotDropItem(item) => log.warn(s"DropItem: $player tried to drop a $item on the ground, but he missed") @@ -1404,7 +1378,6 @@ class WorldSessionActor extends Actor with MDCContextAware { case Zone.Ground.ItemInHand(item) => player.Fit(item) match { case Some(slotNum) => - localService ! RemoverActor.ClearSpecific(List(item), continent) val item_guid = item.GUID val player_guid = player.GUID player.Slot(slotNum).Equipment = item @@ -1417,13 +1390,7 @@ class WorldSessionActor extends Actor with MDCContextAware { definition.Packet.DetailedConstructorData(item).get ) ) - avatarService ! AvatarServiceMessage(continent.Id, if(player.VisibleSlots.contains(slotNum)) { - AvatarAction.EquipmentInHand(player_guid, player_guid, slotNum, item) - } - else { - AvatarAction.ObjectDelete(player_guid, item_guid) - }) - sendResponse(ActionResultMessage.Pass) + 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 } @@ -3947,7 +3914,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 => ; @@ -4440,8 +4407,7 @@ class WorldSessionActor extends Actor with MDCContextAware { */ def TryDisposeOfLootedCorpse(obj : Player) : Boolean = { if(WellLootedCorpse(obj)) { - 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 { diff --git a/pslogin/src/main/scala/services/RemoverActor.scala b/pslogin/src/main/scala/services/RemoverActor.scala index 656d49f1..71af341d 100644 --- a/pslogin/src/main/scala/services/RemoverActor.scala +++ b/pslogin/src/main/scala/services/RemoverActor.scala @@ -10,32 +10,73 @@ 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.
+ *
+ * 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.
+ *
+ * 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 { - protected var firstTask : Cancellable = DefaultCancellable.obj - protected var firstHeap : List[RemoverActor.Entry] = List() + /** + * 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() - protected var secondTask : Cancellable = DefaultCancellable.obj - protected var secondHeap : 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() - protected var taskResolver : ActorRef = Actor.noSender + private var taskResolver : ActorRef = Actor.noSender - protected[this] val log = org.log4s.getLogger + private[this] val log = org.log4s.getLogger + /** + * 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 = { @@ -79,47 +120,32 @@ abstract class RemoverActor extends Actor { } case RemoverActor.HurrySpecific(targets, zone) => - CullTargetsFromFirstHeap(targets, zone) match { - case Nil => ; - case list => - secondTask.cancel - list.foreach { FirstJob } - secondHeap = list ++ secondHeap - import scala.concurrent.ExecutionContext.Implicits.global - secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) - } + HurrySpecific(targets, zone) case RemoverActor.HurryAll() => - firstTask.cancel - firstHeap.foreach { FirstJob } - secondHeap = secondHeap ++ firstHeap - firstHeap = Nil - secondTask.cancel - import scala.concurrent.ExecutionContext.Implicits.global - secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) + HurryAll() case RemoverActor.ClearSpecific(targets, zone) => - CullTargetsFromFirstHeap(targets, zone) + ClearSpecific(targets, zone) case RemoverActor.ClearAll() => - firstTask.cancel - firstHeap = Nil + ClearAll() - //private messages + //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 + 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()) } - log.trace(s"item removal task has found ${secondHeap.size} items to remove") + log.trace(s"item removal task has found ${in.size} items to remove") case RemoverActor.TryDelete() => secondTask.cancel @@ -134,13 +160,79 @@ abstract class RemoverActor extends Actor { case RemoverActor.FailureToWork(entry, ex) => log.error(s"${entry.obj} from ${entry.zone} not properly unregistered - $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 => ; + case 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 = { + 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) + } + + /** + * 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] = { - if(targets.nonEmpty) { - firstTask.cancel - val culledEntries = if(targets.size == 1) { - log.debug(s"a target submitted for early cleanup: ${targets.head}") + val culledEntries = if(targets.nonEmpty) { + if(targets.size == 1) { + log.debug(s"a target submitted: ${targets.head}") //simple selection RemoverActor.recursiveFind(firstHeap.iterator, RemoverActor.Entry(targets.head, zone, 0)) match { case None => ; @@ -152,12 +244,12 @@ abstract class RemoverActor extends Actor { } } else { - log.trace(s"multiple targets submitted for early cleanup: $targets") + log.trace(s"multiple targets submitted: $targets") //cumbersome partition //a - find targets from entries val locatedTargets = for { a <- targets.map(RemoverActor.Entry(_, zone, 0)) - b <- firstHeap + b <- firstHeap//.filter(entry => entry.zone == zone) if b.obj.HasGUID && a.obj.HasGUID && RemoverActor.Similarity(b, a) } yield b if(locatedTargets.nonEmpty) { @@ -173,6 +265,15 @@ abstract class RemoverActor extends Actor { Nil } } + } + else { + log.trace(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 } @@ -181,6 +282,12 @@ abstract class RemoverActor extends Actor { } } + /** + * 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) { @@ -220,53 +327,152 @@ abstract class RemoverActor extends Actor { ) } + /** + * 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 { /** - * na + * 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 nanoseconds) - * @param time when this entry was created (in nanoseconds) + * @param duration how much longer the target will exist in its current state (in nanoseconds) */ - case class Entry(obj : PlanetSideGameObject, zone : Zone, duration : Long, time : Long = System.nanoTime) + 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 diff --git a/pslogin/src/main/scala/services/avatar/AvatarAction.scala b/pslogin/src/main/scala/services/avatar/AvatarAction.scala index d0701767..a929cb81 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarAction.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarAction.scala @@ -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 } diff --git a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala b/pslogin/src/main/scala/services/avatar/AvatarResponse.scala index 5932bace..3ab8e264 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarResponse.scala @@ -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 } diff --git a/pslogin/src/main/scala/services/avatar/AvatarService.scala b/pslogin/src/main/scala/services/avatar/AvatarService.scala index 17cec7ae..8c1f39db 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarService.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarService.scala @@ -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") } } diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala index 04b96a90..a600c0c3 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala @@ -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) } diff --git a/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala b/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala index 1486fb5b..68237ab4 100644 --- a/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala +++ b/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala @@ -1,245 +1,35 @@ // 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") - } - - 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 ClearanceTest(entry : RemoverActor.Entry) : Boolean = { + !entry.zone.Corpses.contains(entry.obj.asInstanceOf[Player]) } - 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) } } diff --git a/pslogin/src/main/scala/services/local/support/DroppedItemRemover.scala b/pslogin/src/main/scala/services/avatar/support/DroppedItemRemover.scala similarity index 81% rename from pslogin/src/main/scala/services/local/support/DroppedItemRemover.scala rename to pslogin/src/main/scala/services/avatar/support/DroppedItemRemover.scala index c9c24a39..ed0fdea7 100644 --- a/pslogin/src/main/scala/services/local/support/DroppedItemRemover.scala +++ b/pslogin/src/main/scala/services/avatar/support/DroppedItemRemover.scala @@ -1,10 +1,10 @@ // Copyright (c) 2017 PSForever -package services.local.support +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 services.local.{LocalAction, LocalServiceMessage} import scala.concurrent.duration._ @@ -22,7 +22,7 @@ class DroppedItemRemover extends RemoverActor { def FirstJob(entry : RemoverActor.Entry) : Unit = { import net.psforever.objects.zones.Zone entry.zone.Ground ! Zone.Ground.PickupItem(entry.obj.GUID) - context.parent ! LocalServiceMessage(entry.zone.Id, LocalAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID)) + context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID)) } def ClearanceTest(entry : RemoverActor.Entry) : Boolean = true diff --git a/pslogin/src/main/scala/services/local/LocalAction.scala b/pslogin/src/main/scala/services/local/LocalAction.scala index 800f86f0..1c04f2e7 100644 --- a/pslogin/src/main/scala/services/local/LocalAction.scala +++ b/pslogin/src/main/scala/services/local/LocalAction.scala @@ -1,7 +1,6 @@ // Copyright (c) 2017 PSForever package services.local -import net.psforever.objects.equipment.Equipment import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.zones.Zone @@ -13,10 +12,8 @@ object LocalAction { final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action - final case class DropItem(player_guid : PlanetSideGUID, item : Equipment) extends Action final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action - final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action final case class ProximityTerminalEffect(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, effectState : Boolean) extends Action final case class TriggerSound(player_guid : PlanetSideGUID, sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Action } diff --git a/pslogin/src/main/scala/services/local/LocalResponse.scala b/pslogin/src/main/scala/services/local/LocalResponse.scala index b6b9329b..fdc2aa37 100644 --- a/pslogin/src/main/scala/services/local/LocalResponse.scala +++ b/pslogin/src/main/scala/services/local/LocalResponse.scala @@ -1,7 +1,6 @@ // Copyright (c) 2017 PSForever package services.local -import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound} import net.psforever.types.Vector3 @@ -10,10 +9,8 @@ object LocalResponse { final case class DoorOpens(door_guid : PlanetSideGUID) extends Response final case class DoorCloses(door_guid : PlanetSideGUID) extends Response - final case class DropItem(item_id : Int, item_guid : PlanetSideGUID, item_data : ConstructorData) extends Response final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response - final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response final case class ProximityTerminalEffect(object_guid : PlanetSideGUID, effectState : Boolean) extends Response final case class TriggerSound(sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Response } diff --git a/pslogin/src/main/scala/services/local/LocalService.scala b/pslogin/src/main/scala/services/local/LocalService.scala index 2d20f1e7..5c01ee1a 100644 --- a/pslogin/src/main/scala/services/local/LocalService.scala +++ b/pslogin/src/main/scala/services/local/LocalService.scala @@ -2,14 +2,12 @@ package services.local import akka.actor.{Actor, Props} -import net.psforever.packet.game.objectcreate.{DroppedItemData, PlacementData} -import services.local.support.{DoorCloseActor, DroppedItemRemover, HackClearActor} -import services.{GenericEventBus, RemoverActor, Service} +import services.local.support.{DoorCloseActor, HackClearActor} +import services.{GenericEventBus, Service} class LocalService extends Actor { private val doorCloser = context.actorOf(Props[DoorCloseActor], "local-door-closer") private val hackClearer = context.actorOf(Props[HackClearActor], "local-hack-clearer") - private val janitor = context.actorOf(Props[DroppedItemRemover], "local-item-remover") private [this] val log = org.log4s.getLogger override def preStart = { @@ -48,21 +46,6 @@ class LocalService extends Actor { LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.DoorCloses(door_guid)) ) - case LocalAction.DropItem(player_guid, item) => - val definition = item.Definition - val objectData = DroppedItemData( - PlacementData(item.Position, item.Orientation), - definition.Packet.ConstructorData(item).get - ) - LocalEvents.publish( - LocalServiceResponse(s"/$forChannel/Local", player_guid, - LocalResponse.DropItem(definition.ObjectId, item.GUID, objectData) - ) - ) - case LocalAction.ObjectDelete(player_guid, item_guid, unk) => - LocalEvents.publish( - LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.ObjectDelete(item_guid, unk)) - ) case LocalAction.HackClear(player_guid, target, unk1, unk2) => LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.HackClear(target.GUID, unk1, unk2)) @@ -83,12 +66,6 @@ class LocalService extends Actor { case _ => ; } - //messages to DroppedItemRemover - case msg @ (RemoverActor.AddTask | - RemoverActor.HurrySpecific | RemoverActor.HurryAll | - RemoverActor.ClearSpecific | RemoverActor.ClearAll) => - janitor ! msg - //response from DoorCloseActor case DoorCloseActor.CloseTheDoor(door_guid, zone_id) => LocalEvents.publish( diff --git a/pslogin/src/test/scala/AvatarServiceTest.scala b/pslogin/src/test/scala/AvatarServiceTest.scala index c122dcdf..884d0fe1 100644 --- a/pslogin/src/test/scala/AvatarServiceTest.scala +++ b/pslogin/src/test/scala/AvatarServiceTest.scala @@ -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) } From ee5d0c666cbc0ca14ed46a2ffd3dfaac4619e95d Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 26 May 2018 01:04:38 -0400 Subject: [PATCH 4/9] replaced DelayedDeconstructionActor and DeconstructionActor with VehicleRemover, a class that does performs both tasks; all messages to, from, and used by the previous two actors have been removed and replaced with the new messages --- .../src/main/scala/WorldSessionActor.scala | 17 +- .../main/scala/services/RemoverActor.scala | 32 +- .../avatar/support/CorpseRemovalActor.scala | 4 +- .../avatar/support/DroppedItemRemover.scala | 2 +- .../services/vehicle/VehicleService.scala | 40 +-- .../vehicle/VehicleServiceMessage.scala | 6 +- .../vehicle/support/DeconstructionActor.scala | 276 ------------------ .../support/DelayedDeconstructionActor.scala | 114 -------- .../vehicle/support/VehicleRemover.scala | 56 ++++ 9 files changed, 102 insertions(+), 445 deletions(-) delete mode 100644 pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala delete mode 100644 pslogin/src/main/scala/services/vehicle/support/DelayedDeconstructionActor.scala create mode 100644 pslogin/src/main/scala/services/vehicle/support/VehicleRemover.scala diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index fb16f896..9633cac2 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -184,7 +184,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)) //start vehicle decay } vehicleService ! Service.Leave(Some(s"${vehicle.Actor}")) @@ -644,7 +644,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 { @@ -698,7 +698,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)) //start vehicle decay } case Mountable.CanDismount(obj : Mountable, _) => @@ -1143,9 +1143,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)) //??? @@ -1408,7 +1405,6 @@ class WorldSessionActor extends Actor with MDCContextAware { 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)) @@ -1460,6 +1456,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)) @@ -2284,8 +2281,8 @@ 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) + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), continent)) + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent, Some(0 seconds))) log.info(s"RequestDestroy: vehicle $object_guid") } else { @@ -2801,7 +2798,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 => diff --git a/pslogin/src/main/scala/services/RemoverActor.scala b/pslogin/src/main/scala/services/RemoverActor.scala index 71af341d..be695d05 100644 --- a/pslogin/src/main/scala/services/RemoverActor.scala +++ b/pslogin/src/main/scala/services/RemoverActor.scala @@ -51,6 +51,8 @@ abstract class RemoverActor extends Actor { 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. @@ -99,6 +101,7 @@ abstract class RemoverActor extends Actor { 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 { @@ -106,17 +109,18 @@ abstract class RemoverActor extends Actor { 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 { - log.trace(s"$obj is already queued for removal") + trace(s"$obj is already queued for removal") } } } else { - log.trace(s"$obj either does not qualify for this Remover or is already queued") + trace(s"$obj either does not qualify for this Remover or is already queued") } case RemoverActor.HurrySpecific(targets, zone) => @@ -145,7 +149,7 @@ abstract class RemoverActor extends Actor { import scala.concurrent.ExecutionContext.Implicits.global secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) } - log.trace(s"item removal task has found ${in.size} items to remove") + trace(s"item removal task has found ${in.size} items to remove") case RemoverActor.TryDelete() => secondTask.cancel @@ -156,7 +160,7 @@ abstract class RemoverActor extends Actor { import scala.concurrent.ExecutionContext.Implicits.global secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) } - log.trace(s"item removal task has removed ${in.size} items") + 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 unregistered - $ex") @@ -172,11 +176,13 @@ abstract class RemoverActor extends Actor { */ def HurrySpecific(targets : List[PlanetSideGameObject], zone : Zone) : Unit = { CullTargetsFromFirstHeap(targets, zone) match { - case Nil => ; + 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 } + secondHeap = secondHeap ++ list.map { RepackageEntry } import scala.concurrent.ExecutionContext.Implicits.global secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) } @@ -186,6 +192,7 @@ abstract class RemoverActor extends Actor { * 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 } @@ -199,7 +206,12 @@ abstract class RemoverActor extends Actor { * Remove specific entries from the first pool. */ def ClearSpecific(targets : List[PlanetSideGameObject], zone : Zone) : Unit = { - CullTargetsFromFirstHeap(targets, zone) + 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") + } } /** @@ -232,7 +244,7 @@ abstract class RemoverActor extends Actor { private def CullTargetsFromFirstHeap(targets : List[PlanetSideGameObject], zone : Zone) : List[RemoverActor.Entry] = { val culledEntries = if(targets.nonEmpty) { if(targets.size == 1) { - log.debug(s"a target submitted: ${targets.head}") + debug(s"a target submitted: ${targets.head}") //simple selection RemoverActor.recursiveFind(firstHeap.iterator, RemoverActor.Entry(targets.head, zone, 0)) match { case None => ; @@ -244,7 +256,7 @@ abstract class RemoverActor extends Actor { } } else { - log.trace(s"multiple targets submitted: $targets") + debug(s"multiple targets submitted: $targets") //cumbersome partition //a - find targets from entries val locatedTargets = for { @@ -267,7 +279,7 @@ abstract class RemoverActor extends Actor { } } else { - log.trace(s"all targets within the specified zone $zone will be submitted") + 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) diff --git a/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala b/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala index 68237ab4..f75da946 100644 --- a/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala +++ b/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala @@ -25,9 +25,7 @@ class CorpseRemovalActor extends RemoverActor { context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID)) } - def ClearanceTest(entry : RemoverActor.Entry) : Boolean = { - !entry.zone.Corpses.contains(entry.obj.asInstanceOf[Player]) - } + def ClearanceTest(entry : RemoverActor.Entry) : Boolean = !entry.zone.Corpses.contains(entry.obj) def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = { GUIDTask.UnregisterPlayer(entry.obj.asInstanceOf[Player])(entry.zone.GUID) diff --git a/pslogin/src/main/scala/services/avatar/support/DroppedItemRemover.scala b/pslogin/src/main/scala/services/avatar/support/DroppedItemRemover.scala index ed0fdea7..01bc9d47 100644 --- a/pslogin/src/main/scala/services/avatar/support/DroppedItemRemover.scala +++ b/pslogin/src/main/scala/services/avatar/support/DroppedItemRemover.scala @@ -25,7 +25,7 @@ class DroppedItemRemover extends RemoverActor { context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID)) } - def ClearanceTest(entry : RemoverActor.Entry) : Boolean = true + 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) diff --git a/pslogin/src/main/scala/services/vehicle/VehicleService.scala b/pslogin/src/main/scala/services/vehicle/VehicleService.scala index 6b16ec2b..e6c548ee 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleService.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleService.scala @@ -4,15 +4,12 @@ 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, Service} +import services.{GenericEventBus, RemoverActor, Service} 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 +84,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 +104,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 +147,11 @@ 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 + vehicleDecon forward RemoverActor.AddTask(vehicle, zone) //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) => diff --git a/pslogin/src/main/scala/services/vehicle/VehicleServiceMessage.scala b/pslogin/src/main/scala/services/vehicle/VehicleServiceMessage.scala index 61ec6854..8ed071e0 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleServiceMessage.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleServiceMessage.scala @@ -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) } diff --git a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala deleted file mode 100644 index 2a95b8a5..00000000 --- a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala +++ /dev/null @@ -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.
- *
- * 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.
- *
- * 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) -} diff --git a/pslogin/src/main/scala/services/vehicle/support/DelayedDeconstructionActor.scala b/pslogin/src/main/scala/services/vehicle/support/DelayedDeconstructionActor.scala deleted file mode 100644 index 7756b415..00000000 --- a/pslogin/src/main/scala/services/vehicle/support/DelayedDeconstructionActor.scala +++ /dev/null @@ -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.
- *
- * 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.
- *
- * 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()) -} diff --git a/pslogin/src/main/scala/services/vehicle/support/VehicleRemover.scala b/pslogin/src/main/scala/services/vehicle/support/VehicleRemover.scala new file mode 100644 index 00000000..cec2e65e --- /dev/null +++ b/pslogin/src/main/scala/services/vehicle/support/VehicleRemover.scala @@ -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) + } +} From c7641fc1172e279a88e18e3839f766b56ef865f1 Mon Sep 17 00:00:00 2001 From: FateJH Date: Sun, 27 May 2018 02:24:20 -0400 Subject: [PATCH 5/9] modified zone ground actor operation to include no-callback item removal message; adjusted tests for Zone and wrote tests for RemoverActor --- .../net/psforever/objects/zones/Zone.scala | 8 +- .../objects/zones/ZoneGroundActor.scala | 25 +- .../src/test/scala/objects/PlayerTest.scala | 6 + common/src/test/scala/objects/ZoneTest.scala | 203 +++++-- pslogin/src/main/scala/PsLogin.scala | 2 +- .../src/main/scala/WorldSessionActor.scala | 39 +- .../main/scala/services/RemoverActor.scala | 2 +- pslogin/src/test/scala/RemoverActorTest.scala | 537 ++++++++++++++++++ 8 files changed, 754 insertions(+), 68 deletions(-) create mode 100644 pslogin/src/test/scala/RemoverActorTest.scala diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala index 4ac4d109..930ac574 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -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") @@ -389,11 +389,13 @@ object Zone { 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(item : Equipment) + final case class CanNotDropItem(zone : Zone, item : Equipment, reason : String) final case class PickupItem(item_guid : PlanetSideGUID) final case class ItemInHand(item : Equipment) - final case class CanNotPickupItem(item_guid : PlanetSideGUID) + final case class CanNotPickupItem(zone : Zone, item_guid : PlanetSideGUID, reason : String) + + final case class RemoveItem(item_guid : PlanetSideGUID) } object Vehicle { diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala index b44be2c3..5b4f3e67 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala @@ -12,17 +12,23 @@ 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.Ground.DropItem(item, pos, orient) => - sender ! (FindItemOnGround(item.GUID) match { - case None => - equipmentOnGround += item - Zone.Ground.ItemOnGround(item, pos, orient) - case Some(_) => - Zone.Ground.CanNotDropItem(item) + 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) => @@ -30,9 +36,12 @@ class ZoneGroundActor(equipmentOnGround : ListBuffer[Equipment]) extends Actor { case Some(item) => Zone.Ground.ItemInHand(item) case None => - Zone.Ground.CanNotPickupItem(item_guid) + Zone.Ground.CanNotPickupItem(zone, item_guid, "can not find") }) + case Zone.Ground.RemoveItem(item_guid) => + FindItemOnGround(item_guid) //intentionally no callback + case _ => ; } diff --git a/common/src/test/scala/objects/PlayerTest.scala b/common/src/test/scala/objects/PlayerTest.scala index 560b1c52..416eba4d 100644 --- a/common/src/test/scala/objects/PlayerTest.scala +++ b/common/src/test/scala/objects/PlayerTest.scala @@ -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) } diff --git a/common/src/test/scala/objects/ZoneTest.scala b/common/src/test/scala/objects/ZoneTest.scala index 84868f23..bec2e384 100644 --- a/common/src/test/scala/objects/ZoneTest.scala +++ b/common/src/test/scala/objects/ZoneTest.scala @@ -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,55 +464,184 @@ 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) + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") + zone.Actor ! Zone.Init() + receiveOne(200 milliseconds) //consume - "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 - - assert(zone.EquipmentOnGround.isEmpty) - assert(item.Position == Vector3.Zero) - assert(item.Orientation == Vector3.Zero) + 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)) - 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 +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) + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") + zone.Actor ! Zone.Init() + + "DropItem" should { + "not drop an item that is not registered" in { + assert(!zone.EquipmentOnGround.contains(item)) zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) - expectNoMsg(Duration.create(100, "ms")) - assert(zone.EquipmentOnGround == List(item)) - zone.Ground ! Zone.Ground.PickupItem(PlanetSideGUID(10)) - val reply = receiveOne(Duration.create(100, "ms")) + 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)) + } + } +} +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 + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") + zone.Actor ! Zone.Init() + + "DropItem" should { + "not drop an item that is not registered to the zone" in { + 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 + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") + zone.Actor ! Zone.Init() + + "DropItem" should { + "not drop an item that has already been dropped" in { + assert(!zone.EquipmentOnGround.contains(item)) assert(zone.EquipmentOnGround.isEmpty) - assert(reply.isInstanceOf[Zone.Ground.ItemInHand]) - assert(reply.asInstanceOf[Zone.Ground.ItemInHand].item == 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.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) - expectNoMsg(Duration.create(100, "ms")) - assert(zone.EquipmentOnGround == List(item)) - zone.Ground ! Zone.Ground.PickupItem(PlanetSideGUID(11)) //wrong guid - expectNoMsg(Duration.create(500, "ms")) + 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) + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") + zone.Actor ! Zone.Init() + + "PickupItem" should { + "pickup an item from ground" in { + 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 + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") + zone.Actor ! Zone.Init() + + "PickupItem" should { + "not pickup an item if it can not be found" in { + 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 + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") + zone.Actor ! Zone.Init() + + "RemoveItem" should { + "remove an item from the ground without callback (even if the item is not found)" in { + 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)) } } } diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index e689aa9e..ebbbfdfb 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -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 { diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 9633cac2..c4ff7e36 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1364,13 +1364,8 @@ class WorldSessionActor extends Actor with MDCContextAware { } avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(exclusionId, item, continent)) - case Zone.Ground.CanNotDropItem(item) => - log.warn(s"DropItem: $player tried to drop a $item on the ground, but he missed") - player.Find(item) match { - case None => //item in limbo - taskResolver ! GUIDTask.UnregisterEquipment(item)(continent.GUID) - case Some(_) => ; - } + 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 { @@ -1392,8 +1387,8 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.Ground ! Zone.Ground.DropItem(item, item.Position, item.Orientation) //restore previous state } - case Zone.Ground.CanNotPickupItem(item_guid) => - continent.GUID(item_guid) match { + 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 => @@ -2283,10 +2278,10 @@ class WorldSessionActor extends Actor with MDCContextAware { && ((vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Health == 0))) { vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), continent)) vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent, Some(0 seconds))) - log.info(s"RequestDestroy: vehicle $object_guid") + log.info(s"RequestDestroy: vehicle $vehicle") } else { - 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) => @@ -2308,20 +2303,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) => diff --git a/pslogin/src/main/scala/services/RemoverActor.scala b/pslogin/src/main/scala/services/RemoverActor.scala index be695d05..85e211e0 100644 --- a/pslogin/src/main/scala/services/RemoverActor.scala +++ b/pslogin/src/main/scala/services/RemoverActor.scala @@ -163,7 +163,7 @@ abstract class RemoverActor extends Actor { 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 unregistered - $ex") + log.error(s"${entry.obj} from ${entry.zone} not properly deleted - $ex") case _ => ; } diff --git a/pslogin/src/test/scala/RemoverActorTest.scala b/pslogin/src/test/scala/RemoverActorTest.scala new file mode 100644 index 00000000..c3815924 --- /dev/null +++ b/pslogin/src/test/scala/RemoverActorTest.scala @@ -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(200 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(100 milliseconds) + assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert]) + val reply2 = probe.receiveOne(100 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(300 milliseconds) + assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) + val reply7 = probe.receiveOne(300 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(200 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(100 milliseconds) + assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert]) + val reply2 = probe.receiveOne(100 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(200 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(100 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(200 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, 3 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(200 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(100 milliseconds) + assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert]) + val reply2 = probe.receiveOne(100 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(300 milliseconds) + assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) + val reply7 = probe.receiveOne(300 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" in { + expectNoMsg(200 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, 3 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(300 milliseconds) + assert(reply6a.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) + val reply7a = probe.receiveOne(300 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(300 milliseconds) + assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) + val reply7b = probe.receiveOne(300 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(200 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, 3 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(300 milliseconds) + assert(reply6a.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) + val reply7a = probe.receiveOne(300 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(300 milliseconds) + assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) + val reply7b = probe.receiveOne(300 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(200 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, 3 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, 3 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(300 milliseconds) + assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) + val reply7b = probe.receiveOne(300 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(200 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, 3 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, 3 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(200 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, 3 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(300 milliseconds) + assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) + val reply7 = probe.receiveOne(300 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(200 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, 3 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(200 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, 3 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, 2 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) + } + }) + } + } +} From 8c7417aabf09becb5c70910fc2b9fae6319e64ff Mon Sep 17 00:00:00 2001 From: FateJH Date: Sun, 27 May 2018 20:59:44 -0400 Subject: [PATCH 6/9] added timing so that thr advanced mobile spawn and the router waste away for 20 minutes, while all other vehicles die early at 5 minutes --- .../net/psforever/objects/GlobalDefinitions.scala | 4 ++++ .../objects/definition/VehicleDefinition.scala | 14 ++++++++++++++ pslogin/src/main/scala/WorldSessionActor.scala | 4 ++-- .../scala/services/vehicle/VehicleService.scala | 4 +++- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index d3b9c05f..a1246bbf 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -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 diff --git a/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala index 5bb11e89..68c7e14e 100644 --- a/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala @@ -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 = { diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index c4ff7e36..cd0fe938 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -184,7 +184,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.Decon(RemoverActor.AddTask(vehicle, continent)) //start vehicle decay + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent), vehicle.Definition.DeconstructionTime) //start vehicle decay } vehicleService ! Service.Leave(Some(s"${vehicle.Actor}")) @@ -698,7 +698,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.Decon(RemoverActor.AddTask(obj, continent)) //start vehicle decay + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, obj.Definition.DeconstructionTime)) //start vehicle decay } case Mountable.CanDismount(obj : Mountable, _) => diff --git a/pslogin/src/main/scala/services/vehicle/VehicleService.scala b/pslogin/src/main/scala/services/vehicle/VehicleService.scala index e6c548ee..0bf781f6 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleService.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleService.scala @@ -8,6 +8,8 @@ import services.vehicle.support.VehicleRemover import net.psforever.types.DriveState import services.{GenericEventBus, RemoverActor, Service} +import scala.concurrent.duration._ + class VehicleService extends Actor { private val vehicleDecon : ActorRef = context.actorOf(Props[VehicleRemover], "vehicle-decon-agent") private [this] val log = org.log4s.getLogger @@ -147,7 +149,7 @@ class VehicleService extends Actor { VehicleEvents.publish( VehicleServiceResponse(s"/${zone.Id}/Vehicle", Service.defaultPlayerGUID, VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata)) ) - vehicleDecon forward RemoverActor.AddTask(vehicle, zone) + vehicleDecon forward RemoverActor.AddTask(vehicle, zone, Some(30 seconds)) //from VehicleSpawnControl case VehicleSpawnPad.DisposeVehicle(vehicle, zone) => From d26e8834c6062f55776de754cccb11eeec3e8e4b Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 28 May 2018 08:24:57 -0400 Subject: [PATCH 7/9] hopefully more generous timing for the tests --- common/src/test/scala/objects/ZoneTest.scala | 4 ++ pslogin/src/test/scala/RemoverActorTest.scala | 70 +++++++++---------- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/common/src/test/scala/objects/ZoneTest.scala b/common/src/test/scala/objects/ZoneTest.scala index bec2e384..882f2344 100644 --- a/common/src/test/scala/objects/ZoneTest.scala +++ b/common/src/test/scala/objects/ZoneTest.scala @@ -476,6 +476,7 @@ class ZoneGroundDropItemTest extends ActorTest { "DropItem" should { "drop item on ground" in { + expectNoMsg(100 milliseconds) 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)) @@ -500,6 +501,7 @@ class ZoneGroundCanNotDropItem1Test extends ActorTest { "DropItem" should { "not drop an item that is not registered" in { + expectNoMsg(100 milliseconds) assert(!zone.EquipmentOnGround.contains(item)) zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) @@ -524,6 +526,7 @@ class ZoneGroundCanNotDropItem2Test extends ActorTest { "DropItem" should { "not drop an item that is not registered to the zone" in { + expectNoMsg(100 milliseconds) assert(!zone.EquipmentOnGround.contains(item)) zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) @@ -548,6 +551,7 @@ class ZoneGroundCanNotDropItem3Test extends ActorTest { "DropItem" should { "not drop an item that has already been dropped" in { + expectNoMsg(100 milliseconds) assert(!zone.EquipmentOnGround.contains(item)) assert(zone.EquipmentOnGround.isEmpty) zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) diff --git a/pslogin/src/test/scala/RemoverActorTest.scala b/pslogin/src/test/scala/RemoverActorTest.scala index c3815924..cfeaae30 100644 --- a/pslogin/src/test/scala/RemoverActorTest.scala +++ b/pslogin/src/test/scala/RemoverActorTest.scala @@ -22,9 +22,9 @@ class StandardRemoverActorTest extends ActorTest { val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere) - val reply1 = probe.receiveOne(100 milliseconds) + val reply1 = probe.receiveOne(200 milliseconds) assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert]) - val reply2 = probe.receiveOne(100 milliseconds) + val reply2 = probe.receiveOne(200 milliseconds) assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert]) probe.expectNoMsg(1 seconds) //delay val reply3 = probe.receiveOne(300 milliseconds) @@ -33,9 +33,9 @@ class StandardRemoverActorTest extends ActorTest { assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) val reply5 = probe.receiveOne(300 milliseconds) assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert]) - val reply6 = probe.receiveOne(300 milliseconds) + val reply6 = probe.receiveOne(500 milliseconds) assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) - val reply7 = probe.receiveOne(300 milliseconds) + val reply7 = probe.receiveOne(500 milliseconds) assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) } } @@ -51,9 +51,9 @@ class DelayedRemoverActorTest extends ActorTest { 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(100 milliseconds) + val reply1 = probe.receiveOne(200 milliseconds) assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert]) - val reply2 = probe.receiveOne(100 milliseconds) + val reply2 = probe.receiveOne(200 milliseconds) assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert]) //no delay val reply3 = probe.receiveOne(300 milliseconds) @@ -81,7 +81,7 @@ class ExcludedRemoverActorTest extends ActorTest { val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") remover ! RemoverActor.AddTask(AlternateTestObject, Zone.Nowhere) - val reply1 = probe.receiveOne(100 milliseconds) + 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 @@ -101,7 +101,7 @@ class MultipleRemoverActorTest extends ActorTest { remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere) remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere) - val replies = probe.receiveN(14, 3 seconds) + val replies = probe.receiveN(14, 5 seconds) var ita : Int = 0 var ija : Int = 0 var fja : Int = 0 @@ -134,9 +134,9 @@ class HurrySpecificRemoverActorTest extends ActorTest { 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(100 milliseconds) + val reply1 = probe.receiveOne(200 milliseconds) assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert]) - val reply2 = probe.receiveOne(100 milliseconds) + 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 @@ -146,9 +146,9 @@ class HurrySpecificRemoverActorTest extends ActorTest { assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) val reply5 = probe.receiveOne(300 milliseconds) assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert]) - val reply6 = probe.receiveOne(300 milliseconds) + val reply6 = probe.receiveOne(500 milliseconds) assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) - val reply7 = probe.receiveOne(300 milliseconds) + val reply7 = probe.receiveOne(500 milliseconds) assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) } } @@ -159,14 +159,14 @@ class HurrySelectionRemoverActorTest extends ActorTest { final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } } "RemoverActor" should { - "be able to hurry certain tasks" in { + "be able to hurry certain tasks, but let others finish normally" in { expectNoMsg(200 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, 3 seconds) + val replies = probe.receiveN(4, 5 seconds) var ita : Int = 0 var ija : Int = 0 replies.collect { @@ -184,9 +184,9 @@ class HurrySelectionRemoverActorTest extends ActorTest { assert(reply4a.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) val reply5a = probe.receiveOne(300 milliseconds) assert(reply5a.isInstanceOf[RemoverActorTest.SecondJobAlert]) - val reply6a = probe.receiveOne(300 milliseconds) + val reply6a = probe.receiveOne(500 milliseconds) assert(reply6a.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) - val reply7a = probe.receiveOne(300 milliseconds) + val reply7a = probe.receiveOne(500 milliseconds) assert(reply7a.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) //second remover ! RemoverActor.HurrySpecific(List(TestObject2), Zone.Nowhere) //hurried @@ -196,9 +196,9 @@ class HurrySelectionRemoverActorTest extends ActorTest { assert(reply4b.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) val reply5b = probe.receiveOne(300 milliseconds) assert(reply5b.isInstanceOf[RemoverActorTest.SecondJobAlert]) - val reply6b = probe.receiveOne(300 milliseconds) + val reply6b = probe.receiveOne(500 milliseconds) assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) - val reply7b = probe.receiveOne(300 milliseconds) + val reply7b = probe.receiveOne(500 milliseconds) assert(reply7b.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) } } @@ -217,7 +217,7 @@ class HurryMultipleRemoverActorTest extends ActorTest { remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds)) remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds)) - val replies = probe.receiveN(4, 3 seconds) + val replies = probe.receiveN(4, 5 seconds) var ita : Int = 0 var ija : Int = 0 replies.collect { @@ -235,9 +235,9 @@ class HurryMultipleRemoverActorTest extends ActorTest { assert(reply4a.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) val reply5a = probe.receiveOne(300 milliseconds) assert(reply5a.isInstanceOf[RemoverActorTest.SecondJobAlert]) - val reply6a = probe.receiveOne(300 milliseconds) + val reply6a = probe.receiveOne(500 milliseconds) assert(reply6a.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) - val reply7a = probe.receiveOne(300 milliseconds) + val reply7a = probe.receiveOne(500 milliseconds) assert(reply7a.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) //second remover ! RemoverActor.HurrySpecific(List(TestObject2), Zone.Nowhere) //hurried @@ -247,9 +247,9 @@ class HurryMultipleRemoverActorTest extends ActorTest { assert(reply4b.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) val reply5b = probe.receiveOne(300 milliseconds) assert(reply5b.isInstanceOf[RemoverActorTest.SecondJobAlert]) - val reply6b = probe.receiveOne(300 milliseconds) + val reply6b = probe.receiveOne(500 milliseconds) assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) - val reply7b = probe.receiveOne(300 milliseconds) + val reply7b = probe.receiveOne(500 milliseconds) assert(reply7b.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) } } @@ -270,7 +270,7 @@ class HurryByZoneRemoverActorTest extends ActorTest { remover ! RemoverActor.AddTask(TestObject2, zone, Some(5 seconds)) remover ! RemoverActor.AddTask(TestObject3, Zone.Nowhere, Some(5 seconds)) - val replies1 = probe.receiveN(6, 3 seconds) + val replies1 = probe.receiveN(6, 5 seconds) var ita : Int = 0 var ija : Int = 0 replies1.collect { @@ -282,7 +282,7 @@ class HurryByZoneRemoverActorTest extends ActorTest { 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, 3 seconds) + val replies2 = probe.receiveN(10, 5 seconds) var fja : Int = 0 var cta : Int = 0 var sja : Int = 0 @@ -307,9 +307,9 @@ class HurryByZoneRemoverActorTest extends ActorTest { assert(reply4b.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) val reply5b = probe.receiveOne(300 milliseconds) assert(reply5b.isInstanceOf[RemoverActorTest.SecondJobAlert]) - val reply6b = probe.receiveOne(300 milliseconds) + val reply6b = probe.receiveOne(500 milliseconds) assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) - val reply7b = probe.receiveOne(300 milliseconds) + val reply7b = probe.receiveOne(500 milliseconds) assert(reply7b.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) } } @@ -329,7 +329,7 @@ class HurryAllRemoverActorTest extends ActorTest { remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(15 seconds)) remover ! RemoverActor.AddTask(TestObject3, Zone.Nowhere, Some(10 seconds)) - val replies1 = probe.receiveN(6, 3 seconds) + val replies1 = probe.receiveN(6, 5 seconds) var ita : Int = 0 var ija : Int = 0 replies1.collect { @@ -341,7 +341,7 @@ class HurryAllRemoverActorTest extends ActorTest { 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, 3 seconds) + val replies2 = probe.receiveN(15, 5 seconds) var fja : Int = 0 var cta : Int = 0 var sja : Int = 0 @@ -374,7 +374,7 @@ class ClearSelectionRemoverActorTest extends ActorTest { remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds)) remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds)) - val replies = probe.receiveN(4, 3 seconds) + val replies = probe.receiveN(4, 5 seconds) var ita : Int = 0 var ija : Int = 0 replies.collect { @@ -392,9 +392,9 @@ class ClearSelectionRemoverActorTest extends ActorTest { assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) val reply5 = probe.receiveOne(300 milliseconds) assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert]) - val reply6 = probe.receiveOne(300 milliseconds) + val reply6 = probe.receiveOne(500 milliseconds) assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) - val reply7 = probe.receiveOne(300 milliseconds) + val reply7 = probe.receiveOne(500 milliseconds) assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) //wait probe.expectNoMsg(2 seconds) //nothing more to do @@ -414,7 +414,7 @@ class ClearAllRemoverActorTest extends ActorTest { remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds)) remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds)) - val replies = probe.receiveN(4, 3 seconds) + val replies = probe.receiveN(4, 5 seconds) var ita : Int = 0 var ija : Int = 0 replies.collect { @@ -443,7 +443,7 @@ class EarlyDeathRemoverActorTest extends ActorTest { remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds)) remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds)) - val replies = probe.receiveN(4, 3 seconds) + val replies = probe.receiveN(4, 5 seconds) var ita : Int = 0 var ija : Int = 0 replies.collect { @@ -455,7 +455,7 @@ class EarlyDeathRemoverActorTest extends ActorTest { probe.expectNoMsg(2 seconds) remover ! akka.actor.PoisonPill // - val replies2 = probe.receiveN(8, 2 seconds) + val replies2 = probe.receiveN(8, 5 seconds) var fja : Int = 0 var cta : Int = 0 var sja : Int = 0 From 9e75fd1191c6f08ff52f1a0461d72d943da59057 Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 28 May 2018 09:04:09 -0400 Subject: [PATCH 8/9] even more generosity with the tests --- common/src/test/scala/objects/ZoneTest.scala | 33 ++++++++----------- pslogin/src/test/scala/RemoverActorTest.scala | 24 +++++++------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/common/src/test/scala/objects/ZoneTest.scala b/common/src/test/scala/objects/ZoneTest.scala index 882f2344..70642ee9 100644 --- a/common/src/test/scala/objects/ZoneTest.scala +++ b/common/src/test/scala/objects/ZoneTest.scala @@ -470,13 +470,11 @@ class ZoneGroundDropItemTest extends ActorTest { hub.register(item, 10) val zone = new Zone("test", new ZoneMap("test-map"), 0) zone.GUID(hub) - zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") - zone.Actor ! Zone.Init() - receiveOne(200 milliseconds) //consume + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!" "DropItem" should { "drop item on ground" in { - expectNoMsg(100 milliseconds) + 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)) @@ -496,12 +494,11 @@ class ZoneGroundCanNotDropItem1Test extends ActorTest { //hub.register(item, 10) //!important val zone = new Zone("test", new ZoneMap("test-map"), 0) zone.GUID(hub) - zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") - zone.Actor ! Zone.Init() + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!" "DropItem" should { "not drop an item that is not registered" in { - expectNoMsg(100 milliseconds) + receiveOne(1 second) //consume assert(!zone.EquipmentOnGround.contains(item)) zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) @@ -521,12 +518,11 @@ class ZoneGroundCanNotDropItem2Test extends ActorTest { hub.register(item, 10) //!important val zone = new Zone("test", new ZoneMap("test-map"), 0) //zone.GUID(hub) //!important - zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") - zone.Actor ! Zone.Init() + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!" "DropItem" should { "not drop an item that is not registered to the zone" in { - expectNoMsg(100 milliseconds) + receiveOne(1 second) //consume assert(!zone.EquipmentOnGround.contains(item)) zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) @@ -546,12 +542,11 @@ class ZoneGroundCanNotDropItem3Test extends ActorTest { hub.register(item, 10) //!important val zone = new Zone("test", new ZoneMap("test-map"), 0) zone.GUID(hub) //!important - zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") - zone.Actor ! Zone.Init() + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!" "DropItem" should { "not drop an item that has already been dropped" in { - expectNoMsg(100 milliseconds) + receiveOne(1 second) //consume assert(!zone.EquipmentOnGround.contains(item)) assert(zone.EquipmentOnGround.isEmpty) zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) @@ -579,11 +574,11 @@ class ZoneGroundPickupItemTest extends ActorTest { hub.register(item, 10) val zone = new Zone("test", new ZoneMap("test-map"), 0) zone.GUID(hub) - zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") - zone.Actor ! Zone.Init() + 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) @@ -606,11 +601,11 @@ class ZoneGroundCanNotPickupItemTest extends ActorTest { hub.register(item, 10) val zone = new Zone("test", new ZoneMap("test-map"), 0) zone.GUID(hub) //still registered to this zone - zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") - zone.Actor ! Zone.Init() + 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) @@ -629,11 +624,11 @@ class ZoneGroundRemoveItemTest extends ActorTest { hub.register(item, 10) val zone = new Zone("test", new ZoneMap("test-map"), 0) zone.GUID(hub) //still registered to this zone - zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-test-zone") - zone.Actor ! Zone.Init() + 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) diff --git a/pslogin/src/test/scala/RemoverActorTest.scala b/pslogin/src/test/scala/RemoverActorTest.scala index cfeaae30..316c58af 100644 --- a/pslogin/src/test/scala/RemoverActorTest.scala +++ b/pslogin/src/test/scala/RemoverActorTest.scala @@ -17,7 +17,7 @@ class StandardRemoverActorTest extends ActorTest { "RemoverActor" should { "handle a simple task" in { - expectNoMsg(200 milliseconds) + expectNoMsg(500 milliseconds) val probe = TestProbe() val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere) @@ -46,7 +46,7 @@ class DelayedRemoverActorTest extends ActorTest { "RemoverActor" should { "handle a simple task (timed)" in { - expectNoMsg(200 milliseconds) + 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)) @@ -76,7 +76,7 @@ class ExcludedRemoverActorTest extends ActorTest { "RemoverActor" should { "allow only specific objects" in { - expectNoMsg(200 milliseconds) + expectNoMsg(500 milliseconds) val probe = TestProbe() val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") remover ! RemoverActor.AddTask(AlternateTestObject, Zone.Nowhere) @@ -95,7 +95,7 @@ class MultipleRemoverActorTest extends ActorTest { "RemoverActor" should { "work on parallel tasks" in { - expectNoMsg(200 milliseconds) + expectNoMsg(500 milliseconds) val probe = TestProbe() val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere) @@ -129,7 +129,7 @@ class HurrySpecificRemoverActorTest extends ActorTest { "RemoverActor" should { "be able to hurry certain tasks" in { - expectNoMsg(200 milliseconds) + 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 @@ -160,7 +160,7 @@ class HurrySelectionRemoverActorTest extends ActorTest { "RemoverActor" should { "be able to hurry certain tasks, but let others finish normally" in { - expectNoMsg(200 milliseconds) + 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)) @@ -211,7 +211,7 @@ class HurryMultipleRemoverActorTest extends ActorTest { "RemoverActor" should { "be able to hurry certain tasks, but only valid ones" in { - expectNoMsg(200 milliseconds) + 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)) @@ -263,7 +263,7 @@ class HurryByZoneRemoverActorTest extends ActorTest { "RemoverActor" should { "be able to hurry certain tasks by their zone" in { - expectNoMsg(200 milliseconds) + 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)) @@ -322,7 +322,7 @@ class HurryAllRemoverActorTest extends ActorTest { "RemoverActor" should { "be able to hurry all tasks to completion" in { - expectNoMsg(200 milliseconds) + 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)) @@ -368,7 +368,7 @@ class ClearSelectionRemoverActorTest extends ActorTest { "RemoverActor" should { "be able to clear certain tasks" in { - expectNoMsg(200 milliseconds) + 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)) @@ -408,7 +408,7 @@ class ClearAllRemoverActorTest extends ActorTest { "RemoverActor" should { "be able to clear all tasks, with no more work on them" in { - expectNoMsg(200 milliseconds) + 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)) @@ -437,7 +437,7 @@ class EarlyDeathRemoverActorTest extends ActorTest { "RemoverActor" should { "be able to hurry certain tasks" in { - expectNoMsg(200 milliseconds) + 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)) From a96d76a3d6319fbb61ad75fbc74805619155a1b0 Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 4 Jun 2018 07:35:44 -0400 Subject: [PATCH 9/9] two clarifying comments requested --- pslogin/src/main/scala/WorldSessionActor.scala | 2 +- pslogin/src/main/scala/services/vehicle/VehicleService.scala | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index cd0fe938..976b7497 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1352,7 +1352,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case Zone.Ground.ItemOnGround(item, pos, orient) => item.Position = pos - item.Orientation = Vector3(0,0, orient.z) //only one kind of rotation is important + 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 diff --git a/pslogin/src/main/scala/services/vehicle/VehicleService.scala b/pslogin/src/main/scala/services/vehicle/VehicleService.scala index 0bf781f6..f4719eb4 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleService.scala +++ b/pslogin/src/main/scala/services/vehicle/VehicleService.scala @@ -149,6 +149,7 @@ class VehicleService extends Actor { VehicleEvents.publish( VehicleServiceResponse(s"/${zone.Id}/Vehicle", Service.defaultPlayerGUID, VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata)) ) + //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