From 9e51e33246cb5524f30257ee5446a9cc03dd8b15 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 9 Jan 2018 20:28:36 -0500 Subject: [PATCH] synchronizing change ammo --- .../scala/net/psforever/objects/AmmoBox.scala | 31 +- .../packet/game/ObjectDetachMessage.scala | 18 +- .../test/scala/objects/ConverterTest.scala | 3 +- .../test/scala/objects/EquipmentTest.scala | 35 ++ .../test/scala/objects/InventoryTest.scala | 8 +- .../src/main/scala/WorldSessionActor.scala | 359 ++++++++++++++++-- .../scala/services/avatar/AvatarAction.scala | 3 +- .../services/avatar/AvatarResponse.scala | 3 +- .../scala/services/avatar/AvatarService.scala | 8 +- .../main/test/scala/AvatarServiceTest.scala | 7 +- 10 files changed, 407 insertions(+), 68 deletions(-) diff --git a/common/src/main/scala/net/psforever/objects/AmmoBox.scala b/common/src/main/scala/net/psforever/objects/AmmoBox.scala index f95cebb7..835488ec 100644 --- a/common/src/main/scala/net/psforever/objects/AmmoBox.scala +++ b/common/src/main/scala/net/psforever/objects/AmmoBox.scala @@ -36,17 +36,26 @@ object AmmoBox { new AmmoBox(ammoDef, Some(capacity)) } - import net.psforever.packet.game.PlanetSideGUID - def apply(guid : PlanetSideGUID, ammoDef : AmmoBoxDefinition) : AmmoBox = { - val obj = new AmmoBox(ammoDef) - obj.GUID = guid - obj - } - - def apply(guid : PlanetSideGUID, ammoDef : AmmoBoxDefinition, capacity : Int) : AmmoBox = { - val obj = new AmmoBox(ammoDef, Some(capacity)) - obj.GUID = guid - obj + /** + * Accepting an `AmmoBox` object that has an uncertain amount of ammunition in it, + * create multiple `AmmoBox` objects where none contain more than the maximum capacity for that ammunition type, + * and the sum of all objects' capacities is the original object's capacity. + * @param box an `AmmoBox` object of unspecified capacity + * @return a `List` of `AmmoBox` objects with correct capacities + */ + def Split(box : AmmoBox) : List[AmmoBox] = { + val ammoDef = box.Definition + var boxCap : Int = box.Capacity + val maxCap : Int = ammoDef.Capacity + val splitCap : Int = boxCap / maxCap + val list : List[AmmoBox] = List.fill(splitCap)(new AmmoBox(ammoDef)) + val leftover = boxCap - maxCap * splitCap + if(leftover > 0) { + list :+ AmmoBox(ammoDef, leftover) + } + else { + list + } } def limitCapacity(count : Int, min : Int = 0) : Int = math.min(math.max(min, count), 65535) diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala index 4d6abda7..a68fa1fb 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala @@ -9,17 +9,17 @@ import scodec.codecs._ /** * Dispatched by the server to cause two associated objects to disentangle from one another.
*
- * `ObjectDetachMessage` is the opposite to `ObjectAttachMessage`. - * When detached, the resulting freed object will be placed at the given coordinates. - * For some container objects, most often static ones, a default placement point does exist. + * `ObjectDetachMessage` is the opposite of `ObjectAttachMessage`. + * When detached, the resulting freed object will be placed at the given coordinates in the game world. + * For detachment from some container objects, a default placement point may exist. * This usually matches the position where the original mounting occurred, or is relative to the current position of the container. - * Using a position that is not the mounting one, in this case, counts as a temporary teleport of the character. - * As soon as available, e.g., the end of an animation, the character will rw-appear at the mounting point. - * The object may also have its orientation aspect changed.
+ * This mounting position overrides the input one, but other temporary side-effects may occur. + * For example, if a player detaches from a vehicle with coordinates for "somewhere else," + * the camera will temporarily be moved to that location "somewhere else" for the duration of the animation + * but it will soon regain the player who appeared where expected.
*
- * This packet is considered proper response to:
- * - `DismountVehicleMsg`
- * - `DropItemMessage` + * An object that is already dropped is a special case where the parent (container) does not technically exist. + * The parent also does not need to exist as the object will still be transported to the specified coordinates. * @param parent_guid the container/connector object * @param child_guid the contained/connected object * @param pos where the contained/connected object will be placed after it has detached diff --git a/common/src/test/scala/objects/ConverterTest.scala b/common/src/test/scala/objects/ConverterTest.scala index fe4f8d58..f9a22fde 100644 --- a/common/src/test/scala/objects/ConverterTest.scala +++ b/common/src/test/scala/objects/ConverterTest.scala @@ -277,7 +277,8 @@ class ConverterTest extends Specification { fury_def.TrunkSize = InventoryTile(11, 11) fury_def.TrunkOffset = 30 - val hellfire_ammo_box = AmmoBox(PlanetSideGUID(432), hellfire_ammo) + val hellfire_ammo_box = AmmoBox(hellfire_ammo) + hellfire_ammo_box.GUID = PlanetSideGUID(432) val fury = Vehicle(fury_def) fury.GUID = PlanetSideGUID(413) diff --git a/common/src/test/scala/objects/EquipmentTest.scala b/common/src/test/scala/objects/EquipmentTest.scala index db2de872..3fbe4501 100644 --- a/common/src/test/scala/objects/EquipmentTest.scala +++ b/common/src/test/scala/objects/EquipmentTest.scala @@ -65,6 +65,41 @@ class EquipmentTest extends Specification { obj.Capacity = 65536 obj.Capacity mustEqual 65535 } + + "split (0)" in { + val obj = AmmoBox(bullet_9mm) + obj.Capacity = 0 + val list = AmmoBox.Split(obj) + list.size mustEqual 0 + } + + "split (1)" in { + val obj = AmmoBox(bullet_9mm) + obj.Capacity = 50 + val list = AmmoBox.Split(obj) + list.size mustEqual 1 + list.head.Capacity mustEqual 50 + } + + "split (2)" in { + val obj = AmmoBox(bullet_9mm) + obj.Capacity = 75 + val list = AmmoBox.Split(obj) + list.size mustEqual 2 + list.head.Capacity mustEqual 50 + list(1).Capacity mustEqual 25 + } + + "split (4)" in { + val obj = AmmoBox(bullet_9mm) + obj.Capacity = 165 + val list = AmmoBox.Split(obj) + list.size mustEqual 4 + list.head.Capacity mustEqual 50 + list(1).Capacity mustEqual 50 + list(2).Capacity mustEqual 50 + list(3).Capacity mustEqual 15 + } } "Tool" should { diff --git a/common/src/test/scala/objects/InventoryTest.scala b/common/src/test/scala/objects/InventoryTest.scala index 34ec7491..3eea16e6 100644 --- a/common/src/test/scala/objects/InventoryTest.scala +++ b/common/src/test/scala/objects/InventoryTest.scala @@ -12,8 +12,12 @@ import scala.collection.mutable.ListBuffer import scala.util.Success class InventoryTest extends Specification { - val bullet9mmBox1 = AmmoBox(PlanetSideGUID(1), bullet_9mm) - val bullet9mmBox2 = AmmoBox(PlanetSideGUID(2), bullet_9mm) + val + bullet9mmBox1 = AmmoBox(bullet_9mm) + bullet9mmBox1.GUID = PlanetSideGUID(1) + val + bullet9mmBox2 = AmmoBox(bullet_9mm) + bullet9mmBox2.GUID = PlanetSideGUID(2) "GridInventory" should { "construct" in { diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 8d1566fc..e1127d25 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -146,6 +146,19 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateGamePacket(0, ArmorChangedMessage(guid, suit, subtype))) } + case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, ammo_id, ammo_guid, ammo_data) => + if(player.GUID != guid) { + sendResponse(PacketCoding.CreateGamePacket(0, + ObjectCreateMessage( + ammo_id, + ammo_guid, + ObjectCreateMessageParent(weapon_guid, weapon_slot), + ammo_data + )) + ) + sendResponse(PacketCoding.CreateGamePacket(0, ChangeAmmoMessage(weapon_guid, 1))) + } + case AvatarResponse.ChangeFireState_Start(weapon_guid) => if(player.GUID != guid) { sendResponse(PacketCoding.CreateGamePacket(0, ChangeFireStateMessage_Start(weapon_guid))) @@ -176,15 +189,14 @@ class WorldSessionActor extends Actor with MDCContextAware { ) } - case AvatarResponse.EquipmentOnGround(pos, orient, item) => + case AvatarResponse.EquipmentOnGround(pos, orient, item_id, item_guid, item_data) => if(player.GUID != guid) { - val definition = item.Definition sendResponse( PacketCoding.CreateGamePacket(0, ObjectCreateMessage( - definition.ObjectId, - item.GUID, - DroppedItemData(PlacementData(pos, Vector3(0f, 0f, orient.z)), definition.Packet.ConstructorData(item).get) + item_id, + item_guid, + DroppedItemData(PlacementData(pos, Vector3(0f, 0f, orient.z)), item_data) ) ) ) @@ -299,6 +311,11 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateGamePacket(0, ChildObjectStateMessage(object_guid, pitch, yaw))) } + case VehicleResponse.DismountVehicle(unk1, unk2) => + if(player.GUID != guid) { + sendResponse(PacketCoding.CreateGamePacket(0, DismountVehicleMsg(guid, unk1, unk2))) + } + case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) => if(player.GUID != guid) { //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? @@ -313,11 +330,6 @@ class WorldSessionActor extends Actor with MDCContextAware { )) } - case VehicleResponse.DismountVehicle(unk1, unk2) => - if(player.GUID != guid) { - sendResponse(PacketCoding.CreateGamePacket(0, DismountVehicleMsg(guid, unk1, unk2))) - } - case VehicleResponse.KickPassenger(unk1, unk2, vehicle_guid) => sendResponse(PacketCoding.CreateGamePacket(0, DismountVehicleMsg(guid, unk1, unk2))) if(guid == player.GUID) { @@ -571,7 +583,8 @@ class WorldSessionActor extends Actor with MDCContextAware { // ) ) ) - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, obj)) + val objDef = obj.Definition + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, objDef.ObjectId, obj.GUID, objDef.Packet.ConstructorData(obj).get)) }) sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true))) } @@ -665,7 +678,8 @@ class WorldSessionActor extends Actor with MDCContextAware { ObjectDetachMessage(tplayer.GUID, obj.GUID, pos, 0f, 0f, orient.z) ) ) - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, obj)) + val objDef = obj.Definition + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, objDef.ObjectId, obj.GUID, objDef.Packet.ConstructorData(obj).get)) }) sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.InfantryLoadout, true))) @@ -1207,9 +1221,9 @@ class WorldSessionActor extends Actor with MDCContextAware { val parent_guid = PlanetSideGUID(terminal_guid) continent.GUID(interface_guid) match { case Some(obj : Terminal) => - val obj_def = obj.Definition - val obj_uid = obj_def.ObjectId - val obj_data = obj_def.Packet.ConstructorData(obj).get + val objDef = obj.Definition + val obj_uid = objDef.ObjectId + val obj_data = objDef.Packet.ConstructorData(obj).get sendResponse(PacketCoding.CreateGamePacket(0, ObjectCreateMessage( obj_uid, @@ -1331,6 +1345,152 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ VoiceHostInfo(player_guid, data) => sendResponse(PacketCoding.CreateGamePacket(0, VoiceHostKill())) + case msg @ ChangeAmmoMessage(item_guid, unk1) => + log.info("ChangeAmmo: " + msg) + (player.VehicleSeated match { + case Some(vehicle_guid) => //weapon is vehicle turret? + continent.GUID(vehicle_guid) match { + case Some(vehicle : Vehicle) => + vehicle.PassengerInSeat(player) match { + case Some(seat_num) => + vehicle.WeaponControlledFromSeat(seat_num) match { + case Some(item : Tool) => + //TODO check that item controlled by seat is item_guid? + (Some(vehicle), Some(item)) + case _ => ; + (None, None) + } + case None => ; + (None, None) + } + case _ => ; + (None, None) + } + case None => //not in vehicle; weapon in hand? + player.Slot(player.DrawnSlot).Equipment match { + //TODO check that item in hand is item_guid? + case Some(item : Tool) => + (Some(player), Some(item)) + case _ => ; + (None, None) + } + }) match { + case (Some(obj), Some(tool : Tool)) => + val originalAmmoType = tool.AmmoType + val fullMagazine = tool.MaxMagazine + do { + val requestedAmmoType = tool.NextAmmoType + if(requestedAmmoType != tool.AmmoSlot.Box.AmmoType) { + FindReloadAmmunition(obj, requestedAmmoType, fullMagazine).reverse match { + case Nil => ; + case x :: xs => + val (deleteFunc, modifyFunc) : ((Int, AmmoBox)=>Unit, (AmmoBox, Int)=>Unit) = obj match { + case (veh : Vehicle) => + (DeleteAmmunitionInVehicle(veh), ModifyAmmunitionInVehicle(veh)) + case _ => + (DeleteAmmunition(obj), ModifyAmmunition(obj)) + } + val (stowFuncTask, stowFunc) : ((Int, AmmoBox)=>TaskResolver.GiveTask, (Int, AmmoBox)=>Unit) = obj match { + case (veh : Vehicle) => + (StowNewAmmunitionInVehicles(veh), StowAmmunitionInVehicles(veh)) + case _ => + (StowNewAmmunition(obj), StowAmmunition(obj)) + } + xs.foreach(item => { + obj.Inventory -= x.start + deleteFunc(item.start, item.obj.asInstanceOf[AmmoBox]) + }) + + //box will be the replacement ammo; give it the discovered magazine and load it into the weapon @ 0 + val box = x.obj.asInstanceOf[AmmoBox] + val originalBoxCapacity = box.Capacity + val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) } + val sumReloadValue : Int = originalBoxCapacity + tailReloadValue + val previousBox = tool.AmmoSlot.Box //current magazine in tool + sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3(0f, 0f, 0f), 0f, 0f, 0f))) + sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(player.GUID, box.GUID, Vector3(0f, 0f, 0f), 0f, 0f, 0f))) + obj.Inventory -= x.start //remove replacement ammo from inventory + val ammoSlotIndex = tool.FireMode.AmmoSlotIndex + tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool + sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(tool.GUID, box.GUID, ammoSlotIndex))) + + //handle inventory contents + box.Capacity = (if(sumReloadValue <= fullMagazine) { + sumReloadValue + } + else { + val splitReloadAmmo : Int = sumReloadValue - fullMagazine + log.info(s"ChangeAmmo: taking ${originalBoxCapacity - splitReloadAmmo} from a box of ${originalBoxCapacity} $requestedAmmoType") + val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) + obj.Inventory += x.start -> boxForInventory //block early; assumption warning: swappable ammo types have the same icon size + taskResolver ! stowFuncTask(x.start, boxForInventory) + fullMagazine + }) + sendResponse(PacketCoding.CreateGamePacket(0, InventoryStateMessage(box.GUID, tool.GUID, box.Capacity))) //should work for both players and vehicles + log.info(s"ChangeAmmo: loading ${box.Capacity} $requestedAmmoType into ${tool.GUID} @ $ammoSlotIndex") + if(previousBox.Capacity > 0) { + //divide capacity across other existing and not full boxes of that ammo type + var capacity = previousBox.Capacity + val iter = obj.Inventory.Items + .map({case(_, entry) => entry }) + .filter(entry => { + entry.obj match { + case (item : AmmoBox) => + item.AmmoType == originalAmmoType && item.FullCapacity != item.Capacity + case _ => + false + } + }) + .toList + .sortBy(_.start) + .iterator + while(capacity > 0 && iter.hasNext) { + val entry = iter.next + val item : AmmoBox = entry.obj.asInstanceOf[AmmoBox] + val ammoAllocated = math.min(item.FullCapacity - item.Capacity, capacity) + log.info(s"ChangeAmmo: putting $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType") + capacity -= ammoAllocated + modifyFunc(item, -ammoAllocated) + } + previousBox.Capacity = capacity + } + if(previousBox.Capacity > 0) { + //TODO split previousBox into AmmoBox objects of appropriate max capacity, e.g., 100 9mm -> 2 x 50 9mm + obj.Inventory.Fit(previousBox.Definition.Tile) match { + case Some(index) => //put retained magazine in inventory + stowFunc(index, previousBox) + case None => //drop + log.info(s"ChangeAmmo: dropping ammo box $previousBox") + val pos = player.Position + val orient = player.Orientation + sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(Service.defaultPlayerGUID, previousBox.GUID, pos, 0f, 0f, orient.z))) + val orient2 = Vector3(0f, 0f, orient.z) + continent.Ground ! Zone.DropItemOnGround(previousBox, pos, orient2) + val objDef = previousBox.Definition + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentOnGround(player.GUID, pos, orient2, objDef.ObjectId, previousBox.GUID, objDef.Packet.ConstructorData(previousBox).get)) + } + } + else { + taskResolver ! GUIDTask.UnregisterObjectTask(previousBox)(continent.GUID) + } + + //announce swapped ammunition box in weapon + val boxDef = box.Definition + val box_guid = box.GUID + val tool_guid = tool.GUID + sendResponse(PacketCoding.CreateGamePacket(0, ChangeAmmoMessage(tool_guid, box.Capacity))) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeAmmo(player.GUID, tool_guid, ammoSlotIndex, boxDef.ObjectId, box.GUID, boxDef.Packet.ConstructorData(box).get)) + } + } + } + while(tool.AmmoType != originalAmmoType && tool.AmmoType != tool.AmmoSlot.Box.AmmoType) + + case (_, Some(_)) => + log.error(s"ChangeAmmo: the object that was found for $item_guid was not a Tool") + case (_, None) => + log.error(s"ChangeAmmo: can not find $item_guid") + } + case msg @ ChangeFireModeMessage(item_guid, fire_mode) => log.info("ChangeFireMode: " + msg) @@ -1363,7 +1523,8 @@ class WorldSessionActor extends Actor with MDCContextAware { player.FreeHand.Equipment = None continent.Ground ! Zone.DropItemOnGround(item, player.Position, orient) sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(player.GUID, item.GUID, player.Position, 0f, 0f, player.Orientation.z))) - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, player.Position, orient, item)) + val objDef = item.Definition + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, player.Position, orient, objDef.ObjectId, item.GUID, objDef.Packet.ConstructorData(item).get)) } else { log.warn(s"item in hand was ${item.GUID} but trying to drop $item_guid; nothing will be dropped") @@ -1415,20 +1576,20 @@ class WorldSessionActor extends Actor with MDCContextAware { case Nil => log.warn(s"ReloadMessage: no ammunition could be found for $item_guid") case list @ x :: xs => - val (deleteFunc, modifyFunc) : ((Int, PlanetSideGUID)=>Unit, (AmmoBox, Int)=>Unit) = obj match { + val (deleteFunc, modifyFunc) : ((Int, AmmoBox)=>Unit, (AmmoBox, Int)=>Unit) = obj match { case (veh : Vehicle) => (DeleteAmmunitionInVehicle(veh), ModifyAmmunitionInVehicle(veh)) case _ => (DeleteAmmunition(obj), ModifyAmmunition(obj)) } xs.foreach(item => { - deleteFunc(item.start, item.obj.GUID) + deleteFunc(item.start, item.obj.asInstanceOf[AmmoBox]) }) val box = x.obj.asInstanceOf[AmmoBox] val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) } val sumReloadValue : Int = box.Capacity + tailReloadValue val actualReloadValue = (if(sumReloadValue <= reloadValue) { - deleteFunc(x.start, box.GUID) + deleteFunc(x.start, box) sumReloadValue } else { @@ -1564,7 +1725,8 @@ class WorldSessionActor extends Actor with MDCContextAware { val orient : Vector3 = Vector3(0f, 0f, sourceOrientZ) continent.Actor ! Zone.DropItemOnGround(item2, pos, orient) sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(source_guid, item2.GUID, pos, 0f, 0f, sourceOrientZ))) //ground - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, pos, orient, item2)) + 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 @@ -1614,9 +1776,6 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ LootItemMessage(item_guid, target_guid) => log.info("LootItem: " + msg) - case msg @ ChangeAmmoMessage(item_guid, unk1) => - log.info("ChangeAmmo: " + msg) - case msg @ AvatarImplantMessage(_, _, _, _) => //(player_guid, unk1, unk2, implant) => log.info("AvatarImplantMessage: " + msg) @@ -1751,6 +1910,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ WeaponDelayFireMessage(seq_time, weapon_guid) => log.info("WeaponDelayFire: " + msg) + + case msg @ WeaponDryFireMessage(weapon_guid) => + log.info("WeaponDryFireMessage: "+msg) (player.VehicleSeated match { case Some(vehicle_guid) => continent.GUID(vehicle_guid) match { @@ -1986,9 +2148,6 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ HitHint(source, player_guid) => log.info("HitHint: "+msg) - case msg @ WeaponDryFireMessage(weapon) => - log.info("WeaponDryFireMessage: "+msg) - case msg @ TargetingImplantRequest(list) => log.info("TargetingImplantRequest: "+msg) @@ -2063,7 +2222,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * @see `PutInSlot` * @return a `TaskResolver.GiveTask` message */ - private def PutEquipmentInSlot(target : Player, obj : Equipment, index : Int) : TaskResolver.GiveTask = { + private def PutEquipmentInSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = { val regTask = GUIDTask.RegisterEquipment(obj)(continent.GUID) obj match { case tool : Tool => @@ -2085,7 +2244,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * @see `RemoveFromSlot` * @return a `TaskResolver.GiveTask` message */ - private def RemoveEquipmentFromSlot(target : Player, obj : Equipment, index : Int) : TaskResolver.GiveTask = { + private def RemoveEquipmentFromSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = { val regTask = GUIDTask.UnregisterEquipment(obj)(continent.GUID) //to avoid an error from a GUID-less object from being searchable, it is removed from the inventory first obj match { @@ -2103,7 +2262,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * @param index the slot where the new `Equipment` will be placed * @return a `TaskResolver.GiveTask` message */ - private def PutInSlot(target : Player, obj : Equipment, index : Int) : TaskResolver.GiveTask = { + private def PutInSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = { TaskResolver.GiveTask( new Task() { private val localTarget = target @@ -2139,7 +2298,7 @@ class WorldSessionActor extends Actor with MDCContextAware { ) ) if(0 <= localIndex && localIndex < 5) { - localService ! AvatarServiceMessage(localTarget.Continent, AvatarAction.EquipmentInHand(localTarget.GUID, localIndex, localObject)) + localService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(localTarget.GUID, localIndex, localObject)) } } }) @@ -2256,7 +2415,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * @param index the slot where the `Equipment` is stored * @return a `TaskResolver.GiveTask` message */ - private def RemoveFromSlot(target : Player, obj : Equipment, index : Int) : TaskResolver.GiveTask = { + private def RemoveFromSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = { TaskResolver.GiveTask( new Task() { private val localTarget = target @@ -2265,6 +2424,7 @@ class WorldSessionActor extends Actor with MDCContextAware { private val localObjectGUID = obj.GUID private val localAnnounce = self //self may not be the same when it executes private val localService = avatarService + private val localContinent = continent.Id override def isComplete : Task.Resolution.Value = { if(localTarget.Slot(localIndex).Equipment.contains(localObject)) { @@ -2283,7 +2443,7 @@ class WorldSessionActor extends Actor with MDCContextAware { override def onSuccess() : Unit = { localAnnounce ! ResponseToSelf(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(localObjectGUID, 0))) if(0 <= localIndex && localIndex < 5) { - localService ! AvatarServiceMessage(localTarget.Continent, AvatarAction.ObjectDelete(localTarget.GUID, localObjectGUID)) + localService ! AvatarServiceMessage(localContinent, AvatarAction.ObjectDelete(localTarget.GUID, localObjectGUID)) } } } @@ -2456,24 +2616,36 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + /** + * Common preparation for interfacing with a vehicle. + * Join a vehicle-specific group for shared updates. + * Construct every object in the vehicle's inventory fpr shared manipulation updates. + * @param vehicle the vehicle + */ def AccessContents(vehicle : Vehicle) : Unit = { vehicleService ! Service.Join(s"${vehicle.Actor}") val parent_guid = vehicle.GUID vehicle.Trunk.Items.foreach({ case ((_, entry)) => val obj = entry.obj - val obj_def = obj.Definition + val objDef = obj.Definition sendResponse(PacketCoding.CreateGamePacket(0, ObjectCreateDetailedMessage( - obj_def.ObjectId, + objDef.ObjectId, obj.GUID, ObjectCreateMessageParent(parent_guid, entry.start), - obj_def.Packet.DetailedConstructorData(obj).get + objDef.Packet.DetailedConstructorData(obj).get ) )) }) } + /** + * Common preparation for disengaging from a vehicle. + * Leave the vehicle-specific group that was used for shared updates. + * Deconstruct every object in the vehicle's inventory. + * @param vehicle the vehicle + */ def UnAccessContents(vehicle : Vehicle) : Unit = { vehicleService ! Service.Leave(Some(s"${vehicle.Actor}")) vehicle.Trunk.Items.foreach({ @@ -2514,27 +2686,138 @@ class WorldSessionActor extends Actor with MDCContextAware { }) } - private def DeleteAmmunition(obj : PlanetSideGameObject with Container)(start : Int, item_guid : PlanetSideGUID) : Unit = { + /** + * Given an object that contains a box of amunition in its `Inventory` at a certain location, + * remove it permanently. + * @param obj the `Container` + * @param start where the ammunition can be found + * @param item an object to unregister (should have been the ammunition that was removed); + * not explicitly checked + */ + private def DeleteAmmunition(obj : PlanetSideGameObject with Container)(start : Int, item : AmmoBox) : Unit = { + val item_guid = item.GUID obj.Inventory -= start + taskResolver ! GUIDTask.UnregisterEquipment(item)(continent.GUID) sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(item_guid, 0))) } - private def DeleteAmmunitionInVehicle(obj : Vehicle)(start : Int, item_guid : PlanetSideGUID) : Unit = { - DeleteAmmunition(obj)(start, item_guid) + /** + * Given a vehicle that contains a box of amunition in its `Trunk` at a certain location, + * remove it permanently. + * @see `DeleteAmmunition` + * @param obj the `Vehicle` + * @param start where the ammunition can be found + * @param item an object to unregister (should have been the ammunition that was removed); + * not explicitly checked + */ + private def DeleteAmmunitionInVehicle(obj : Vehicle)(start : Int, item : AmmoBox) : Unit = { + val item_guid = item.GUID + DeleteAmmunition(obj)(start, item) vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player.GUID, item_guid)) } + /** + * Given an object that contains a box of amunition in its `Inventry` at a certain location, + * change the amount of ammunition within that box. + * @param obj the `Container` + * @param box an `AmmoBox` to modify + * @param reloadValue the value to modify the `AmmoBox`; + * subtracted from the current `Capacity` of `Box` + */ private def ModifyAmmunition(obj : PlanetSideGameObject with Container)(box : AmmoBox, reloadValue : Int) : Unit = { val capacity = box.Capacity - reloadValue box.Capacity = capacity sendResponse(PacketCoding.CreateGamePacket(0, InventoryStateMessage(box.GUID, obj.GUID, capacity))) } + /** + * Given a vehicle that contains a box of amunition in its `Trunk` at a certain location, + * change the amount of ammunition within that box. + * @param obj the `Container` + * @param box an `AmmoBox` to modify + * @param reloadValue the value to modify the `AmmoBox`; + * subtracted from the current `Capacity` of `Box` + */ private def ModifyAmmunitionInVehicle(obj : Vehicle)(box : AmmoBox, reloadValue : Int) : Unit = { val capacity = ModifyAmmunition(obj)(box, reloadValue) vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.InventoryState(player.GUID, box, obj.GUID, obj.Find(box).get, box.Definition.Packet.DetailedConstructorData(box).get)) } + /** + * Announce that an already-registered `AmmoBox` object exists in a given position in some `Container` object's inventory. + * @see `StowAmmunitionInVehicles` + * @see `ChangeAmmoMessage` + * @param obj the `Container` object + * @param index an index in `obj`'s inventory + * @param item an `AmmoBox` + */ + def StowAmmunition(obj : PlanetSideGameObject with Container)(index : Int, item : AmmoBox) : Unit = { + obj.Inventory += index -> item + sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(obj.GUID, item.GUID, index))) + } + + /** + * Announce that an already-registered `AmmoBox` object exists in a given position in some vehicle's inventory. + * @see `StowAmmunition` + * @see `ChangeAmmoMessage` + * @param obj the `Vehicle` object + * @param index an index in `obj`'s inventory + * @param item an `AmmoBox` + */ + def StowAmmunitionInVehicles(obj : Vehicle)(index : Int, item : AmmoBox) : Unit = { + StowAmmunition(obj)(index, item) + vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player.GUID, obj.GUID, index, item)) + } + + /** + * Prepare tasking that registers an `AmmoBox` object + * and announces that it exists in a given position in some `Container` object's inventory. + * `PutEquipmentInSlot` is the fastest way to achieve these goals. + * @see `StowNewAmmunitionInVehicles` + * @see `ChangeAmmoMessage` + * @param obj the `Container` object + * @param index an index in `obj`'s inventory + * @param item an `AmmoBox` + * @return a `TaskResolver.GiveTask` chain that executes the action + */ + def StowNewAmmunition(obj : PlanetSideGameObject with Container)(index : Int, item : AmmoBox) : TaskResolver.GiveTask = { + PutEquipmentInSlot(obj, item, index) + } + + /** + * Prepare tasking that registers an `AmmoBox` object + * and announces that it exists in a given position in some vehicle's inventory. + * `PutEquipmentInSlot` is the fastest way to achieve these goals. + * @see `StowNewAmmunition` + * @see `ChangeAmmoMessage` + * @param obj the `Container` object + * @param index an index in `obj`'s inventory + * @param item an `AmmoBox` + * @return a `TaskResolver.GiveTask` chain that executes the action + */ + def StowNewAmmunitionInVehicles(obj : Vehicle)(index : Int, item : AmmoBox) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localService = vehicleService + private val localPlayer = player + private val localVehicle = obj + private val localIndex = index + private val localItem = item + + override def isComplete : Task.Resolution.Value = Task.Resolution.Success + + def Execute(resolver : ActorRef) : Unit = { + localService ! VehicleServiceMessage( + s"${localVehicle.Actor}", + VehicleAction.StowEquipment(localPlayer.GUID, localVehicle.GUID, localIndex, localItem) + ) + resolver ! scala.util.Success(this) + } + }, + List(StowNewAmmunition(obj)(index, item)) + ) + } + /** * A predicate used to determine if an `InventoryItem` object contains `Equipment` that should be dropped. * Used to filter through lists of object data before it is placed into a player's inventory. diff --git a/pslogin/src/main/scala/services/avatar/AvatarAction.scala b/pslogin/src/main/scala/services/avatar/AvatarAction.scala index ef0d4926..9a09742a 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarAction.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarAction.scala @@ -10,12 +10,13 @@ object AvatarAction { trait Action final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action + final case class ChangeAmmo(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID, weapon_slot : Int, ammo_id : Int, ammo_guid : PlanetSideGUID, ammo_data : ConstructorData) extends Action 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(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action - final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, 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 diff --git a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala b/pslogin/src/main/scala/services/avatar/AvatarResponse.scala index 0a892825..1da7fcba 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarResponse.scala @@ -10,12 +10,13 @@ object AvatarResponse { trait Response final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response + final case class ChangeAmmo(weapon_guid : PlanetSideGUID, weapon_slot : Int, ammo_id : Int, ammo_guid : PlanetSideGUID, ammo_data : ConstructorData) extends Response 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 DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response final case class EquipmentInHand(slot : Int, item : Equipment) extends Response - final case class EquipmentOnGround(pos : Vector3, orient : Vector3, 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 // final case class LoadMap() extends Response diff --git a/pslogin/src/main/scala/services/avatar/AvatarService.scala b/pslogin/src/main/scala/services/avatar/AvatarService.scala index 09196cb1..5e47c3e8 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarService.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarService.scala @@ -39,6 +39,10 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ArmorChanged(suit, subtype)) ) + case AvatarAction.ChangeAmmo(player_guid, weapon_guid, weapon_slot, ammo_id, ammo_guid, ammo_data) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, ammo_id, ammo_guid, ammo_data)) + ) case AvatarAction.ChangeFireState_Start(player_guid, weapon_guid) => AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ChangeFireState_Start(weapon_guid)) @@ -55,9 +59,9 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.EquipmentInHand(slot, obj)) ) - case AvatarAction.EquipmentOnGround(player_guid, pos, orient, obj) => + case AvatarAction.EquipmentOnGround(player_guid, pos, orient, item_id, item_guid, item_data) => AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.EquipmentOnGround(pos, orient, obj)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.EquipmentOnGround(pos, orient, item_id, item_guid, item_data)) ) case AvatarAction.LoadPlayer(player_guid, pdata) => AvatarEvents.publish( diff --git a/pslogin/src/main/test/scala/AvatarServiceTest.scala b/pslogin/src/main/test/scala/AvatarServiceTest.scala index 9c43ac25..6bbdb9bf 100644 --- a/pslogin/src/main/test/scala/AvatarServiceTest.scala +++ b/pslogin/src/main/test/scala/AvatarServiceTest.scala @@ -94,14 +94,15 @@ class AvatarService5Test extends ActorTest { } class AvatarService6Test extends ActorTest { - val tool = Tool(GlobalDefinitions.beamer) + val toolDef = GlobalDefinitions.beamer + val tool = Tool(toolDef) "AvatarService" should { "pass EquipmentOnGround" in { val service = system.actorOf(Props[AvatarService], "service") service ! Service.Join("test") - service ! AvatarServiceMessage("test", AvatarAction.EquipmentOnGround(PlanetSideGUID(10), Vector3(300f, 200f, 100f), Vector3(450f, 300f, 150f), tool)) - expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentOnGround(Vector3(300f, 200f, 100f), Vector3(450f, 300f, 150f), tool))) + service ! AvatarServiceMessage("test", AvatarAction.EquipmentOnGround(PlanetSideGUID(10), Vector3(300f, 200f, 100f), Vector3(450f, 300f, 150f), toolDef.ObjectId, PlanetSideGUID(11), toolDef.Packet.ConstructorData(tool).get)) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentOnGround(Vector3(300f, 200f, 100f), Vector3(450f, 300f, 150f), toolDef.ObjectId, PlanetSideGUID(11), toolDef.Packet.ConstructorData(tool).get))) } } }