diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 1cc05ec0..db4c14fe 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -689,6 +689,17 @@ object GlobalDefinitions { } } + def isMaxArms(tdef : ToolDefinition) : Boolean = { + tdef match { + case `trhev_dualcycler` | `nchev_scattercannon` | `vshev_quasar` + | `trhev_pounder` | `nchev_falcon` | `vshev_comet` + | `trhev_burster` | `nchev_sparrow` | `vshev_starfire` => + true + case _ => + false + } + } + def AIMAX(faction : PlanetSideEmpire.Value) : ToolDefinition = { faction match { case PlanetSideEmpire.TR => trhev_dualcycler diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala index 73699a10..68b7df90 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala @@ -4,7 +4,7 @@ package net.psforever.objects.definition.converter import net.psforever.objects.{EquipmentSlot, Player} import net.psforever.objects.equipment.Equipment import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, InternalSlot, InventoryData, PlacementData, RibbonBars} -import net.psforever.types.{CharacterGender, GrenadeState} +import net.psforever.types.{CharacterGender, GrenadeState, Vector3} import scala.annotation.tailrec import scala.util.{Failure, Success, Try} @@ -33,7 +33,7 @@ class CorpseConverter extends AvatarConverter { */ private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = { CharacterAppearanceData( - PlacementData(obj.Position, obj.Orientation), + PlacementData(obj.Position, Vector3(0,0, obj.Orientation.z)), BasicCharacterData(obj.Name, obj.Faction, CharacterGender.Male, 0, 0), 0, false, diff --git a/common/src/test/scala/objects/ZoneTest.scala b/common/src/test/scala/objects/ZoneTest.scala index 49fe866b..2c5cfcb6 100644 --- a/common/src/test/scala/objects/ZoneTest.scala +++ b/common/src/test/scala/objects/ZoneTest.scala @@ -12,7 +12,7 @@ import net.psforever.objects.serverobject.structures.{Building, FoundationBuilde import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap} -import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Vehicle} +import net.psforever.objects._ import net.psforever.packet.game.PlanetSideGUID import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} import org.specs2.mutable.Specification @@ -489,6 +489,60 @@ class ZonePopulationTest extends ActorTest { } } +class ZoneGroundTest extends ActorTest { + val item = AmmoBox(GlobalDefinitions.bullet_9mm) + item.GUID = PlanetSideGUID(10) + + "ZoneGroundActor" 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) + zone.Ground ! Zone.DropItemOnGround(item, Vector3(1.1f, 2.2f, 3.3f), Vector3(4.4f, 5.5f, 6.6f)) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.EquipmentOnGround == List(item)) + assert(item.Position == Vector3(1.1f, 2.2f, 3.3f)) + assert(item.Orientation == Vector3(4.4f, 5.5f, 6.6f)) + } + + "get item from ground (success)" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "get-item-test-good") ! "!" + receiveOne(Duration.create(200, "ms")) //consume + zone.Ground ! Zone.DropItemOnGround(item, Vector3.Zero, Vector3.Zero) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.EquipmentOnGround == List(item)) + zone.Ground ! Zone.GetItemOnGround(player, 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) + } + + "get item from ground (failure)" in { + val zone = new Zone("test", new ZoneMap(""), 0) + val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "get-item-test-fail") ! "!" + receiveOne(Duration.create(200, "ms")) //consume + zone.Ground ! Zone.DropItemOnGround(item, Vector3.Zero, Vector3.Zero) + expectNoMsg(Duration.create(100, "ms")) + + assert(zone.EquipmentOnGround == List(item)) + zone.Ground ! Zone.GetItemOnGround(player, PlanetSideGUID(11)) //wrong guid + expectNoMsg(Duration.create(500, "ms")) + } + } +} + object ZoneTest { class ZoneInitActor(zone : Zone) extends Actor { def receive : Receive = { diff --git a/pslogin/src/main/scala/Maps.scala b/pslogin/src/main/scala/Maps.scala index 9b64ad92..ea3cfc2d 100644 --- a/pslogin/src/main/scala/Maps.scala +++ b/pslogin/src/main/scala/Maps.scala @@ -37,21 +37,21 @@ object Maps { LocalObject(371, Door.Constructor) //courtyard LocalObject(372, Door.Constructor) //courtyard LocalObject(373, Door.Constructor) //courtyard - LocalObject(375, Door.Constructor) //2nd level door - LocalObject(376, Door.Constructor) //2nd level door + LocalObject(375, Door.Constructor(Vector3(3924.0f, 4231.2656f, 271.82812f), Vector3(0, 0, 180))) //2nd level door, south + LocalObject(376, Door.Constructor(Vector3(3924.0f, 4240.2656f, 271.82812f), Vector3(0, 0, 0))) //2nd level door, north LocalObject(383, Door.Constructor) //courtyard - LocalObject(384, Door.Constructor) //3rd floor door + LocalObject(384, Door.Constructor(Vector3(3939.6328f, 4232.547f, 279.26562f), Vector3(0, 0, 270))) //3rd floor door LocalObject(385, Door.Constructor) //courtyard - LocalObject(387, Door.Constructor) //2nd level door + LocalObject(387, Door.Constructor(Vector3(3951.9531f, 4260.008f, 271.82812f), Vector3(0, 0, 270))) //2nd level door, stairwell LocalObject(391, Door.Constructor) //courtyard - LocalObject(393, Door.Constructor) //air term building, upstairs door - LocalObject(394, Door.Constructor) //air term building, f.door + LocalObject(393, Door.Constructor(Vector3(3997.8984f, 4344.3203f, 271.8125f), Vector3(0, 0, 0))) //air term building, upstairs door + LocalObject(394, Door.Constructor(Vector3(3999.9766f, 4314.3203f, 266.82812f), Vector3(0, 0, 270))) //air term building, f.door LocalObject(396, Door.Constructor) //courtyard LocalObject(398, Door.Constructor) //courtyard LocalObject(399, Door.Constructor) //courtyard LocalObject(402, Door.Constructor) //courtyard LocalObject(403, Door.Constructor) //courtyard - LocalObject(404, Door.Constructor) //b.door + LocalObject(404, Door.Constructor(Vector3(4060.0078f, 4319.9766f, 266.8125f), Vector3(0, 0, 0))) //b.door LocalObject(603, Door.Constructor) LocalObject(604, Door.Constructor) LocalObject(605, Door.Constructor) @@ -61,19 +61,33 @@ object Maps { LocalObject(611, Door.Constructor) LocalObject(614, Door.Constructor) LocalObject(619, Door.Constructor) - LocalObject(620, Door.Constructor) //generator room door + LocalObject(620, Door.Constructor(Vector3(3983.9531f, 4299.992f, 249.29688f), Vector3(0, 0, 90))) //generator room door LocalObject(621, Door.Constructor) - LocalObject(622, Door.Constructor) //spawn room door - LocalObject(623, Door.Constructor) //spawn room door - LocalObject(630, Door.Constructor) //spawn room door + LocalObject(622, Door.Constructor(Vector3(3988.0078f, 4248.0156f, 256.82812f), Vector3(0, 0, 180))) //spawn room door + LocalObject(623, Door.Constructor(Vector3(3988.0078f, 4271.9766f, 256.79688f), Vector3(0, 0, 0))) //spawn room door + LocalObject(630, Door.Constructor(Vector3(4000.0078f, 4252.0f, 249.29688f), Vector3(0, 0, 270))) //spawn room door LocalObject(631, Door.Constructor) //spawn room door, kitchen LocalObject(634, Door.Constructor) //air term building, interior - LocalObject(638, Door.Constructor) //cc door - LocalObject(642, Door.Constructor) //cc door, interior - LocalObject(643, Door.Constructor) //cc door - LocalObject(645, Door.Constructor) //b.door interior - LocalObject(646, Door.Constructor) //b.door interior - LocalObject(715, Door.Constructor) //f.door + LocalObject(638, Door.Constructor(Vector3(4016.0078f, 4212.008f, 249.29688f), Vector3(0, 0, 270))) //cc door + LocalObject(642, Door.Constructor(Vector3(4023.9844f, 4212.008f, 249.32812f), Vector3(0, 0, 90))) //cc door, interior + LocalObject(643, Door.Constructor) //cc door, exterior + LocalObject(645, Door.Constructor) //b.door, interior + LocalObject(646, Door.Constructor) //b.door, interior + LocalObject(715, Door.Constructor(Vector3(3961.5938f ,4235.8125f, 266.84375f), Vector3(0, 0, 90))) //f.door + LocalObject(751, IFFLock.Constructor) + LocalObject(860, IFFLock.Constructor) + LocalObject(863, IFFLock.Constructor) + LocalObject(866, IFFLock.Constructor) + LocalObject(868, IFFLock.Constructor) + LocalObject(873, IFFLock.Constructor) + LocalObject(874, IFFLock.Constructor) + LocalObject(875, IFFLock.Constructor) + LocalObject(876, IFFLock.Constructor) + LocalObject(878, IFFLock.Constructor) + LocalObject(879, IFFLock.Constructor) + LocalObject(882, IFFLock.Constructor) + LocalObject(884, IFFLock.Constructor) + LocalObject(885, IFFLock.Constructor) LocalObject(1177, Locker.Constructor) LocalObject(1178, Locker.Constructor) LocalObject(1179, Locker.Constructor) @@ -111,7 +125,7 @@ object Maps { LocalObject(2324, Door.Constructor) //spawn tube door LocalObject(2419, Terminal.Constructor(ground_vehicle_terminal)) LocalObject(500, - VehicleSpawnPad.Constructor(Vector3(3962.0f, 4334.0f, 268.0f), Vector3(0f, 0f, 180.0f)) + VehicleSpawnPad.Constructor(Vector3(3962.0f, 4334.0f, 267.75f), Vector3(0f, 0f, 180.0f)) ) //TODO guid not correct LocalObject(224, Terminal.Constructor(dropship_vehicle_terminal)) LocalObject(501, @@ -160,6 +174,20 @@ object Maps { ObjectToBuilding(645, 2) ObjectToBuilding(646, 2) ObjectToBuilding(715, 2) + ObjectToBuilding(751, 2) + ObjectToBuilding(860, 2) + ObjectToBuilding(863, 2) + ObjectToBuilding(866, 2) + ObjectToBuilding(868, 2) + ObjectToBuilding(873, 2) + ObjectToBuilding(874, 2) + ObjectToBuilding(875, 2) + ObjectToBuilding(876, 2) + ObjectToBuilding(878, 2) + ObjectToBuilding(879, 2) + ObjectToBuilding(882, 2) + ObjectToBuilding(884, 2) + ObjectToBuilding(885, 2) ObjectToBuilding(1177, 2) ObjectToBuilding(1178, 2) ObjectToBuilding(1179, 2) @@ -198,6 +226,20 @@ object Maps { ObjectToBuilding(2419, 2) ObjectToBuilding(500, 2) ObjectToBuilding(501, 2) + DoorToLock(375, 863) + DoorToLock(376, 860) + DoorToLock(384, 866) + DoorToLock(387, 868) + DoorToLock(393, 876) + DoorToLock(394, 879) + DoorToLock(404, 885) + DoorToLock(620, 873) + DoorToLock(622, 876) + DoorToLock(623, 874) + DoorToLock(630, 878) + DoorToLock(638, 882) + DoorToLock(642, 884) + DoorToLock(715, 751) TerminalToSpawnPad(224, 501) TerminalToSpawnPad(2419, 500) } @@ -285,7 +327,7 @@ object Maps { def Building49() : Unit = { //North Akna Air Tower - LocalBuilding(49, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(3864.2266f, 4518.0234f, 0)))) + LocalBuilding(49, FoundationBuilder(Building.Structure(StructureType.Tower, Vector3(4358.3203f, 3989.5625f, 0)))) LocalObject(430, Door.Constructor(Vector3(4366.0156f, 3981.9922f, 237.96875f), Vector3(0f, 0f, 180f))) //s1 LocalObject(431, Door.Constructor(Vector3(4366.0156f, 3981.9922f, 257.89062f), Vector3(0f, 0f, 180f))) //s2 LocalObject(432, Door.Constructor(Vector3(4366.0156f, 3997.9297f, 237.96875f), Vector3(0f, 0f, 0f))) //n1 @@ -328,6 +370,8 @@ object Maps { ObjectToBuilding(1591, 49) ObjectToBuilding(1592, 49) ObjectToBuilding(1593, 49) + ObjectToBuilding(2156, 49) + ObjectToBuilding(2157, 49) ObjectToBuilding(2333, 49) ObjectToBuilding(2334, 49) DoorToLock(430, 906) @@ -359,10 +403,12 @@ object Maps { Building77() def Building1() : Unit = { + //warpgate? LocalBuilding(1, FoundationBuilder(WarpGate.Structure)) } def Building3() : Unit = { + //warpgate? LocalBuilding(3, FoundationBuilder(WarpGate.Structure)) } @@ -373,7 +419,8 @@ object Maps { // TerminalToInterface(520, 1081) def Building2() : Unit = { - LocalBuilding(2, FoundationBuilder(Building.Structure(StructureType.Building))) //HART building C + //HART building C + LocalBuilding(2, FoundationBuilder(Building.Structure(StructureType.Building))) LocalObject(186, Terminal.Constructor(cert_terminal)) LocalObject(187, Terminal.Constructor(cert_terminal)) LocalObject(188, Terminal.Constructor(cert_terminal)) @@ -471,7 +518,8 @@ object Maps { } def Building29() : Unit = { - LocalBuilding(29, FoundationBuilder(Building.Structure(StructureType.Tower))) //South Villa Gun Tower + //South Villa Gun Tower + LocalBuilding(29, FoundationBuilder(Building.Structure(StructureType.Tower))) LocalObject(330, Door.Constructor(Vector3(3979.9219f, 2592.0547f, 91.140625f), Vector3(0, 0, 180))) LocalObject(331, Door.Constructor(Vector3(3979.9219f, 2592.0547f, 111.140625f), Vector3(0, 0, 180))) LocalObject(332, Door.Constructor(Vector3(3979.9688f, 2608.0625f, 91.140625f), Vector3(0, 0, 0))) @@ -495,7 +543,8 @@ object Maps { } def Building42() : Unit = { - LocalBuilding(42, FoundationBuilder(Building.Structure(StructureType.Building, Vector3(1, 0, 0)))) //spawn building south of HART C + //spawn building south of HART C + LocalBuilding(42, FoundationBuilder(Building.Structure(StructureType.Building, Vector3(1, 0, 0)))) LocalObject(258, Door.Constructor) //spawn tube door LocalObject(259, Door.Constructor) //spawn tube door LocalObject(260, Door.Constructor) //spawn tube door @@ -510,12 +559,12 @@ object Maps { LocalObject(433, Door.Constructor) //vr door LocalObject(434, Door.Constructor) //vr door LocalObject(435, Door.Constructor) //vr door - LocalObject(744, SpawnTube.Constructor(Vector3(3684.336f, 2709.0469f, 91.859375f), Vector3(0, 0, 180))) - LocalObject(745, SpawnTube.Constructor(Vector3(3684.336f, 2713.2344f, 91.859375f), Vector3(0, 0, 0))) - LocalObject(746, SpawnTube.Constructor(Vector3(3691.0703f, 2709.0469f, 91.859375f), Vector3(0, 0, 180))) - LocalObject(747, SpawnTube.Constructor(Vector3(3691.0703f, 2713.2344f, 91.859375f), Vector3(0, 0, 0))) - LocalObject(748, SpawnTube.Constructor(Vector3(3697.711f, 2709.0469f, 91.859375f), Vector3(0, 0, 180))) - LocalObject(749, SpawnTube.Constructor(Vector3(3697.711f, 2713.2344f, 91.859375f), Vector3(0, 0, 0))) + LocalObject(744, SpawnTube.Constructor(Vector3(3684.336f, 2709.0469f, 91.9f), Vector3(0, 0, 180))) + LocalObject(745, SpawnTube.Constructor(Vector3(3684.336f, 2713.75f, 91.9f), Vector3(0, 0, 0))) + LocalObject(746, SpawnTube.Constructor(Vector3(3690.9062f, 2708.4219f, 91.9f), Vector3(0, 0, 180))) + LocalObject(747, SpawnTube.Constructor(Vector3(3691.0703f, 2713.8672f, 91.9f), Vector3(0, 0, 0))) + LocalObject(748, SpawnTube.Constructor(Vector3(3697.664f, 2708.3984f, 91.9f), Vector3(0, 0, 180))) + LocalObject(749, SpawnTube.Constructor(Vector3(3697.711f, 2713.2344f, 91.9f), Vector3(0, 0, 0))) LocalObject(852, Terminal.Constructor(order_terminal)) //s. wall LocalObject(853, Terminal.Constructor(order_terminal)) //n. wall LocalObject(854, Terminal.Constructor(order_terminal)) //s. wall @@ -551,7 +600,8 @@ object Maps { } def Building51() : Unit = { - LocalBuilding(51, FoundationBuilder(Building.Structure(StructureType.Platform))) //air terminal west of HART C + //air terminal west of HART C + LocalBuilding(51, FoundationBuilder(Building.Structure(StructureType.Platform))) LocalObject(304, Terminal.Constructor(dropship_vehicle_terminal)) LocalObject(292, VehicleSpawnPad.Constructor(Vector3(3508.9844f, 2895.961f, 92.296875f), Vector3(0f, 0f, 270.0f)) @@ -562,7 +612,8 @@ object Maps { } def Building77() : Unit = { - LocalBuilding(77, FoundationBuilder(Building.Structure(StructureType.Platform))) //ground terminal west of HART C + //ground terminal west of HART C + LocalBuilding(77, FoundationBuilder(Building.Structure(StructureType.Platform))) LocalObject(1063, Terminal.Constructor(ground_vehicle_terminal)) LocalObject(706, VehicleSpawnPad.Constructor(Vector3(3506.0f, 2820.0f, 92.0f), Vector3(0f, 0f, 270.0f)) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 6d24c17c..ddf16df9 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -12,6 +12,7 @@ import MDCContextAware.Implicits._ import net.psforever.objects.GlobalDefinitions._ import services.ServiceManager.Lookup import net.psforever.objects._ +import net.psforever.objects.definition.ToolDefinition import net.psforever.objects.definition.converter.CorpseConverter import net.psforever.objects.equipment._ import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} @@ -101,8 +102,9 @@ class WorldSessionActor extends Actor with MDCContextAware { } continent.Population ! Zone.Population.Release(avatar) continent.Population ! Zone.Population.Leave(avatar) + player.Position = Vector3.Zero //save character before doing this avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectDelete(player_guid, player_guid)) - taskResolver ! GUIDTask. UnregisterAvatar(player)(continent.GUID) + taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) //TODO normally, the actual player avatar persists a minute or so after the user disconnects } } @@ -1026,8 +1028,6 @@ class WorldSessionActor extends Actor with MDCContextAware { tplayer.Position = spawn_tube.Position tplayer.Orientation = spawn_tube.Orientation - import scala.concurrent.duration._ - import scala.concurrent.ExecutionContext.Implicits.global val (target, msg) : (ActorRef, Any) = if(sameZone) { if(backpack) { //respawning from unregistered player @@ -1052,6 +1052,8 @@ 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 context.system.scheduler.scheduleOnce(respawnTime seconds, target, msg) case Zone.Lattice.NoValidSpawnPoint(zone_number, None) => @@ -1103,6 +1105,7 @@ class WorldSessionActor extends Actor with MDCContextAware { player = tplayer val guid = tplayer.GUID sendResponse(SetCurrentAvatarMessage(guid,0,0)) + sendResponse(PlayerStateShiftMessage(ShiftState(1, tplayer.Position, tplayer.Orientation.z))) if(spectator) { sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None)) } @@ -1300,8 +1303,8 @@ class WorldSessionActor extends Actor with MDCContextAware { player = new Player(avatar) //player.Position = Vector3(3561.0f, 2854.0f, 90.859375f) //home3, HART C //player.Orientation = Vector3(0f, 0f, 90f) - player.Position = Vector3(4266.0547f, 4046.4844f, 250.23438f) //z6, Akna.tower - player.Orientation = Vector3(0f, 0f, 320f) + player.Position = Vector3(4262.211f ,4067.0625f ,262.35938f) //z6, Akna.tower + player.Orientation = Vector3(0f, 0f, 132.1875f) // player.ExoSuit = ExoSuitType.MAX //TODO strange issue; divide number above by 10 when uncommenting player.Slot(0).Equipment = SimpleItem(remote_electronics_kit) //Tool(GlobalDefinitions.StandardPistol(player.Faction)) player.Slot(2).Equipment = Tool(punisher) //suppressor @@ -1502,11 +1505,17 @@ class WorldSessionActor extends Actor with MDCContextAware { player.VehicleSeated match { case None => continent.Population ! Zone.Corpse.Add(player) //TODO move back out of this match case when changing below issue - val knife = player.Slot(4).Equipment.get - player.Slot(4).Equipment = None - taskResolver ! RemoveEquipmentFromSlot(player, knife, 4) - TurnPlayerIntoCorpse(player) - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent)) + FriskCorpse(player) + if(!WellLootedCorpse(player)) { + TurnPlayerIntoCorpse(player) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent)) + } + else { //no items in inventory; leave no corpse + val player_guid = player.GUID + sendResponse(ObjectDeleteMessage(player_guid, 0)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) + taskResolver ! GUIDTask.UnregisterPlayer(player)(continent.GUID) + } case Some(_) => //TODO we do not want to delete the player if he is seated in a vehicle when releasing @@ -1514,8 +1523,8 @@ class WorldSessionActor extends Actor with MDCContextAware { val player_guid = player.GUID sendResponse(ObjectDeleteMessage(player_guid, 0)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) - self ! PacketCoding.CreateGamePacket(0, DismountVehicleMsg(player_guid, 0, true)) //let vehicle try to clean up its fields taskResolver ! GUIDTask.UnregisterPlayer(player)(continent.GUID) + self ! PacketCoding.CreateGamePacket(0, DismountVehicleMsg(player_guid, 0, true)) //let vehicle try to clean up its fields //sendResponse(ObjectDetachMessage(vehicle_guid, player.GUID, Vector3.Zero, 0, 0, 0)) //sendResponse(PlayerStateShiftMessage(ShiftState(1, Vector3.Zero, 0))) } @@ -1980,9 +1989,9 @@ class WorldSessionActor extends Actor with MDCContextAware { vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item_guid)) vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player_guid, source_guid, index, item2)) //TODO visible slot verification, in the case of BFR arms - case (_ : Player) => + case (obj : Player) => if(source.VisibleSlots.contains(index)) { - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(source_guid, index, item2)) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(source_guid, index, item2)) } case _ => ; //TODO something? @@ -2168,6 +2177,8 @@ class WorldSessionActor extends Actor with MDCContextAware { obj.AccessingTrunk = None UnAccessContents(obj) } + case Some(obj : Player) => + TryDisposeOfLootedCorpse(obj) case _ =>; } @@ -2772,13 +2783,19 @@ class WorldSessionActor extends Actor with MDCContextAware { ) } + /** + * Before calling `Interstellar.GetWorld` to change zones, perform the following task (which can be a nesting of subtasks). + * @param priorTask the tasks to perform + * @param zoneId the zone to load afterwards + * @return a `TaskResolver.GiveTask` message + */ def TaskBeforeZoneChange(priorTask : TaskResolver.GiveTask, zoneId : String) : TaskResolver.GiveTask = { TaskResolver.GiveTask( new Task() { private val localService = galaxy private val localMsg = InterstellarCluster.GetWorld(zoneId) - override def isComplete : Task.Resolution.Value = Task.Resolution.Success + override def isComplete : Task.Resolution.Value = priorTask.task.isComplete def Execute(resolver : ActorRef) : Unit = { localService ! localMsg @@ -3493,6 +3510,31 @@ class WorldSessionActor extends Actor with MDCContextAware { obj } + /** + * Remove items from a deceased player that is not expected to be found on a corpse. + * Most all players have their melee slot knife (which can not be un-equipped normally) removed. + * MAX's have their primary weapon in the designated slot removed. + * @param obj the player to be turned into a corpse + */ + def FriskCorpse(obj : Player) : Unit = { + if(obj.isBackpack) { + obj.Slot(4).Equipment match { + case None => ; + case Some(knife) => + obj.Slot(4).Equipment = None + taskResolver ! RemoveEquipmentFromSlot(obj, knife, 4) + } + obj.Slot(0).Equipment match { + case Some(arms : Tool) => + if(GlobalDefinitions.isMaxArms(arms.Definition)) { + obj.Slot(0).Equipment = None + taskResolver ! RemoveEquipmentFromSlot(obj, arms, 0) + } + case _ => ; + } + } + } + /** * Creates a player that has the characteristics of a corpse. * To the game, that is a backpack (or some pastry, festive graphical modification allowing). @@ -3505,6 +3547,34 @@ class WorldSessionActor extends Actor with MDCContextAware { ) } + /** + * If the corpse has been well-looted, it has no items in its primary holsters nor any items in its inventory. + * @param obj the corpse + * @return `true`, if the `obj` is actually a corpse and has no objects in its holsters or backpack; + * `false`, otherwise + */ + def WellLootedCorpse(obj : Player) : Boolean = { + obj.isBackpack && obj.Holsters().count(_.Equipment.nonEmpty) == 0 && obj.Inventory.Size == 0 + } + + /** + * If the corpse has been well-looted, remove it from the ground. + * @param obj the corpse + * @return `true`, if the `obj` is actually a corpse and has no objects in its holsters or backpack; + * `false`, otherwise + */ + 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 + } + else { + false + } + } + /** * Attempt to tranfer to the player's faction-specific sanctuary continent. * If the server thinks the player is already on his sanctuary continent, diff --git a/pslogin/src/main/scala/services/avatar/AvatarAction.scala b/pslogin/src/main/scala/services/avatar/AvatarAction.scala index 54c5725f..80d49828 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarAction.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarAction.scala @@ -26,7 +26,7 @@ object AvatarAction { 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) extends Action + final case class Release(player : Player, zone : Zone, time : Option[Long] = None) extends Action final case class Reload(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class WeaponDryFire(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action diff --git a/pslogin/src/main/scala/services/avatar/AvatarService.scala b/pslogin/src/main/scala/services/avatar/AvatarService.scala index 2d8f0f8c..2978f1a1 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarService.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarService.scala @@ -2,11 +2,11 @@ package services.avatar import akka.actor.{Actor, ActorRef, Props} -import services.avatar.support.UndertakerActor +import services.avatar.support.CorpseRemovalActor import services.{GenericEventBus, Service} class AvatarService extends Actor { - private val undertaker : ActorRef = context.actorOf(Props[UndertakerActor], "corpse-removal-agent") + private val undertaker : ActorRef = context.actorOf(Props[CorpseRemovalActor], "corpse-removal-agent") undertaker ! "startup" private [this] val log = org.log4s.getLogger @@ -90,8 +90,11 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarResponse.PlayerState(msg, spectator, weapon)) ) - case AvatarAction.Release(player, zone) => - undertaker ! UndertakerActor.AddCorpse(player, zone) + case AvatarAction.Release(player, zone, time) => + undertaker ! (time match { + case Some(t) => CorpseRemovalActor.AddCorpse(player, zone, t) + case None => CorpseRemovalActor.AddCorpse(player, zone) + }) AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player.GUID, AvatarResponse.Release(player)) ) @@ -103,9 +106,14 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.WeaponDryFire(weapon_guid)) ) + case _ => ; } + //message to Undertaker + case AvatarServiceMessage.RemoveSpecificCorpse(corpses) => + undertaker ! AvatarServiceMessage.RemoveSpecificCorpse( corpses.filter(corpse => {corpse.HasGUID && corpse.isBackpack}) ) + /* case AvatarService.PlayerStateMessage(msg) => // log.info(s"NEW: ${m}") diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala index e3e35cd3..04b96a90 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala @@ -1,4 +1,10 @@ // 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]) +} diff --git a/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala b/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala new file mode 100644 index 00000000..ace1fc92 --- /dev/null +++ b/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala @@ -0,0 +1,199 @@ +// 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 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() + + 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 + } + + def receive : Receive = { + case "startup" => + ServiceManager.serviceManager ! Lookup("taskResolver") //ask for a resolver to deal with the GUID system + + case ServiceManager.LookupResult("taskResolver", endpoint) => + //Cart Master: Bring out your dead! + taskResolver = endpoint + context.become(Processing) + + case _ => ; + } + + def Processing : Receive = { + case CorpseRemovalActor.AddCorpse(corpse, zone, time) => + if(corpse.isBackpack) { + 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 + 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) => + BurialTask(corpses(index)) + corpses = corpses.take(index) ++ corpses.drop(index+1) + } + } + else { + log.debug(s"multiple target corpses submitted for early cleanup: $targets") + //cumbersome partition + //a - find targets from corpses + (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) + corpses = (for { + a <- targets + 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) + } + RetimeFirstTask() + } + + case CorpseRemovalActor.Dispose() => + burial.cancel + val now : Long = System.nanoTime + val (buried, rotting) = corpses.partition(entry => { now - entry.time >= entry.timeAlive }) + corpses = rotting + buried.foreach { BurialTask } + RetimeFirstTask() + + case CorpseRemovalActor.FailureToWork(target, zone, ex) => + //Cart Master: Oh, I can't take him like that. It's against regulations. + log.error(s"corpse $target from $zone not properly unregistered - $ex") + + case _ => ; + } + + def RetimeFirstTask(now : Long = System.nanoTime) : Unit = { + //Cart Master: Thursday. + burial.cancel + if(corpses.nonEmpty) { + val short_timeout : FiniteDuration = math.max(1, corpses.head.timeAlive - (now - corpses.head.time)) nanoseconds + import scala.concurrent.ExecutionContext.Implicits.global + burial = context.system.scheduler.scheduleOnce(short_timeout, self, CorpseRemovalActor.Dispose()) + } + } + + 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) + } + + def BurialTask(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()) + + final case class FailureToWork(corpse : Player, zone : Zone, ex : Throwable) + + final case class Dispose() + + /** + * 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 == player && corpse.Continent.equals(player.Continent) && corpse.GUID == player.GUID) { + Some(index) + } + else { + recursiveFindCorpse(iter, player, index + 1) + } + } + } +} diff --git a/pslogin/src/main/scala/services/avatar/support/UndertakerActor.scala b/pslogin/src/main/scala/services/avatar/support/UndertakerActor.scala deleted file mode 100644 index 83788a17..00000000 --- a/pslogin/src/main/scala/services/avatar/support/UndertakerActor.scala +++ /dev/null @@ -1,145 +0,0 @@ -// 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 services.{Service, ServiceManager} -import services.ServiceManager.Lookup -import services.avatar.{AvatarAction, AvatarServiceMessage} - -import scala.annotation.tailrec -import scala.concurrent.duration._ - -class UndertakerActor extends Actor { - private var burial : Cancellable = DefaultCancellable.obj - - private var corpses : List[UndertakerActor.Entry] = List() - - private var taskResolver : ActorRef = Actor.noSender - - private[this] val log = org.log4s.getLogger("Cart Master") - - override def postStop() = { - corpses.foreach { BurialTask } - } - - def receive : Receive = { - case "startup" => - 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 _ => ; - } - - def Processing : Receive = { - case UndertakerActor.AddCorpse(corpse, zone, time) => - if(corpse.isBackpack) { - corpses = corpses :+ UndertakerActor.Entry(corpse, zone, time) - if(corpses.size == 1) { //we were the only entry so the event must be started from scratch - import scala.concurrent.ExecutionContext.Implicits.global - burial = context.system.scheduler.scheduleOnce(UndertakerActor.timeout, self, UndertakerActor.Dispose()) - } - } - else { - log.warn(s"he's not dead yet - $corpse") - } - - case UndertakerActor.Dispose() => - burial.cancel - val now : Long = System.nanoTime - val (buried, rotting) = PartitionEntries(corpses, now) - corpses = rotting - buried.foreach { BurialTask } - if(rotting.nonEmpty) { - val short_timeout : FiniteDuration = math.max(1, UndertakerActor.timeout_time - (now - rotting.head.time)) nanoseconds - import scala.concurrent.ExecutionContext.Implicits.global - burial = context.system.scheduler.scheduleOnce(short_timeout, self, UndertakerActor.Dispose()) - } - - case UndertakerActor.FailureToWork(target, zone, ex) => - log.error(s"$target failed to be properly cleaned up from $zone - $ex") - - case _ => ; - } - - def BurialTask(entry : UndertakerActor.Entry) : Unit = { - val target = entry.corpse - val zone = entry.zone - entry.zone.Population ! Zone.Corpse.Remove(target) - context.parent ! AvatarServiceMessage(zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, target.GUID)) //call up to the main event system - taskResolver ! BurialTask(target, zone) - } - - def BurialTask(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 = Task.Resolution.Success - - def Execute(resolver : ActorRef) : Unit = { - resolver ! scala.util.Success(this) - } - - override def onFailure(ex : Throwable): Unit = { - localAnnounce ! UndertakerActor.FailureToWork(localCorpse, localZone, ex) - } - }, List(GUIDTask.UnregisterPlayer(corpse)(zone.GUID)) - ) - } - - private def PartitionEntries(list : List[UndertakerActor.Entry], now : Long) : (List[UndertakerActor.Entry], List[UndertakerActor.Entry]) = { - val n : Int = recursivePartitionEntries(list.iterator, now, UndertakerActor.timeout_time) - (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[UndertakerActor.Entry], now : Long, duration : Long, index : Int = 0) : Int = { - if(!iter.hasNext) { - index - } - else { - val entry = iter.next() - if(now - entry.time >= duration) { - recursivePartitionEntries(iter, now, duration, index + 1) - } - else { - index - } - } - } -} - -object UndertakerActor { - /** A `Long` for calculation simplicity */ - private final val timeout_time : Long = 180000000000L //3 min (180s) - /** A `FiniteDuration` for `Executor` simplicity */ - private final val timeout : FiniteDuration = timeout_time nanoseconds - - final case class AddCorpse(corpse : Player, zone : Zone, time : Long = System.nanoTime()) - - final case class Entry(corpse : Player, zone : Zone, time : Long = System.nanoTime()) - - final case class FailureToWork(corpse : Player, zone : Zone, ex : Throwable) - - final case class Dispose() - - //TODO design mass disposal cases -} diff --git a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala index adc9668a..2aaefff4 100644 --- a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala +++ b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala @@ -7,6 +7,7 @@ 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} @@ -80,6 +81,7 @@ class DeconstructionActor extends Actor { 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.DespawnVehicle(vehicle) context.parent ! DeconstructionActor.DeleteVehicle(vehicle.GUID, zone.Id) //call up to the main event system context.parent ! VehicleServiceMessage.RevokeActorControl(vehicle) //call up to a sibling manager diff --git a/pslogin/src/test/scala/AvatarServiceTest.scala b/pslogin/src/test/scala/AvatarServiceTest.scala index 94d5a147..deda78b1 100644 --- a/pslogin/src/test/scala/AvatarServiceTest.scala +++ b/pslogin/src/test/scala/AvatarServiceTest.scala @@ -1,15 +1,21 @@ // Copyright (c) 2017 PSForever import akka.actor.Props +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.types.{CharacterGender, ExoSuitType, PlanetSideEmpire, Vector3} -import services.Service +import services.{Service, ServiceManager} import services.avatar._ +import scala.concurrent.duration._ + class AvatarService1Test extends ActorTest { "AvatarService" should { "construct" in { - system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) assert(true) } } @@ -18,7 +24,8 @@ class AvatarService1Test extends ActorTest { class AvatarService2Test extends ActorTest { "AvatarService" should { "subscribe" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") assert(true) } @@ -27,8 +34,9 @@ class AvatarService2Test extends ActorTest { class AvatarService3Test extends ActorTest { "AvatarService" should { + ServiceManager.boot(system) "subscribe to a specific channel" in { - val service = system.actorOf(Props[AvatarService], "service") + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! Service.Leave() assert(true) @@ -39,7 +47,8 @@ class AvatarService3Test extends ActorTest { class AvatarService4Test extends ActorTest { "AvatarService" should { "subscribe" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! Service.LeaveAll() assert(true) @@ -50,7 +59,8 @@ class AvatarService4Test extends ActorTest { class AvatarService5Test extends ActorTest { "AvatarService" should { "pass an unhandled message" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! "hello" expectNoMsg() @@ -61,7 +71,8 @@ class AvatarService5Test extends ActorTest { class ArmorChangedTest extends ActorTest { "AvatarService" should { "pass ArmorChanged" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ArmorChanged(PlanetSideGUID(10), ExoSuitType.Reinforced, 0)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ArmorChanged(ExoSuitType.Reinforced, 0))) @@ -72,7 +83,8 @@ class ArmorChangedTest extends ActorTest { class ConcealPlayerTest extends ActorTest { "AvatarService" should { "pass ConcealPlayer" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ConcealPlayer(PlanetSideGUID(10))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ConcealPlayer())) @@ -85,7 +97,8 @@ class EquipmentInHandTest extends ActorTest { "AvatarService" should { "pass EquipmentInHand" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.EquipmentInHand(PlanetSideGUID(10), 2, tool)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentInHand(2, tool))) @@ -101,7 +114,8 @@ class EquipmentOnGroundTest extends ActorTest { "AvatarService" should { "pass EquipmentOnGround" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) 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))) @@ -117,7 +131,8 @@ class LoadPlayerTest extends ActorTest { "AvatarService" should { "pass LoadPlayer" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.LoadPlayer(PlanetSideGUID(10), pdata)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.LoadPlayer(pdata))) @@ -128,7 +143,8 @@ class LoadPlayerTest extends ActorTest { class ObjectDeleteTest extends ActorTest { "AvatarService" should { "pass ObjectDelete" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ObjectDelete(PlanetSideGUID(10), PlanetSideGUID(11))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectDelete(PlanetSideGUID(11), 0))) @@ -142,7 +158,8 @@ class ObjectDeleteTest extends ActorTest { class ObjectHeldTest extends ActorTest { "AvatarService" should { "pass ObjectHeld" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ObjectHeld(PlanetSideGUID(10), 1)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectHeld(1))) @@ -153,7 +170,8 @@ class ObjectHeldTest extends ActorTest { class PlanetsideAttributeTest extends ActorTest { "AvatarService" should { "pass PlanetsideAttribute" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.PlanetsideAttribute(PlanetSideGUID(10), 5, 1200L)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.PlanetsideAttribute(5, 1200L))) @@ -166,7 +184,8 @@ class PlayerStateTest extends ActorTest { "AvatarService" should { "pass PlayerState" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.PlayerState(PlanetSideGUID(10), msg, false, false)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.PlayerState(msg, false, false))) @@ -177,7 +196,8 @@ class PlayerStateTest extends ActorTest { class ReloadTest extends ActorTest { "AvatarService" should { "pass Reload" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.Reload(PlanetSideGUID(10), PlanetSideGUID(40))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.Reload(PlanetSideGUID(40)))) @@ -191,7 +211,8 @@ class ChangeAmmoTest extends ActorTest { "AvatarService" should { "pass ChangeAmmo" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ChangeAmmo(PlanetSideGUID(10), PlanetSideGUID(40), 0, PlanetSideGUID(40), ammoDef.ObjectId, PlanetSideGUID(41), ammoDef.Packet.ConstructorData(ammoBox).get)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ChangeAmmo(PlanetSideGUID(40), 0, PlanetSideGUID(40), ammoDef.ObjectId, PlanetSideGUID(41), ammoDef.Packet.ConstructorData(ammoBox).get))) @@ -205,7 +226,8 @@ class ChangeFireModeTest extends ActorTest { "AvatarService" should { "pass ChangeFireMode" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ChangeFireMode(PlanetSideGUID(10), PlanetSideGUID(40), 0)) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ChangeFireMode(PlanetSideGUID(40), 0))) @@ -216,7 +238,8 @@ class ChangeFireModeTest extends ActorTest { class ChangeFireStateStartTest extends ActorTest { "AvatarService" should { "pass ChangeFireState_Start" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ChangeFireState_Start(PlanetSideGUID(10), PlanetSideGUID(40))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ChangeFireState_Start(PlanetSideGUID(40)))) @@ -227,7 +250,8 @@ class ChangeFireStateStartTest extends ActorTest { class ChangeFireStateStopTest extends ActorTest { "AvatarService" should { "pass ChangeFireState_Stop" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.ChangeFireState_Stop(PlanetSideGUID(10), PlanetSideGUID(40))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ChangeFireState_Stop(PlanetSideGUID(40)))) @@ -238,7 +262,8 @@ class ChangeFireStateStopTest extends ActorTest { class WeaponDryFireTest extends ActorTest { "AvatarService" should { "pass WeaponDryFire" in { - val service = system.actorOf(Props[AvatarService], "service") + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.WeaponDryFire(PlanetSideGUID(10), PlanetSideGUID(40))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.WeaponDryFire(PlanetSideGUID(40)))) @@ -246,6 +271,177 @@ class WeaponDryFireTest extends ActorTest { } } -object AvatarServiceTest { - //decoy +/* +Preparation for these three Release tests is involved. +The ServiceManager must not only be set up correctly, but must be given a TaskResolver. +The AvatarService is started and that starts CorpseRemovalActor, an essential part of this test. +The CorpseRemovalActor needs that TaskResolver created by the ServiceManager; +but, another independent TaskResolver will be needed for manual parts of the test. +(The ServiceManager's TaskResolver can be "borrowed" but that requires writing code to intercept it.) +The Zone needs to be set up and initialized properly with a ZoneActor. +The ZoneActor builds the GUID Actor and the ZonePopulationActor. + +ALL of these Actors will talk to each other. +The lines of communication can short circuit if the next Actor does not have the correct information. +Putting Actor startup in the main class, outside of the body of the test, helps. +Frequent pauses to allow everything to sort their messages also helps. +Even with all this work, the tests have a high chance of failure just due to being asynchronous. + */ +class AvatarReleaseTest 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], "release-test-resolver") + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "release-test-zone") + zone.Actor ! Zone.Init() + val obj = Player(Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1)) + obj.Continent = "test" + obj.Release + + "AvatarService" should { + "pass Release" in { + expectNoMsg(100 milliseconds) //spacer + + service ! Service.Join("test") + taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) + assert(zone.Corpses.isEmpty) + zone.Population ! Zone.Corpse.Add(obj) + expectNoMsg(100 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 + + val reply1 = receiveOne(100 milliseconds) + assert(reply1.isInstanceOf[AvatarServiceResponse]) + val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] + assert(reply1msg.toChannel == "/test/Avatar") + assert(reply1msg.avatar_guid == guid) + assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release]) + assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj) + + val reply2 = receiveOne(2 seconds) + assert(reply2.isInstanceOf[AvatarServiceResponse]) + val reply2msg = reply2.asInstanceOf[AvatarServiceResponse] + assert(reply2msg.toChannel.equals("/test/Avatar")) + assert(reply2msg.avatar_guid == Service.defaultPlayerGUID) + assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) + assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) + + expectNoMsg(200 milliseconds) + assert(zone.Corpses.isEmpty) + assert(!obj.HasGUID) + } + } +} + +class AvatarReleaseEarly1Test 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], "release-test-resolver") + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "release-test-zone") + zone.Actor ! Zone.Init() + val obj = Player(Avatar("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1)) + obj.Continent = "test" + obj.Release + + "AvatarService" should { + "pass Release" in { + expectNoMsg(100 milliseconds) //spacer + + service ! Service.Join("test") + taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) + assert(zone.Corpses.isEmpty) + zone.Population ! Zone.Corpse.Add(obj) + expectNoMsg(100 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) + assert(reply1.isInstanceOf[AvatarServiceResponse]) + val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] + assert(reply1msg.toChannel == "/test/Avatar") + assert(reply1msg.avatar_guid == guid) + 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) + assert(reply2.isInstanceOf[AvatarServiceResponse]) + val reply2msg = reply2.asInstanceOf[AvatarServiceResponse] + assert(reply2msg.toChannel.equals("/test/Avatar")) + assert(reply2msg.avatar_guid == Service.defaultPlayerGUID) + assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) + assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) + + expectNoMsg(200 milliseconds) + assert(zone.Corpses.isEmpty) + assert(!obj.HasGUID) + } + } +} + +class AvatarReleaseEarly2Test 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], "release-test-resolver") + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "release-test-zone") + zone.Actor ! Zone.Init() + val objAlt = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 1, 1)) //necessary clutter + val obj = Player(Avatar("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1)) + obj.Continent = "test" + obj.Release + + "AvatarService" should { + "pass Release" in { + expectNoMsg(100 milliseconds) //spacer + + service ! Service.Join("test") + taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) + assert(zone.Corpses.isEmpty) + zone.Population ! Zone.Corpse.Add(obj) + expectNoMsg(100 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) + assert(reply1.isInstanceOf[AvatarServiceResponse]) + val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] + assert(reply1msg.toChannel == "/test/Avatar") + assert(reply1msg.avatar_guid == guid) + assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release]) + assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj) + + service ! AvatarServiceMessage.RemoveSpecificCorpse(List(objAlt, obj)) //IMPORTANT: TWO ENTRIES + val reply2 = receiveOne(100 milliseconds) + assert(reply2.isInstanceOf[AvatarServiceResponse]) + val reply2msg = reply2.asInstanceOf[AvatarServiceResponse] + assert(reply2msg.toChannel.equals("/test/Avatar")) + assert(reply2msg.avatar_guid == Service.defaultPlayerGUID) + assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) + assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) + + expectNoMsg(200 milliseconds) + assert(zone.Corpses.isEmpty) + assert(!obj.HasGUID) + } + } +} + +object AvatarServiceTest { + import java.util.concurrent.atomic.AtomicInteger + private val number = new AtomicInteger(1) + + def TestName : String = { + s"service${number.getAndIncrement()}" + } }