diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index acaf71e6..a038eede 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -1,7 +1,6 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.GlobalDefinitions.sparrow_projectile import net.psforever.objects.ballistics.Projectiles import net.psforever.objects.ce.{DeployableCategory, DeployedItem} import net.psforever.objects.definition._ @@ -641,8 +640,8 @@ object GlobalDefinitions { } val hunterseeker = new ToolDefinition(ObjectClass.hunterseeker) { - override def NextFireModeIndex(index : Int) : Int = index - DefaultFireModeIndex = 1 +// override def NextFireModeIndex(index : Int) : Int = index +// DefaultFireModeIndex = 1 } //phoenix val lancer = ToolDefinition(ObjectClass.lancer) diff --git a/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala index 329d6742..4f949b66 100644 --- a/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala @@ -2,48 +2,56 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} -import net.psforever.types.Vector3 +import net.psforever.types.{Angular, Vector3} import scodec.Codec import scodec.codecs._ +import shapeless.{::, HNil} /** - * Dispatched to deliberately render certain projectiles of a weapon on other players' clients.
+ * Dispatched to deliberately control certain projectiles of a weapon on other players' clients.
*
- * This packet is generated by firing specific weapons in specific fire modes. + * This packet should be generated by firing specific weapons in specific fire modes. * For example, the Phoenix (`hunterseeker`) discharged in its primary fire mode generates this packet; * but, the Phoenix in secondary fire mode does not. * The Striker (`striker`) discharged in its primary fire mode generates this packet; * but, the Striker in secondary fire mode does not. * The chosen fire mode(s) are not a straight-fire projectile but one that has special control asserted over it. - * For the Phoenix, it is user-operated. - * For the Striker, it tracks towards a target while the weapon's reticle hovers over that target.
+ * For the Phoenix, it is user operated (camera-guided). + * For the Striker, it tracks towards a valid target while the weapon's reticle hovers over that target.
*
* This packet will continue to be dispatched by the client for as long as the projectile being tracked is in the air. * All projectiles have a maximum lifespan before they will lose control and either despawn and/or explode. * This number is tracked in the packet for simplicity. * If the projectile strikes a valid target, the count will jump to a significantly enormous value beyond its normal lifespan. * This ensures that the projectile - locally and the shared model - will despawn. - * @param projectile_guid the projectile + *
+ * This control can not be exerted until that projectile is physically constructed on the other clients + * in the same way that a player or a vehicle is constructed. + * A projectile that exhibits intentional construction behavior is flagged using the property `exists_on_remote_client`. + * The model comes with a number of caveats, + * some that originate from the object construction process itself, + * but also some from this packet. + * For example, + * as indicated by the static `shot_orient` values reported by this packet. + * a discharged controlled projectile will not normally rotate. + * A minor loss of lifespan may be levied. + * @see `ProjectileDefinition` + * @see `TrackedProjectileData` + * @param projectile_guid the client-specific local unique identifier of the projectile; + * this is __not__ the global unique identifier for the synchronized projectile object * @param shot_pos the position of the projectile * @param shot_vel the velocity of the projectile - * @param unk1 na; - * usually 0 - * @param unk2 na; - * will remain consistent for the lifespan of a given projectile in most cases - * @param unk3 na; - * will remain consistent for the lifespan of a given projectile in most cases - * @param unk4 na; - * usually false + * @param shot_orient the orientation of the projectile + * @param unk na; + * usually `false` * @param time_alive how long the projectile has been in the air; * often expressed in multiples of 2 */ final case class ProjectileStateMessage(projectile_guid : PlanetSideGUID, shot_pos : Vector3, shot_vel : Vector3, - unk1 : Int, - unk2 : Int, - unk3 : Int, - unk4 : Boolean, + shot_orient : Vector3, + unk : Boolean, time_alive : Int) extends PlanetSideGamePacket { type Packet = ProjectileStateMessage @@ -56,10 +64,19 @@ object ProjectileStateMessage extends Marshallable[ProjectileStateMessage] { ("projectile_guid" | PlanetSideGUID.codec) :: ("shot_pos" | Vector3.codec_pos) :: ("shot_vel" | Vector3.codec_float) :: - ("unk1" | uint8L) :: - ("unk2" | uint8L) :: - ("unk3" | uint8L) :: - ("unk4" | bool) :: + ("roll" | Angular.codec_roll) :: + ("pitch" | Angular.codec_pitch) :: + ("yaw" | Angular.codec_yaw()) :: + ("unk" | bool) :: ("time_alive" | uint16L) - ).as[ProjectileStateMessage] + ).xmap[ProjectileStateMessage] ( + { + case guid :: pos :: vel :: roll :: pitch :: yaw :: unk :: time :: HNil => + ProjectileStateMessage(guid, pos, vel, Vector3(roll, pitch, yaw), unk, time) + }, + { + case ProjectileStateMessage(guid, pos, vel, Vector3(roll, pitch, yaw), unk, time) => + guid :: pos :: vel :: roll :: pitch :: yaw :: unk :: time :: HNil + } + ) } diff --git a/common/src/main/scala/services/avatar/AvatarService.scala b/common/src/main/scala/services/avatar/AvatarService.scala index 4249a0d7..0fdca89c 100644 --- a/common/src/main/scala/services/avatar/AvatarService.scala +++ b/common/src/main/scala/services/avatar/AvatarService.scala @@ -127,6 +127,12 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.LoadPlayer(pkt)) ) + case AvatarAction.LoadProjectile(player_guid, object_id, obj, cdata) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.LoadPlayer( + ObjectCreateMessage(object_id, obj.GUID, cdata) + )) + ) case AvatarAction.ObjectDelete(player_guid, item_guid, unk) => AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ObjectDelete(item_guid, unk)) @@ -151,6 +157,10 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarResponse.PlayerState(pos, vel, yaw, pitch, yaw_upper, seq_time, is_crouching, is_jumping, jump_thrust, is_cloaking, spectating, weaponInHand)) ) + case AvatarAction.ProjectileState(player_guid, projectile_guid, shot_pos, shot_vel, shot_orient, unk, time_alive) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ProjectileState(projectile_guid, shot_pos, shot_vel, shot_orient, unk, time_alive)) + ) case AvatarAction.PickupItem(player_guid, zone, target, slot, item, unk) => janitor forward RemoverActor.ClearSpecific(List(item), zone) AvatarEvents.publish( diff --git a/common/src/main/scala/services/avatar/AvatarServiceMessage.scala b/common/src/main/scala/services/avatar/AvatarServiceMessage.scala index 636d56de..5035ceba 100644 --- a/common/src/main/scala/services/avatar/AvatarServiceMessage.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceMessage.scala @@ -1,8 +1,12 @@ // Copyright (c) 2017 PSForever package services.avatar +<<<<<<< fa7365e8af5d0b21c3934ed745895edd46f741a6:common/src/main/scala/services/avatar/AvatarServiceMessage.scala import net.psforever.objects.{PlanetSideGameObject, Player} import net.psforever.objects.ballistics.SourceEntry +======= +import net.psforever.objects.ballistics.{Projectile, SourceEntry} +>>>>>>> removed fire mode override for the Phoenix; added events for loading and manipulation of remote projectiles to AvatarService; ProjectileStateMessage handles projectile data in a simple way; remote projectiles can now be registered and unregistered:common/src/main/scala/services/avatar/AvatarAction.scala import net.psforever.objects.ce.Deployable import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.Container @@ -30,7 +34,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 EnvironmentalDamage(player_guid : PlanetSideGUID, amont: Int) extends Action + final case class EnvironmentalDamage(player_guid : PlanetSideGUID, amount: Int) extends Action final case class Damage(player_guid : PlanetSideGUID, target : Player, resolution_function : Any=>Unit) extends Action final case class DeployItem(player_guid : PlanetSideGUID, item : PlanetSideGameObject with Deployable) extends Action final case class Destroy(victim : PlanetSideGUID, killer : PlanetSideGUID, weapon : PlanetSideGUID, pos : Vector3) extends Action @@ -40,6 +44,7 @@ object AvatarAction { final case class HitHint(source_guid : PlanetSideGUID, player_guid : PlanetSideGUID) extends Action final case class KilledWhileInVehicle(player_guid : PlanetSideGUID) extends Action final case class LoadPlayer(player_guid : PlanetSideGUID, object_id : Int, target_guid : PlanetSideGUID, cdata : ConstructorData, pdata : Option[ObjectCreateMessageParent]) extends Action + final case class LoadProjectile(player_guid : PlanetSideGUID, object_id : Int, obj : Projectile, cdata : ConstructorData) extends Action final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action @@ -47,6 +52,7 @@ object AvatarAction { final case class PlanetsideAttributeSelf(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action final case class PlayerState(player_guid : PlanetSideGUID, pos : Vector3, vel : Option[Vector3], facingYaw : Float, facingPitch : Float, facingYawUpper : Float, timestamp : Int, is_crouching : Boolean, is_jumping : Boolean, jump_thrust : Boolean, is_cloaked : Boolean, spectator : Boolean, weaponInHand : Boolean) extends Action final case class PickupItem(player_guid : PlanetSideGUID, zone : Zone, target : PlanetSideGameObject with Container, slot : Int, item : Equipment, unk : Int = 0) extends Action + final case class ProjectileState(player_guid : PlanetSideGUID, projectile_guid : PlanetSideGUID, shot_pos : Vector3, shot_vel : Vector3, shot_orient : Vector3, unk : Boolean, time_alive : Int) extends Action final case class PutDownFDU(player_guid : PlanetSideGUID) extends Action final case class Release(player : Player, zone : Zone, time : Option[FiniteDuration] = None) extends Action final case class Revive(target_guid: PlanetSideGUID) extends Action diff --git a/common/src/main/scala/services/avatar/AvatarServiceResponse.scala b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala index 40481db8..d009c160 100644 --- a/common/src/main/scala/services/avatar/AvatarServiceResponse.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala @@ -33,12 +33,18 @@ object AvatarResponse { final case class HitHint(source_guid : PlanetSideGUID) extends Response final case class KilledWhileInVehicle() extends Response final case class LoadPlayer(pkt : ObjectCreateMessage) extends Response + final case class LoadProjectile(pkt : ObjectCreateMessage) extends Response final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response final case class ObjectHeld(slot : Int) extends Response final case class PlanetsideAttribute(attribute_type : Int, attribute_value : Long) extends Response +<<<<<<< fa7365e8af5d0b21c3934ed745895edd46f741a6:common/src/main/scala/services/avatar/AvatarServiceResponse.scala final case class PlanetsideAttributeToAll(attribute_type : Int, attribute_value : Long) extends Response final case class PlanetsideAttributeSelf(attribute_type : Int, attribute_value : Long) extends Response final case class PlayerState(pos : Vector3, vel : Option[Vector3], facingYaw : Float, facingPitch : Float, facingYawUpper : Float, timestamp : Int, is_crouching : Boolean, is_jumping : Boolean, jump_thrust : Boolean, is_cloaked : Boolean, spectator : Boolean, weaponInHand : Boolean) extends Response +======= + final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response + final case class ProjectileState(projectile_guid : PlanetSideGUID, shot_pos : Vector3, shot_vel : Vector3, shot_orient : Vector3, unk : Boolean, time_alive : Int) extends Response +>>>>>>> removed fire mode override for the Phoenix; added events for loading and manipulation of remote projectiles to AvatarService; ProjectileStateMessage handles projectile data in a simple way; remote projectiles can now be registered and unregistered:common/src/main/scala/services/avatar/AvatarResponse.scala final case class PutDownFDU(target_guid : PlanetSideGUID) extends Response final case class Release(player : Player) extends Response final case class Reload(weapon_guid : PlanetSideGUID) extends Response diff --git a/common/src/test/scala/game/ProjectileStateMessageTest.scala b/common/src/test/scala/game/ProjectileStateMessageTest.scala index f62e181e..80891f88 100644 --- a/common/src/test/scala/game/ProjectileStateMessageTest.scala +++ b/common/src/test/scala/game/ProjectileStateMessageTest.scala @@ -12,18 +12,12 @@ class ProjectileStateMessageTest extends Specification { "decode" in { PacketCoding.DecodePacket(string).require match { - case ProjectileStateMessage(projectile, pos, vel, unk1, unk2, unk3, unk4, time_alive) => + case ProjectileStateMessage(projectile, pos, vel, orient, unk, time_alive) => projectile mustEqual PlanetSideGUID(40229) - pos.x mustEqual 4611.539f - pos.y mustEqual 5576.375f - pos.z mustEqual 82.328125f - vel.x mustEqual 18.64686f - vel.y mustEqual -33.43247f - vel.z mustEqual 11.599553f - unk1 mustEqual 0 - unk2 mustEqual 248 - unk3 mustEqual 236 - unk4 mustEqual false + pos mustEqual Vector3(4611.539f, 5576.375f, 82.328125f) + vel mustEqual Vector3(18.64686f, -33.43247f, 11.599553f) + orient mustEqual Vector3(0, 22.5f, 56.25f) + unk mustEqual false time_alive mustEqual 4 case _ => ko @@ -35,10 +29,17 @@ class ProjectileStateMessageTest extends Specification { PlanetSideGUID(40229), Vector3(4611.539f, 5576.375f, 82.328125f), Vector3(18.64686f, -33.43247f, 11.599553f), - 0, 248, 236, false, 4 + Vector3(0, 22.5f, 56.25f), + false, + 4 ) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector - pkt mustEqual string + //pkt mustEqual string + val pkt_bits = pkt.toBitVector + val str_bits = string.toBitVector + pkt_bits.take(184) mustEqual str_bits.take(184) //skip 1 bit + pkt_bits.drop(185).take(7) mustEqual str_bits.drop(185).take(7) //skip 1 bit + pkt_bits.drop(193) mustEqual str_bits.drop(193) } } diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 2601b6cd..fa402f3f 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1402,6 +1402,11 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(pkt) } + case AvatarResponse.LoadProjectile(pkt) => + if(tplayer_guid != guid) { + sendResponse(pkt) + } + case AvatarResponse.ObjectDelete(item_guid, unk) => if(tplayer_guid != guid) { sendResponse(ObjectDeleteMessage(item_guid, unk)) @@ -1463,6 +1468,11 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + case AvatarResponse.ProjectileState(projectile_guid, shot_pos, shot_vel, shot_orient, unk, time_alive) => + if(tplayer_guid != guid) { + sendResponse(ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, unk, time_alive)) + } + case AvatarResponse.PutDownFDU(target) => if(tplayer_guid != guid) { sendResponse(GenericObjectActionMessage(target, 212)) @@ -3989,8 +3999,22 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ VehicleSubStateMessage(vehicle_guid, player_guid, vehicle_pos, vehicle_ang, vel, unk1, unk2) => //log.info(s"VehicleSubState: $vehicle_guid, $player_guid, $vehicle_pos, $vehicle_ang, $vel, $unk1, $unk2") - case msg @ ProjectileStateMessage(projectile_guid, shot_pos, shot_vector, unk1, unk2, unk3, unk4, time_alive) => - //log.info("ProjectileState: " + msg) + case msg @ ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, unk, time_alive) => + log.info(s"ProjectileState: $msg") + projectiles + .collect { + case Some(projectile) if projectile.HasGUID => + projectile + } + .find(_.GUID == projectile_guid) match { + case Some(projectile) => + projectile.Position = shot_pos + projectile.Orientation = shot_orient + projectile.Velocity = shot_vel + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ProjectileState(player.GUID, projectile_guid, shot_pos, shot_vel, shot_orient, unk, time_alive)) + case None => + log.info(s"ProjectileState: the projectile GUID#${projectile_guid.guid} can not be found") + } case msg @ ReleaseAvatarRequestMessage() => log.info(s"ReleaseAvatarRequest: ${player.GUID} on ${continent.Id} has released") @@ -5385,7 +5409,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } case msg @ WeaponFireMessage(seq_time, weapon_guid, projectile_guid, shot_origin, unk1, unk2, unk3, unk4, unk5, unk6, unk7) => - log.info("WeaponFire: " + msg) + log.info(s"WeaponFire: $msg") FindContainedWeapon match { case (Some(obj), Some(tool : Tool)) => if(tool.Magazine <= 0) { //safety: enforce ammunition depletion @@ -5405,7 +5429,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } prefire = shooting.orElse(Some(weapon_guid)) - tool.Discharge + tool.Discharge //always val projectileIndex = projectile_guid.guid - Projectile.BaseUID val projectilePlace = projectiles(projectileIndex) if(projectilePlace match { @@ -5426,8 +5450,12 @@ class WorldSessionActor extends Actor with MDCContextAware { } val distanceToOwner = Vector3.DistanceSquared(shot_origin, player.Position) if(distanceToOwner <= acceptableDistanceToOwner) { - projectiles(projectileIndex) = - Some(Projectile(tool.Projectile, tool.Definition, tool.FireMode, player, attribution, shot_origin, angle)) + val projectile_info = tool.Projectile + val projectile = Projectile(projectile_info, tool.Definition, tool.FireMode, player, attribution, shot_origin, angle) + projectiles(projectileIndex) = Some(projectile) + if(projectile_info.ExistsOnRemoteClients) { + taskResolver ! ReregisterProjectile(projectile) + } } else { log.warn(s"WeaponFireMessage: $player's ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect") @@ -6049,6 +6077,34 @@ class WorldSessionActor extends Actor with MDCContextAware { }, List(GUIDTask.RegisterAvatar(driver)(continent.GUID), GUIDTask.RegisterVehicle(obj)(continent.GUID))) } + def RegisterProjectile(obj : Projectile) : TaskResolver.GiveTask = { + val definition = obj.Definition + TaskResolver.GiveTask( + new Task() { + private val globalProjectile = obj + private val localAnnounce = avatarService + private val localMsg = AvatarServiceMessage( + continent.Id, + AvatarAction.LoadProjectile(player.GUID, definition.ObjectId, obj, definition.Packet.ConstructorData(obj).get) + ) + + override def isComplete : Task.Resolution.Value = { + if(globalProjectile.HasGUID) { + Task.Resolution.Success + } + else { + Task.Resolution.Incomplete + } + } + + def Execute(resolver : ActorRef) : Unit = { + localAnnounce ! localMsg + resolver ! scala.util.Success(this) + } + }, List(GUIDTask.RegisterObjectTask(obj)(continent.GUID)) + ) + } + def UnregisterDrivenVehicle(obj : Vehicle, driver : Player) : TaskResolver.GiveTask = { TaskResolver.GiveTask( new Task() { @@ -6070,6 +6126,30 @@ class WorldSessionActor extends Actor with MDCContextAware { }, List(GUIDTask.UnregisterAvatar(driver)(continent.GUID), GUIDTask.UnregisterVehicle(obj)(continent.GUID))) } + def UnregisterProjectile(obj : Projectile) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val globalProjectile = obj + private val localAnnounce = avatarService + private val localMsg = AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, obj.GUID)) + + override def isComplete : Task.Resolution.Value = { + if(!globalProjectile.HasGUID) { + Task.Resolution.Success + } + else { + Task.Resolution.Incomplete + } + } + + def Execute(resolver : ActorRef) : Unit = { + localAnnounce ! localMsg + resolver ! scala.util.Success(this) + } + }, List(GUIDTask.UnregisterObjectTask(obj)(continent.GUID)) + ) + } + /** * Construct tasking that removes the `Equipment` to `target`. * @param target what object that contains the `Equipment` @@ -6112,6 +6192,22 @@ class WorldSessionActor extends Actor with MDCContextAware { ) } + def ReregisterProjectile(obj : Projectile) : TaskResolver.GiveTask = { + val reg = RegisterProjectile(obj) + if(obj.HasGUID) { + TaskResolver.GiveTask( + reg.task, + List(TaskResolver.GiveTask( + reg.subs(0).task, + List(UnregisterProjectile(obj)) + )) + ) + } + else { + reg + } + } + /** * After some subtasking is completed, draw a particular slot, as if an `ObjectHeldMessage` packet was sent/received.
*