diff --git a/common/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala b/common/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala index 09000022..82c43bcc 100644 --- a/common/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala +++ b/common/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala @@ -20,4 +20,8 @@ object InventoryItem { def apply(obj : Equipment, start : Int) : InventoryItem = { new InventoryItem(obj, start) } + + def unapply(entry : InventoryItem) : Option[(Equipment, Int)] = { + Some((entry.obj, entry.start)) + } } diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index e5a212b9..72b3e1e3 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -273,14 +273,14 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(GenericObjectActionMessage(guid, 36)) } - case AvatarResponse.EquipmentInHand(slot, item) => + case AvatarResponse.EquipmentInHand(target, slot, item) => if(tplayer_guid != guid) { val definition = item.Definition sendResponse( ObjectCreateMessage( definition.ObjectId, item.GUID, - ObjectCreateMessageParent(guid, slot), + ObjectCreateMessageParent(target, slot), definition.Packet.ConstructorData(item).get ) ) @@ -369,6 +369,19 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ReloadMessage(item_guid, 1, 0)) } + case AvatarResponse.StowEquipment(target, slot, item) => + if(tplayer_guid != guid) { + val definition = item.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + item.GUID, + ObjectCreateMessageParent(target, slot), + definition.Packet.DetailedConstructorData(item).get + ) + ) + } + case AvatarResponse.WeaponDryFire(weapon_guid) => if(tplayer_guid != guid) { sendResponse(WeaponDryFireMessage(weapon_guid)) @@ -589,7 +602,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val player_guid : PlanetSideGUID = tplayer.GUID val obj_guid : PlanetSideGUID = obj.GUID log.info(s"MountVehicleMsg: $player_guid mounts $obj @ $seat_num") - //tplayer.VehicleSeated = Some(obj_guid) + PlayerActionsToCancel() sendResponse(PlanetsideAttributeMessage(obj_guid, 0, 1000L)) //health of mech sendResponse(ObjectAttachMessage(obj_guid, player_guid, seat_num)) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.MountVehicle(player_guid, obj_guid, seat_num)) @@ -599,7 +612,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val player_guid : PlanetSideGUID = tplayer.GUID log.info(s"MountVehicleMsg: $player_guid mounts $obj_guid @ $seat_num") vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(obj_guid) //clear all deconstruction timers - //tplayer.VehicleSeated = Some(obj_guid) + PlayerActionsToCancel() if(seat_num == 0) { //simplistic vehicle ownership management obj.Owner match { case Some(owner_guid) => @@ -739,7 +752,7 @@ class WorldSessionActor extends Actor with MDCContextAware { definition.Packet.DetailedConstructorData(obj).get ) ) - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, index, obj)) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, player.GUID, index, obj)) case None => ; } }) @@ -1296,7 +1309,7 @@ class WorldSessionActor extends Actor with MDCContextAware { ) ) if(tplayer.VisibleSlots.contains(slot)) { - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentInHand(player_guid, slot, item)) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentInHand(player_guid, player_guid, slot, item)) } case None => continent.Ground ! Zone.DropItemOnGround(item, item.Position, item.Orientation) //restore @@ -2027,8 +2040,8 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(obj : Equipment) => val findFunc : PlanetSideGameObject with Container => Option[(PlanetSideGameObject with Container, Option[Int])] = FindInLocalContainer(object_guid) - findFunc(player) - .orElse(findFunc(player.Locker)) + findFunc(player.Locker) + .orElse(findFunc(player)) .orElse(accessedContainer match { case Some(parent) => findFunc(parent) @@ -2056,100 +2069,159 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ObjectDeleteMessage(object_guid, 0)) log.info("ObjectDelete: " + msg) - case msg @ MoveItemMessage(item_guid, source_guid, destination_guid, dest, unk1) => + case msg @ MoveItemMessage(item_guid, source_guid, destination_guid, dest, _) => log.info(s"MoveItem: $msg") (continent.GUID(source_guid), continent.GUID(destination_guid), continent.GUID(item_guid)) match { case (Some(source : Container), Some(destination : Container), Some(item : Equipment)) => source.Find(item_guid) match { case Some(index) => val indexSlot = source.Slot(index) - val destSlot = destination.Slot(dest) - val destItem = destSlot.Equipment + val tile = item.Definition.Tile + val destinationCollisionTest = destination.Collisions(dest, tile.Width, tile.Height) + val destItemEntry = destinationCollisionTest match { + case Success(entry :: Nil) => + Some(entry) + case _ => + None + } if( { - val tile = item.Definition.Tile - destination.Collisions(dest, tile.Width, tile.Height) match { - case Success(Nil) => - destItem.isEmpty //no item swap; abort if encountering an unexpected item - case Success(entry :: Nil) => - destItem.contains(entry.obj) //one item to swap; abort if destination item is missing or is wrong - case Success(_) | scala.util.Failure(_) => + destinationCollisionTest match { + case Success(Nil) | Success(_ :: Nil) => + true //no item or one item to swap + case _ => false //abort when too many items at destination or other failure case } } && indexSlot.Equipment.contains(item)) { log.info(s"MoveItem: $item_guid moved from $source_guid @ $index to $destination_guid @ $dest") + val player_guid = player.GUID + val sourceIsNotDestination : Boolean = source != destination //if source is destination, OCDM style is not required + //remove item from source indexSlot.Equipment = None - destItem match { //do we have a swap item? - case Some(item2) => //yes, swap - destSlot.Equipment = None //remove item2 to make room for item - destSlot.Equipment = item + source match { + case obj : Vehicle => + vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item_guid)) + case obj : Player => + if(obj.isBackpack || source.VisibleSlots.contains(index)) { //corpse being looted, or item was in hands + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, item_guid)) + } + case _ => ; + } + + destItemEntry match { //do we have a swap item in the destination slot? + case Some(InventoryItem(item2, destIndex)) => //yes, swap + //cleanly shuffle items around to avoid losing icons + //the next ObjectDetachMessage is necessary to avoid icons being lost, but only as part of this swap + sendResponse(ObjectDetachMessage(source_guid, item_guid, Vector3.Zero, 0f, 0f, 0f)) + val item2_guid = item2.GUID + destination.Slot(destIndex).Equipment = None //remove the swap item from destination (indexSlot.Equipment = item2) match { case Some(_) => //item and item2 swapped places successfully - log.info(s"MoveItem: ${item2.GUID} swapped to $source_guid @ $index") - //cleanly shuffle items around to avoid losing icons - sendResponse(ObjectDetachMessage(source_guid, item_guid, Vector3(0f, 0f, 0f), 0f, 0f, 0f)) //ground; A -> C - sendResponse(ObjectAttachMessage(source_guid, item2.GUID, index)) //B -> A - source match { - case (obj : Vehicle) => - val player_guid = player.GUID - 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 (obj : Player) => - if(source.VisibleSlots.contains(index)) { - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(source_guid, index, item2)) + log.info(s"MoveItem: $item2_guid swapped to $source_guid @ $index") + //remove item2 from destination + sendResponse(ObjectDetachMessage(destination_guid, item2_guid, Vector3.Zero, 0f, 0f, 0f)) + destination match { + case obj : Vehicle => + vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item2_guid)) + case obj : Player => + if(obj.isBackpack || destination.VisibleSlots.contains(dest)) { //corpse being looted, or item was in hands + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, item2_guid)) + } + case _ => ; + } + //display item2 in source + if(sourceIsNotDestination && player == source) { + val objDef = item2.Definition + sendResponse( + ObjectCreateDetailedMessage( + objDef.ObjectId, + item2_guid, + ObjectCreateMessageParent(source_guid, index), + objDef.Packet.DetailedConstructorData(item2).get + ) + ) + } + else { + sendResponse(ObjectAttachMessage(source_guid, item2_guid, index)) + } + source match { + case obj : Vehicle => + vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player_guid, source_guid, index, item2)) + case obj : Player => + if(source.VisibleSlots.contains(index)) { //item is put in hands + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(player_guid, source_guid, index, item2)) + } + else if(obj.isBackpack) { //corpse being given item + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.StowEquipment(player_guid, source_guid, index, item2)) } case _ => ; - //TODO something? } case None => //item2 does not fit; drop on ground + log.info(s"MoveItem: $item2_guid can not fit in swap location; dropping on ground @ ${source.Position}") val pos = source.Position val sourceOrientZ = source.Orientation.z val orient : Vector3 = Vector3(0f, 0f, sourceOrientZ) continent.Actor ! Zone.DropItemOnGround(item2, pos, orient) - sendResponse(ObjectDetachMessage(source_guid, item2.GUID, pos, 0f, 0f, sourceOrientZ)) //ground + sendResponse(ObjectDetachMessage(destination_guid, item2_guid, pos, 0f, 0f, sourceOrientZ)) //ground val objDef = item2.Definition - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, pos, orient, objDef.ObjectId, item2.GUID, objDef.Packet.ConstructorData(item2).get)) - } - - case None => //just move item over - destSlot.Equipment = item - source match { - case (obj : Vehicle) => - vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player.GUID, item_guid)) - //TODO visible slot verification, in the case of BFR arms - case _ => ; - //TODO something? + destination match { + case obj : Vehicle => + vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item2_guid)) + 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)) } + case None => ; + } + //move item into destination slot + destination.Slot(dest).Equipment = item + if(sourceIsNotDestination && player == destination) { + val objDef = item.Definition + sendResponse( + ObjectCreateDetailedMessage( + objDef.ObjectId, + item_guid, + ObjectCreateMessageParent(destination_guid, dest), + objDef.Packet.DetailedConstructorData(item).get + ) + ) + } + else { + sendResponse(ObjectAttachMessage(destination_guid, item_guid, dest)) } - sendResponse(ObjectAttachMessage(destination_guid, item_guid, dest)) destination match { - case (obj : Vehicle) => - vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player.GUID, destination_guid, dest, item)) - //TODO visible slot verification, in the case of BFR arms - case (_ : Player) => - if(destination.VisibleSlots.contains(dest)) { - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(destination_guid, dest, item)) + case obj : Vehicle => + vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player_guid, destination_guid, dest, item)) + case obj : Player => + if(destination.VisibleSlots.contains(dest)) { //item is put in hands + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(player_guid, destination_guid, dest, item)) + } + else if(obj.isBackpack) { //corpse being given item + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.StowEquipment(player_guid, destination_guid, dest, item)) } case _ => ; - //TODO something? } } - else if(indexSlot.Equipment.nonEmpty) { - log.error(s"MoveItem: wanted to move $item_guid, but unexpected item ${indexSlot.Equipment.get} at origin") + else if(!indexSlot.Equipment.contains(item)) { + log.error(s"MoveItem: wanted to move $item_guid, but found unexpected ${indexSlot.Equipment.get} at source location") } else { - log.error(s"MoveItem: wanted to move $item_guid, but unexpected item(s) at destination") + destinationCollisionTest match { + case Success(_) => + log.error(s"MoveItem: wanted to move $item_guid, but multiple unexpected items at destination blocked progress") + case scala.util.Failure(err) => + log.error(s"MoveItem: wanted to move $item_guid, but $err") + } } case _ => log.error(s"MoveItem: wanted to move $item_guid, but could not find it") } - case (None, _, _) => - log.error(s"MoveItem: wanted to move $item_guid from $source_guid, but could not find source") + log.error(s"MoveItem: wanted to move $item_guid from $source_guid, but could not find source object") case (_, None, _) => - log.error(s"MoveItem: wanted to move $item_guid from $source_guid to $destination_guid, but could not find destination") + log.error(s"MoveItem: wanted to move $item_guid to $destination_guid, but could not find destination object") case (_, _, None) => log.error(s"MoveItem: wanted to move $item_guid, but could not find it") case _ => @@ -2680,7 +2752,7 @@ class WorldSessionActor extends Actor with MDCContextAware { ) ) if(localTarget.VisibleSlots.contains(localIndex)) { - localService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(localTarget.GUID, localIndex, localObject)) + localService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(localTarget.GUID, localTarget.GUID, localIndex, localObject)) } } }) @@ -3554,7 +3626,7 @@ class WorldSessionActor extends Actor with MDCContextAware { /** * An event has occurred that would cause the player character to stop certain stateful activities. - * These activities include shooting, hacking, accessing (a container), flying, and running. + * These activities include shooting, weapon drawing, hacking, accessing (a container), flying, and running. * Other players in the same zone must be made aware that the player has stopped as well.
*
* Things whose configuration should not be changed:
@@ -3583,6 +3655,11 @@ class WorldSessionActor extends Actor with MDCContextAware { shooting = None case None => ; } + if(player != null && player.isAlive && player.VisibleSlots.contains(player.DrawnSlot)) { + player.DrawnSlot = Player.HandsDownSlot + sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, true)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectHeld(player.GUID, player.LastDrawnSlot)) + } if(flying) { sendResponse(ChatMsg(ChatMessageType.CMT_FLY, false, "", "off", None)) flying = false diff --git a/pslogin/src/main/scala/services/avatar/AvatarAction.scala b/pslogin/src/main/scala/services/avatar/AvatarAction.scala index 80d49828..d0701767 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarAction.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarAction.scala @@ -17,7 +17,7 @@ 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 EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) 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 @@ -28,6 +28,7 @@ object AvatarAction { 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 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 diff --git a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala b/pslogin/src/main/scala/services/avatar/AvatarResponse.scala index f3c605ed..5932bace 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarResponse.scala @@ -16,7 +16,7 @@ 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(slot : Int, item : Equipment) 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 LoadPlayer(pdata : ConstructorData) extends Response // final case class unLoadMap() extends Response @@ -27,6 +27,7 @@ object AvatarResponse { final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response final case class Release(player : Player) extends Response final case class Reload(weapon_guid : PlanetSideGUID) extends Response + final case class StowEquipment(target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Response final case class WeaponDryFire(weapon_guid : PlanetSideGUID) extends Response // final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response // final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response diff --git a/pslogin/src/main/scala/services/avatar/AvatarService.scala b/pslogin/src/main/scala/services/avatar/AvatarService.scala index 2978f1a1..17cec7ae 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarService.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarService.scala @@ -62,9 +62,9 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ConcealPlayer()) ) - case AvatarAction.EquipmentInHand(player_guid, slot, obj) => + case AvatarAction.EquipmentInHand(player_guid, target_guid, slot, obj) => AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.EquipmentInHand(slot, obj)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.EquipmentInHand(target_guid, slot, obj)) ) case AvatarAction.EquipmentOnGround(player_guid, pos, orient, item_id, item_guid, item_data) => AvatarEvents.publish( @@ -102,6 +102,10 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Reload(weapon_guid)) ) + case AvatarAction.StowEquipment(player_guid, target_guid, slot, obj) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.StowEquipment(target_guid, slot, obj)) + ) case AvatarAction.WeaponDryFire(player_guid, weapon_guid) => AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.WeaponDryFire(weapon_guid)) diff --git a/pslogin/src/test/scala/AvatarServiceTest.scala b/pslogin/src/test/scala/AvatarServiceTest.scala index ea86a6d7..c122dcdf 100644 --- a/pslogin/src/test/scala/AvatarServiceTest.scala +++ b/pslogin/src/test/scala/AvatarServiceTest.scala @@ -100,8 +100,8 @@ class EquipmentInHandTest extends ActorTest { 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))) + service ! AvatarServiceMessage("test", AvatarAction.EquipmentInHand(PlanetSideGUID(10), PlanetSideGUID(11), 2, tool)) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentInHand(PlanetSideGUID(11), 2, tool))) } } } @@ -271,6 +271,20 @@ class WeaponDryFireTest extends ActorTest { } } +class AvatarStowEquipmentTest extends ActorTest { + val tool = Tool(GlobalDefinitions.beamer) + + "AvatarService" should { + "pass StowEquipment" in { + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) + service ! Service.Join("test") + service ! AvatarServiceMessage("test", AvatarAction.StowEquipment(PlanetSideGUID(10), PlanetSideGUID(11), 2, tool)) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.StowEquipment(PlanetSideGUID(11), 2, tool))) + } + } +} + /* Preparation for these three Release tests is involved. The ServiceManager must not only be set up correctly, but must be given a TaskResolver.