diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index b7e803aa8..e5a212b9d 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -127,9 +127,9 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.Population ! Zone.Population.Release(avatar) player.VehicleSeated match { case None => - continent.Population ! Zone.Corpse.Add(player) FriskCorpse(player) //TODO eliminate dead letters if(!WellLootedCorpse(player)) { + continent.Population ! Zone.Corpse.Add(player) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent)) taskResolver ! GUIDTask.UnregisterLocker(player.Locker)(continent.GUID) //rest of player will be cleaned up with corpses } @@ -304,6 +304,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case AvatarResponse.ObjectDelete(item_guid, unk) => if(tplayer_guid != guid) { + log.info(s"Made to delete item $item_guid") sendResponse(ObjectDeleteMessage(item_guid, unk)) } @@ -1601,10 +1602,10 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.Population ! Zone.Population.Release(avatar) player.VehicleSeated match { case None => - continent.Population ! Zone.Corpse.Add(player) //TODO move back out of this match case when changing below issue FriskCorpse(player) if(!WellLootedCorpse(player)) { TurnPlayerIntoCorpse(player) + continent.Population ! Zone.Corpse.Add(player) //TODO move back out of this match case when changing below issue avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent)) } else { //no items in inventory; leave no corpse diff --git a/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala b/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala index ace1fc920..446907e92 100644 --- a/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala +++ b/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala @@ -15,17 +15,26 @@ import scala.concurrent.duration._ class CorpseRemovalActor extends Actor { private var burial : Cancellable = DefaultCancellable.obj - private var corpses : List[CorpseRemovalActor.Entry] = List() + private var decomposition : Cancellable = DefaultCancellable.obj + private var buriedCorpses : List[CorpseRemovalActor.Entry] = List() + private var taskResolver : ActorRef = Actor.noSender private[this] val log = org.log4s.getLogger override def postStop() = { //Cart Master: See you on Thursday. - corpses.foreach { BurialTask } - corpses = Nil + super.postStop() + burial.cancel + decomposition.cancel + + corpses.foreach(corpse => { + BurialTask(corpse) + LastRitesTask(corpse) + }) + buriedCorpses.foreach { LastRitesTask } } def receive : Receive = { @@ -42,7 +51,7 @@ class CorpseRemovalActor extends Actor { def Processing : Receive = { case CorpseRemovalActor.AddCorpse(corpse, zone, time) => - if(corpse.isBackpack) { + if(corpse.isBackpack && !buriedCorpses.exists(_.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)) @@ -51,9 +60,11 @@ class CorpseRemovalActor extends Actor { else { //unknown number of entries; append, sort, then re-time tasking val oldHead = corpses.head - corpses = (corpses :+ CorpseRemovalActor.Entry(corpse, zone, time)).sortBy(_.timeAlive) - if(oldHead != corpses.head) { - RetimeFirstTask() + if(!corpses.exists(_.corpse == corpse)) { + corpses = (corpses :+ CorpseRemovalActor.Entry(corpse, zone, time)).sortBy(_.timeAlive) + if(oldHead != corpses.head) { + RetimeFirstTask() + } } } } @@ -72,42 +83,66 @@ class CorpseRemovalActor extends Actor { 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) + 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") + decomposition.cancel //cumbersome partition //a - find targets from corpses - (for { + val locatedTargets = for { a <- targets b <- corpses if b.corpse == a && b.corpse.Continent.equals(a.Continent) && b.corpse.HasGUID && a.HasGUID && b.corpse.GUID == a.GUID - } yield b).foreach { BurialTask } - //b - corpses after the found targets are - //removed (note: cull any non-GUID entries while at it) + } yield b + locatedTargets.foreach { BurialTask } + buriedCorpses = locatedTargets ++ buriedCorpses + //b - corpses, after the found targets are removed (cull any non-GUID entries while at it) corpses = (for { - a <- targets + a <- locatedTargets.map { _.corpse } b <- corpses if b.corpse.HasGUID && a.HasGUID && (b.corpse != a || !b.corpse.Continent.equals(a.Continent) || !b.corpse.HasGUID || !a.HasGUID || b.corpse.GUID != a.GUID) } yield b).sortBy(_.timeAlive) + import scala.concurrent.ExecutionContext.Implicits.global + decomposition = context.system.scheduler.scheduleOnce(500 milliseconds, self, CorpseRemovalActor.TryDelete()) } RetimeFirstTask() } - case CorpseRemovalActor.Dispose() => + 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. @@ -122,21 +157,24 @@ class CorpseRemovalActor extends Actor { 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.Dispose()) + burial = context.system.scheduler.scheduleOnce(short_timeout, self, CorpseRemovalActor.StartDelete()) } } def BurialTask(entry : CorpseRemovalActor.Entry) : Unit = { - //Cart master: Nine pence. val target = entry.corpse - val zone = entry.zone - target.Position = Vector3.Zero //somewhere it will not disturb anything entry.zone.Population ! Zone.Corpse.Remove(target) - context.parent ! AvatarServiceMessage(zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, target.GUID)) - taskResolver ! BurialTask(target, zone) + context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, target.GUID)) } - def BurialTask(corpse : Player, zone : Zone) : TaskResolver.GiveTask = { + 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() { @@ -170,9 +208,11 @@ object CorpseRemovalActor { final case class Entry(corpse : Player, zone : Zone, timeAlive : Long = CorpseRemovalActor.time, time : Long = System.nanoTime()) - final case class FailureToWork(corpse : Player, zone : Zone, ex : Throwable) + private final case class FailureToWork(corpse : Player, zone : Zone, ex : Throwable) - final case class Dispose() + private final case class StartDelete() + + private final case class TryDelete() /** * A recursive function that finds and removes a specific player from a list of players. diff --git a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala index 9d7a3efad..906e7e9cc 100644 --- a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala +++ b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala @@ -30,10 +30,25 @@ class DeconstructionActor extends Actor { 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 = { /* @@ -52,45 +67,56 @@ class DeconstructionActor extends Actor { def Processing : Receive = { case DeconstructionActor.RequestDeleteVehicle(vehicle, zone, time) => - 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.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()) } - }) - 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.TryDeleteVehicle()) } - case DeconstructionActor.TryDeleteVehicle() => + case DeconstructionActor.StartDeleteVehicle() => scrappingProcess.cancel + heapEmptyProcess.cancel val now : Long = System.nanoTime val (vehiclesToScrap, vehiclesRemain) = PartitionEntries(vehicles, now) - vehicles = vehiclesRemain - vehiclesToScrap.foreach(entry => { - val vehicle = entry.vehicle - val zone = entry.zone - vehicle.Position = Vector3.Zero //somewhere it will not disturb anything - entry.zone.Transport ! Zone.Vehicle.Despawn(vehicle) - context.parent ! DeconstructionActor.DeleteVehicle(vehicle.GUID, zone.Id) //call up to the main event system - taskResolver ! DeconstructionTask(vehicle, zone) - }) - + vehicles = vehiclesRemain //entries from original list before partition + vehicleScrapHeap = vehicleScrapHeap ++ vehiclesToScrap //may include existing entries + vehiclesToScrap.foreach { RetirementTask } 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.TryDeleteVehicle()) + 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) => @@ -99,6 +125,20 @@ class DeconstructionActor extends Actor { case _ => ; } + def RetirementTask(entry : DeconstructionActor.VehicleEntry) : Unit = { + val vehicle = entry.vehicle + val zone = entry.zone + 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 + vehicle.Position = Vector3.Zero //somewhere it will not disturb anything + taskResolver ! DeconstructionTask(vehicle, zone) + } + /** * Construct a middleman `Task` intended to return error messages to the `DeconstructionActor`. * @param vehicle the `Vehicle` object @@ -195,6 +235,12 @@ object DeconstructionActor { 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() diff --git a/pslogin/src/test/scala/AvatarServiceTest.scala b/pslogin/src/test/scala/AvatarServiceTest.scala index deda78b11..ea86a6d75 100644 --- a/pslogin/src/test/scala/AvatarServiceTest.scala +++ b/pslogin/src/test/scala/AvatarServiceTest.scala @@ -329,7 +329,7 @@ class AvatarReleaseTest extends ActorTest { assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) - expectNoMsg(200 milliseconds) + expectNoMsg(1000 milliseconds) assert(zone.Corpses.isEmpty) assert(!obj.HasGUID) } @@ -379,7 +379,7 @@ class AvatarReleaseEarly1Test extends ActorTest { assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) - expectNoMsg(200 milliseconds) + expectNoMsg(600 milliseconds) assert(zone.Corpses.isEmpty) assert(!obj.HasGUID) } @@ -430,7 +430,7 @@ class AvatarReleaseEarly2Test extends ActorTest { assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) - expectNoMsg(200 milliseconds) + expectNoMsg(600 milliseconds) assert(zone.Corpses.isEmpty) assert(!obj.HasGUID) }