From 36b9d81e6c7917089e01376de10a9236b5ce68ce Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 25 May 2018 21:11:25 -0400 Subject: [PATCH] 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) }