From 0628b988fef6fa4b9f28237547caa4c150b7ae0b Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 1 Jun 2023 23:13:05 -0400 Subject: [PATCH] using force psm occlusion to eliminate certain other packets that would be fine if hidden --- .../support/SessionAvatarHandlers.scala | 84 +++- .../support/SessionVehicleHandlers.scala | 40 +- .../session/support/VehicleOperations.scala | 4 - .../WeaponAndProjectileOperations.scala | 425 ++++++++++++------ .../psforever/objects/GlobalDefinitions.scala | 2 +- .../services/avatar/AvatarService.scala | 2 +- .../services/vehicle/VehicleService.scala | 31 +- .../vehicle/VehicleServiceMessage.scala | 13 + .../vehicle/VehicleServiceResponse.scala | 16 +- 9 files changed, 433 insertions(+), 184 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index 6c9f9ea03..7e8b366e7 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -3,6 +3,7 @@ package net.psforever.actors.session.support import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, typed} +import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.services.Service import scala.collection.mutable @@ -73,9 +74,9 @@ class SessionAvatarHandlers( canSeeReallyFar ) if isNotSameTarget => val pstateToSave = pstate.copy(timestamp = 0) - val (lastMsg, lastTime, lastPosition, wasVisible) = lastSeenStreamMessage.get(guid.guid) match { - case Some(SessionAvatarHandlers.LastUpstream(Some(msg), visible, time)) => (Some(msg), time, msg.pos, visible) - case _ => (None, 0L, Vector3.Zero, false) + val (lastMsg, lastTime, lastPosition, wasVisible, wasShooting) = lastSeenStreamMessage.get(guid.guid) match { + case Some(SessionAvatarHandlers.LastUpstream(Some(msg), visible, shooting, time)) => (Some(msg), time, msg.pos, visible, shooting) + case _ => (None, 0L, Vector3.Zero, false, None) } val drawConfig = Config.app.game.playerDraw //m val maxRange = drawConfig.rangeMax * drawConfig.rangeMax //sq.m @@ -129,10 +130,10 @@ class SessionAvatarHandlers( isCloaking ) ) - lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, now)) + lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, now)) } else { //is visible, but skip reinforcement - lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, lastTime)) + lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, lastTime)) } } else { //conditions where the target is not currently visible @@ -151,10 +152,10 @@ class SessionAvatarHandlers( is_cloaked = isCloaking ) ) - lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, now)) + lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, now)) } else { //skip drawing altogether - lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, lastTime)) + lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, lastTime)) } } @@ -179,10 +180,24 @@ class SessionAvatarHandlers( case AvatarResponse.ObjectHeld(_, previousSlot) => sendResponse(ObjectHeldMessage(guid, previousSlot, unk1=false)) - case AvatarResponse.ChangeFireState_Start(weaponGuid) if isNotSameTarget => + case AvatarResponse.ChangeFireState_Start(weaponGuid) + if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { _.visible } => + sendResponse(ChangeFireStateMessage_Start(weaponGuid)) + val entry = lastSeenStreamMessage(guid.guid) + lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = Some(weaponGuid))) + + case AvatarResponse.ChangeFireState_Start(weaponGuid) + if isNotSameTarget => sendResponse(ChangeFireStateMessage_Start(weaponGuid)) - case AvatarResponse.ChangeFireState_Stop(weaponGuid) if isNotSameTarget => + case AvatarResponse.ChangeFireState_Stop(weaponGuid) + if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { msg => msg.visible || msg.shooting.nonEmpty } => + sendResponse(ChangeFireStateMessage_Stop(weaponGuid)) + val entry = lastSeenStreamMessage(guid.guid) + lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = None)) + + case AvatarResponse.ChangeFireState_Stop(weaponGuid) + if isNotSameTarget => sendResponse(ChangeFireStateMessage_Stop(weaponGuid)) case AvatarResponse.LoadPlayer(pkt) if isNotSameTarget => @@ -388,7 +403,8 @@ class SessionAvatarHandlers( sendResponse(msg) /* common messages (maybe once every respawn) */ - case AvatarResponse.Reload(itemGuid) if isNotSameTarget => + case AvatarResponse.Reload(itemGuid) + if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { _.visible } => sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) case AvatarResponse.Killed(mount) => @@ -467,18 +483,14 @@ class SessionAvatarHandlers( ) /* uncommon messages (utility, or once in a while) */ + case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) + if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { _.visible } => + changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data) + sendResponse(ChangeAmmoMessage(weapon_guid, 1)) + case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) if isNotSameTarget => - sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0)) - sendResponse( - ObjectCreateMessage( - ammo_id, - ammo_guid, - ObjectCreateMessageParent(weapon_guid, weapon_slot), - ammo_data - ) - ) - sendResponse(ChangeAmmoMessage(weapon_guid, 1)) + changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data) case AvatarResponse.ChangeFireMode(itemGuid, mode) if isNotSameTarget => sendResponse(ChangeFireModeMessage(itemGuid, mode)) @@ -546,21 +558,45 @@ class SessionAvatarHandlers( ) ) - case AvatarResponse.WeaponDryFire(weaponGuid) if isNotSameTarget => + case AvatarResponse.WeaponDryFire(weaponGuid) + if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { _.visible } => continent.GUID(weaponGuid).collect { case tool: Tool if tool.Magazine == 0 => // check that the magazine is still empty before sending WeaponDryFireMessage // if it has been reloaded since then, other clients will not see it firing sendResponse(WeaponDryFireMessage(weaponGuid)) - case _ => - sendResponse(WeaponDryFireMessage(weaponGuid)) } case _ => () } } + + private def changeAmmoProcedures( + weaponGuid: PlanetSideGUID, + previousAmmoGuid: PlanetSideGUID, + ammoTypeId: Int, + ammoGuid: PlanetSideGUID, + ammoSlot: Int, + ammoData: ConstructorData + ): Unit = { + sendResponse(ObjectDetachMessage(weaponGuid, previousAmmoGuid, Vector3.Zero, 0)) + //TODO? sendResponse(ObjectDeleteMessage(previousAmmoGuid, 0)) + sendResponse( + ObjectCreateMessage( + ammoTypeId, + ammoGuid, + ObjectCreateMessageParent(weaponGuid, ammoSlot), + ammoData + ) + ) + } } object SessionAvatarHandlers { - private[support] case class LastUpstream(msg: Option[AvatarResponse.PlayerState], visible: Boolean, time: Long) + private[support] case class LastUpstream( + msg: Option[AvatarResponse.PlayerState], + visible: Boolean, + shooting: Option[PlanetSideGUID], + time: Long + ) } diff --git a/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala index abf83f5d1..55d2d4286 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala @@ -7,7 +7,7 @@ import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, Jamma import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.pad.VehicleSpawnPad -import net.psforever.objects.{GlobalDefinitions, Player, Vehicle, Vehicles} +import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles} import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent import net.psforever.packet.game._ import net.psforever.services.Service @@ -79,6 +79,36 @@ class SessionVehicleHandlers( if isNotSameTarget => sendResponse(FrameVehicleStateMessage(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA)) + case VehicleResponse.ChangeFireState_Start(weaponGuid) if isNotSameTarget => + sendResponse(ChangeFireStateMessage_Start(weaponGuid)) + + case VehicleResponse.ChangeFireState_Stop(weaponGuid) if isNotSameTarget => + sendResponse(ChangeFireStateMessage_Stop(weaponGuid)) + + case VehicleResponse.Reload(itemGuid) if isNotSameTarget => + sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) + + case VehicleResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) if isNotSameTarget => + sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0)) + //TODO? sendResponse(ObjectDeleteMessage(previousAmmoGuid, 0)) + sendResponse( + ObjectCreateMessage( + ammo_id, + ammo_guid, + ObjectCreateMessageParent(weapon_guid, weapon_slot), + ammo_data + ) + ) + sendResponse(ChangeAmmoMessage(weapon_guid, 1)) + + case VehicleResponse.WeaponDryFire(weaponGuid) if isNotSameTarget => + continent.GUID(weaponGuid).collect { + case tool: Tool if tool.Magazine == 0 => + // check that the magazine is still empty before sending WeaponDryFireMessage + // if it has been reloaded since then, other clients will not see it firing + sendResponse(WeaponDryFireMessage(weaponGuid)) + } + case VehicleResponse.DismountVehicle(bailType, wasKickedByDriver) if isNotSameTarget => sendResponse(DismountVehicleMsg(guid, bailType, wasKickedByDriver)) @@ -330,10 +360,10 @@ class SessionVehicleHandlers( } private def changeLoadoutDeleteOldEquipment( - vehicle: Vehicle, - oldWeapons: Iterable[(Equipment, PlanetSideGUID)], - oldInventory: Iterable[(Equipment, PlanetSideGUID)] - ): Unit = { + vehicle: Vehicle, + oldWeapons: Iterable[(Equipment, PlanetSideGUID)], + oldInventory: Iterable[(Equipment, PlanetSideGUID)] + ): Unit = { vehicle.PassengerInSeat(player) match { case Some(seatNum) => //participant: observe changes to equipment diff --git a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala index f062043c8..47ef74692 100644 --- a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala @@ -271,10 +271,6 @@ class VehicleOperations( sessionData.validObject(mountable_guid, decorator = "MountVehicle").collect { case obj: Mountable => obj.Actor ! Mountable.TryMount(player, entry_point) - case _: Mountable => - log.warn( - s"DismountVehicleMsg: ${player.Name} can not mount while server has asserted control; please wait" - ) case _ => log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}") } diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index b6407e45a..c9bab2145 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -74,19 +74,28 @@ private[support] class WeaponAndProjectileOperations( def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = { val WeaponDryFireMessage(weapon_guid) = pkt - FindWeapon + val (containerOpt, tools) = FindContainedWeapon + tools .find { _.GUID == weapon_guid } - .orElse { continent.GUID(weapon_guid) } match { - case Some(_: Equipment) => - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.WeaponDryFire(player.GUID, weapon_guid) - ) - case _ => + .orElse { continent.GUID(weapon_guid) } + .collect { + case _: Equipment if containerOpt.exists(_.isInstanceOf[Player]) => + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.WeaponDryFire(player.GUID, weapon_guid) + ) + case _: Equipment => + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.WeaponDryFire(player.GUID, weapon_guid) + ) + } + .orElse { log.warn( s"WeaponDryFire: ${player.Name}'s weapon ${weapon_guid.guid} is either not a weapon or does not exist" ) - } + None + } } def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { @@ -111,45 +120,16 @@ private[support] class WeaponAndProjectileOperations( val ChangeFireStateMessage_Start(item_guid) = pkt if (shooting.isEmpty) { sessionData.findEquipment(item_guid) match { + case Some(tool: Tool) if player.VehicleSeated.isEmpty => + fireStateStartWhenPlayer(tool, item_guid) case Some(tool: Tool) => - if (tool.FireMode.RoundsPerShot == 0 || tool.Magazine > 0 || prefire.contains(item_guid)) { - prefire -= item_guid - shooting += item_guid - shootingStart += item_guid -> System.currentTimeMillis() - ongoingShotsFired = 0 - //special case - suppress the decimator's alternate fire mode, by projectile - if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Start(player.GUID, item_guid) - ) - } - //charge ammunition drain - tool.FireMode match { - case mode: ChargeFireModeDefinition => - sessionData.progressBarValue = Some(0f) - sessionData.progressBarUpdate = context.system.scheduler.scheduleOnce( - (mode.Time + mode.DrainInterval) milliseconds, - context.self, - SessionActor.ProgressEvent(1f, () => {}, Tools.ChargeFireMode(player, tool), mode.DrainInterval) - ) - case _ => ; - } - } else { - log.warn( - s"ChangeFireState_Start: ${player.Name}'s ${tool.Definition.Name} magazine was empty before trying to shoot" - ) - EmptyMagazine(item_guid, tool) - } - case Some(_) => //permissible, for now - prefire -= item_guid - shooting += item_guid - shootingStart += item_guid -> System.currentTimeMillis() - ongoingShotsFired = 0 - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Start(player.GUID, item_guid) - ) + fireStateStartWhenMounted(tool, item_guid) + case Some(_) if player.VehicleSeated.isEmpty => + fireStateStartSetup(item_guid) + fireStateStartPlayerMessages(item_guid) + case Some(_) => + fireStateStartSetup(item_guid) + fireStateStartMountedMessages(item_guid) case None => log.warn(s"ChangeFireState_Start: can not find $item_guid") } @@ -162,48 +142,23 @@ private[support] class WeaponAndProjectileOperations( prefire -= item_guid shootingStop += item_guid -> now shooting -= item_guid - val pguid = player.GUID sessionData.findEquipment(item_guid) match { + case Some(tool: Tool) if player.VehicleSeated.isEmpty => + fireStateStopWhenPlayer(tool, item_guid) case Some(tool: Tool) => - //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why - if ( - tool.Definition == GlobalDefinitions.phoenix && - tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile - ) { - //suppress the decimator's alternate fire mode, however - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Start(pguid, item_guid) - ) - shootingStart += item_guid -> (now - 1L) - } - avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(tool.Definition.ObjectId,ongoingShotsFired,0,0)) - tool.FireMode match { - case _: ChargeFireModeDefinition => - sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) - case _ => ; - } - if (tool.Magazine == 0) { - FireCycleCleanup(tool) - } - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Stop(pguid, item_guid) - ) - + fireStateStopWhenMounted(tool, item_guid) case Some(trigger: BoomerTrigger) => - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Start(pguid, item_guid) - ) - continent.GUID(trigger.Companion) match { - case Some(boomer: BoomerDeployable) => + fireStateStopPlayerMessages(item_guid) + continent.GUID(trigger.Companion).collect { + case boomer: BoomerDeployable => boomer.Actor ! CommonMessages.Use(player, Some(trigger)) - case Some(_) | None => ; } - - case _ => ; - //log.warn(s"ChangeFireState_Stop: ${player.Name} never started firing item ${item_guid.guid} in the first place?") + case Some(_) if player.VehicleSeated.isEmpty => + fireStateStopPlayerMessages(item_guid) + case Some(_) => + fireStateStopMountedMessages(item_guid) + case _ => () + log.warn(s"ChangeFireState_Stop: can not find $item_guid") } sessionData.progressBarUpdate.cancel() sessionData.progressBarValue = None @@ -212,54 +167,10 @@ private[support] class WeaponAndProjectileOperations( def handleReload(pkt: ReloadMessage): Unit = { val ReloadMessage(item_guid, _, unk1) = pkt FindContainedWeapon match { + case (Some(obj: Player), tools) => + handleReloadWhenPlayer(item_guid, obj, tools, unk1) case (Some(obj: PlanetSideServerObject with Container), tools) => - tools.filter { _.GUID == item_guid }.foreach { tool => - val currentMagazine : Int = tool.Magazine - val magazineSize : Int = tool.MaxMagazine - val reloadValue : Int = magazineSize - currentMagazine - if (magazineSize > 0 && reloadValue > 0) { - FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match { - case Nil => ; - case x :: xs => - val (deleteFunc, modifyFunc) : (Equipment => Future[Any], (AmmoBox, Int) => Unit) = obj match { - case veh : Vehicle => - (RemoveOldEquipmentFromInventory(veh)(_), ModifyAmmunitionInVehicle(veh)(_, _)) - case _ => - (RemoveOldEquipmentFromInventory(obj)(_), ModifyAmmunition(obj)(_, _)) - } - xs.foreach { item => deleteFunc(item.obj) } - val box = x.obj.asInstanceOf[AmmoBox] - val tailReloadValue : Int = if (xs.isEmpty) { - 0 - } - else { - xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum - } - val sumReloadValue : Int = box.Capacity + tailReloadValue - val actualReloadValue = if (sumReloadValue <= reloadValue) { - deleteFunc(box) - sumReloadValue - } - else { - modifyFunc(box, reloadValue - tailReloadValue) - reloadValue - } - val finalReloadValue = actualReloadValue + currentMagazine - log.info( - s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}" - ) - tool.Magazine = finalReloadValue - sendResponse(ReloadMessage(item_guid, finalReloadValue, unk1)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.Reload(player.GUID, item_guid) - ) - } - } else { - //the weapon can not reload due to full magazine; the UI for the magazine is obvious bugged, so fix it - sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, magazineSize)) - } - } + handleReloadWhenMountable(item_guid, obj, tools, unk1) case (_, _) => log.warn(s"ReloadMessage: either can not find $item_guid or the object found was not a Tool") } @@ -272,17 +183,19 @@ private[support] class WeaponAndProjectileOperations( log.warn(s"ChangeAmmo: either can not find $item_guid or the object found was not Equipment") } else { equipment foreach { - case obj : ConstructionItem => + case obj: ConstructionItem => if (Deployables.performConstructionItemAmmoChange(player.avatar.certifications, obj, obj.AmmoTypeIndex)) { log.info( s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${obj.AmmoType} (option #${obj.FireModeIndex})" ) sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) } - case tool : Tool => + case tool: Tool => thing match { - case Some(correctThing: PlanetSideServerObject with Container) => - PerformToolAmmoChange(tool, correctThing) + case Some(player: Player) => + PerformToolAmmoChange(tool, player, ModifyAmmunition(player)) + case Some(mountable: PlanetSideServerObject with Container) => + PerformToolAmmoChange(tool, mountable, ModifyAmmunitionInMountable(mountable)) case _ => log.warn(s"ChangeAmmo: the ${thing.get.Definition.Name} in ${player.Name}'s is not the correct type") } @@ -804,7 +717,7 @@ private[support] class WeaponAndProjectileOperations( * @param reloadValue the value to modify the `AmmoBox`; * subtracted from the current `Capacity` of `Box` */ - def ModifyAmmunitionInVehicle(obj: Vehicle)(box: AmmoBox, reloadValue: Int): Unit = { + def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { ModifyAmmunition(obj)(box, reloadValue) obj.Find(box) match { case Some(index) => @@ -827,19 +740,19 @@ private[support] class WeaponAndProjectileOperations( * @param tool na * @param obj na */ - def PerformToolAmmoChange(tool: Tool, obj: PlanetSideServerObject with Container): Unit = { + def PerformToolAmmoChange( + tool: Tool, + obj: PlanetSideServerObject with Container, + modifyFunc: (AmmoBox, Int) => Unit + ): Unit = { val originalAmmoType = tool.AmmoType do { val requestedAmmoType = tool.NextAmmoType val fullMagazine = tool.MaxMagazine if (requestedAmmoType != tool.AmmoSlot.Box.AmmoType) { FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match { - case Nil => ; + case Nil => () case x :: xs => - val modifyFunc: (AmmoBox, Int) => Unit = obj match { - case veh: Vehicle => ModifyAmmunitionInVehicle(veh) - case _ => ModifyAmmunition(obj) - } val stowNewFunc: Equipment => TaskBundle = PutNewEquipmentInInventoryOrDrop(obj) val stowFunc: Equipment => Future[Any] = PutEquipmentInInventoryOrDrop(obj) @@ -1231,13 +1144,233 @@ private[support] class WeaponAndProjectileOperations( */ def FindWeapon: Set[Tool] = FindContainedWeapon._2 + /* + used by ChangeFireStateMessage_Start handling + */ + private def fireStateStartSetup(itemGuid: PlanetSideGUID): Unit = { + prefire -= itemGuid + shooting += itemGuid + shootingStart += itemGuid -> System.currentTimeMillis() + ongoingShotsFired = 0 + } + + private def fireStateStartChargeMode(tool: Tool): Unit = { + //charge ammunition drain + tool.FireMode match { + case mode: ChargeFireModeDefinition => + sessionData.progressBarValue = Some(0f) + sessionData.progressBarUpdate = context.system.scheduler.scheduleOnce( + (mode.Time + mode.DrainInterval) milliseconds, + context.self, + SessionActor.ProgressEvent(1f, () => {}, Tools.ChargeFireMode(player, tool), mode.DrainInterval) + ) + case _ => () + } + } + + private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) + ) + } + + private def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = { + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.ChangeFireState_Start(player.GUID, itemGuid) + ) + } + + private def allowFireStateChangeStart(tool: Tool, itemGuid: PlanetSideGUID): Boolean = { + tool.FireMode.RoundsPerShot == 0 || tool.Magazine > 0 || prefire.contains(itemGuid) + } + + private def enforceEmptyMagazine(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + log.warn( + s"ChangeFireState_Start: ${player.Name}'s ${tool.Definition.Name} magazine was empty before trying to shoot" + ) + EmptyMagazine(itemGuid, tool) + } + + private def fireStateStartWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + if (allowFireStateChangeStart(tool, itemGuid)) { + fireStateStartSetup(itemGuid) + //special case - suppress the decimator's alternate fire mode, by projectile + if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) { + fireStateStartPlayerMessages(itemGuid) + } + fireStateStartChargeMode(tool) + } else { + enforceEmptyMagazine(tool, itemGuid) + } + } + + private def fireStateStartWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + if (allowFireStateChangeStart(tool, itemGuid)) { + fireStateStartSetup(itemGuid) + fireStateStartMountedMessages(itemGuid) + fireStateStartChargeMode(tool) + } else { + enforceEmptyMagazine(tool, itemGuid) + } + } + + /* + used by ChangeFireStateMessage_Stop handling + */ + private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { + avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(tool.Definition.ObjectId, ongoingShotsFired, 0, 0)) + tool.FireMode match { + case _: ChargeFireModeDefinition => + sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) + case _ => ; + } + if (tool.Magazine == 0) { + FireCycleCleanup(tool) + } + } + + private def fireStateStopPlayerMessages(itemGuid: PlanetSideGUID): Unit = { + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeFireState_Stop(player.GUID, itemGuid) + ) + } + + private def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = { + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.ChangeFireState_Stop(player.GUID, itemGuid) + ) + } + + private def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why + //suppress the decimator's alternate fire mode, however + if ( + tool.Definition == GlobalDefinitions.phoenix && + tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile + ) { + fireStateStartPlayerMessages(itemGuid) + } + fireStateStopUpdateChargeAndCleanup(tool) + fireStateStopPlayerMessages(itemGuid) + } + + private def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + fireStateStopUpdateChargeAndCleanup(tool) + fireStateStopMountedMessages(itemGuid) + } + + /* + used by ReloadMessage handling + */ + private def reloadPlayerMessages(itemGuid: PlanetSideGUID): Unit = { + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.Reload(player.GUID, itemGuid) + ) + } + + private def reloadVehicleMessages(itemGuid: PlanetSideGUID): Unit = { + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.Reload(player.GUID, itemGuid) + ) + } + + private def handleReloadProcedure( + itemGuid: PlanetSideGUID, + obj: PlanetSideGameObject with Container, + tools: Set[Tool], + unk1: Int, + deleteFunc: Equipment => Future[Any], + modifyFunc: (AmmoBox, Int) => Unit, + messageFunc: PlanetSideGUID => Unit + ): Unit = { + tools + .filter { _.GUID == itemGuid } + .foreach { tool => + val currentMagazine : Int = tool.Magazine + val magazineSize : Int = tool.MaxMagazine + val reloadValue : Int = magazineSize - currentMagazine + if (magazineSize > 0 && reloadValue > 0) { + FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match { + case Nil => () + case x :: xs => + xs.foreach { item => deleteFunc(item.obj) } + val box = x.obj.asInstanceOf[AmmoBox] + val tailReloadValue : Int = if (xs.isEmpty) { + 0 + } + else { + xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum + } + val sumReloadValue : Int = box.Capacity + tailReloadValue + val actualReloadValue = if (sumReloadValue <= reloadValue) { + deleteFunc(box) + sumReloadValue + } + else { + modifyFunc(box, reloadValue - tailReloadValue) + reloadValue + } + val finalReloadValue = actualReloadValue + currentMagazine + log.info( + s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}" + ) + tool.Magazine = finalReloadValue + sendResponse(ReloadMessage(itemGuid, finalReloadValue, unk1)) + messageFunc(itemGuid) + } + } else { + //the weapon can not reload due to full magazine; the UI for the magazine is obvious bugged, so fix it + sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, magazineSize)) + } + } + } + + private def handleReloadWhenPlayer( + itemGuid: PlanetSideGUID, + obj: Player, + tools: Set[Tool], + unk1: Int + ): Unit = { + handleReloadProcedure( + itemGuid, + obj, + tools, + unk1, + RemoveOldEquipmentFromInventory(obj)(_), + ModifyAmmunition(obj)(_, _), + reloadPlayerMessages + ) + } + + private def handleReloadWhenMountable( + itemGuid: PlanetSideGUID, + obj: PlanetSideServerObject with Container, + tools: Set[Tool], + unk1: Int + ): Unit = { + handleReloadProcedure( + itemGuid, + obj, + tools, + unk1, + RemoveOldEquipmentFromInventory(obj)(_), + ModifyAmmunitionInMountable(obj)(_, _), + reloadVehicleMessages + ) + } + override protected[session] def stop(): Unit = { if (player != null && player.HasGUID) { (prefire ++ shooting).foreach { guid => - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Stop(player.GUID, guid) - ) + //do I need to do this? (maybe) + fireStateStopPlayerMessages(guid) + fireStateStopMountedMessages(guid) } projectiles.indices.foreach { projectiles.update(_, None) } } diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 78b770939..a5910e792 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -8156,7 +8156,7 @@ object GlobalDefinitions { lightgunship.DrownAtMaxDepth = true lightgunship.MaxDepth = 2 //flying vehicles will automatically disable lightgunship.Geometry = GeometryForm.representByCylinder(radius = 2.375f, height = 1.98438f) - lightgunship.collision.avatarCollisionDamageMax = 750 + lightgunship.collision.avatarCollisionDamageMax = 75 lightgunship.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 60), (0.5f, 120), (0.75f, 180), (1f, 250))) lightgunship.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) lightgunship.maxForwardSpeed = 104f diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index c17c9cc1e..6fb9d8cba 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -17,7 +17,7 @@ class AvatarService(zone: Zone) extends Actor { val AvatarEvents = new GenericEventBus[AvatarServiceResponse] //AvatarEventBus - def receive = { + def receive: Receive = { case Service.Join(channel) => val path = s"/$channel/Avatar" val who = sender() diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala index d4d18a491..35b377981 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala @@ -16,7 +16,7 @@ class VehicleService(zone: Zone) extends Actor { val VehicleEvents = new GenericEventBus[VehicleServiceResponse] - def receive = { + def receive: Receive = { case Service.Join(channel) => val path = s"/$channel/Vehicle" VehicleEvents.subscribe(sender(), path) @@ -33,6 +33,26 @@ class VehicleService(zone: Zone) extends Actor { case VehicleServiceMessage(forChannel, action) => action match { + case VehicleAction.ChangeAmmo(player_guid, weapon_guid, weapon_slot, old_ammo_guid, ammo_id, ammo_guid, ammo_data) => + VehicleEvents.publish( + VehicleServiceResponse( + s"/$forChannel/Vehicle", + player_guid, + VehicleResponse.ChangeAmmo(weapon_guid, weapon_slot, old_ammo_guid, ammo_id, ammo_guid, ammo_data) + ) + ) + case VehicleAction.ChangeFireState_Start(player_guid, weapon_guid) => + VehicleEvents.publish( + VehicleServiceResponse( + s"/$forChannel/Vehicle", + player_guid, + VehicleResponse.ChangeFireState_Start(weapon_guid) + ) + ) + case VehicleAction.ChangeFireState_Stop(player_guid, weapon_guid) => + VehicleEvents.publish( + VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.ChangeFireState_Stop(weapon_guid)) + ) case VehicleAction.ChildObjectState(player_guid, object_guid, pitch, yaw) => VehicleEvents.publish( VehicleServiceResponse( @@ -176,6 +196,11 @@ class VehicleService(zone: Zone) extends Actor { VehicleResponse.PlanetsideAttribute(target_guid, attribute_type, attribute_value) ) ) + + case VehicleAction.Reload(player_guid, weapon_guid) => + VehicleEvents.publish( + VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.Reload(weapon_guid)) + ) case VehicleAction.SeatPermissions(player_guid, vehicle_guid, seat_group, permission) => VehicleEvents.publish( VehicleServiceResponse( @@ -199,6 +224,10 @@ class VehicleService(zone: Zone) extends Actor { ) ) ) + case VehicleAction.WeaponDryFire(player_guid, weapon_guid) => + VehicleEvents.publish( + VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.WeaponDryFire(weapon_guid)) + ) case VehicleAction.UnloadVehicle(player_guid, vehicle, vehicle_guid) => VehicleEvents.publish( VehicleServiceResponse( diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala b/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala index 49c49b81c..979d179f9 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala @@ -23,6 +23,17 @@ object VehicleServiceMessage { object VehicleAction { trait Action + final case class ChangeAmmo( + player_guid: PlanetSideGUID, + weapon_guid: PlanetSideGUID, + weapon_slot: Int, + old_ammo_guid: PlanetSideGUID, + 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 ChildObjectState(player_guid: PlanetSideGUID, object_guid: PlanetSideGUID, pitch: Float, yaw: Float) extends Action final case class DeployRequest( @@ -89,6 +100,7 @@ object VehicleAction { attribute_type: Int, attribute_value: Long ) extends Action + final case class Reload(player_guid: PlanetSideGUID, weapon_guid: PlanetSideGUID) extends Action final case class SeatPermissions( player_guid: PlanetSideGUID, vehicle_guid: PlanetSideGUID, @@ -118,6 +130,7 @@ object VehicleAction { unk6: Boolean ) extends Action final case class SendResponse(player_guid: PlanetSideGUID, msg: PlanetSideGamePacket) extends Action + final case class WeaponDryFire(player_guid: PlanetSideGUID, weapon_guid: PlanetSideGUID) extends Action final case class UpdateAmsSpawnPoint(zone: Zone) extends Action final case class TransferPassengerChannel( diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala b/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala index a22e3129c..e64c6536e 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala @@ -22,6 +22,16 @@ final case class VehicleServiceResponse( object VehicleResponse { trait Response + final case class ChangeAmmo( + weapon_guid: PlanetSideGUID, + weapon_slot: Int, + old_ammo_guid: PlanetSideGUID, + 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 ChildObjectState(object_guid: PlanetSideGUID, pitch: Float, yaw: Float) extends Response final case class ConcealPlayer(player_guid: PlanetSideGUID) extends Response final case class DeployRequest( @@ -66,8 +76,9 @@ object VehicleResponse { final case class Ownership(vehicle_guid: PlanetSideGUID) extends Response final case class PlanetsideAttribute(vehicle_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long) extends Response - final case class RevealPlayer(player_guid: PlanetSideGUID) extends Response - final case class SeatPermissions(vehicle_guid: PlanetSideGUID, seat_group: Int, permission: Long) extends Response + final case class Reload(weapon_guid: PlanetSideGUID) extends Response + final case class RevealPlayer(player_guid: PlanetSideGUID) extends Response + final case class SeatPermissions(vehicle_guid: PlanetSideGUID, seat_group: Int, permission: Long) extends Response final case class StowEquipment( vehicle_guid: PlanetSideGUID, slot: Int, @@ -75,6 +86,7 @@ object VehicleResponse { iguid: PlanetSideGUID, idata: ConstructorData ) extends Response + final case class WeaponDryFire(weapon_guid: PlanetSideGUID) extends Response final case class UnloadVehicle(vehicle: Vehicle, vehicle_guid: PlanetSideGUID) extends Response final case class UnstowEquipment(item_guid: PlanetSideGUID) extends Response final case class VehicleState(