From 42db02f576683fa5cbd2b0441194577fa48b90ea Mon Sep 17 00:00:00 2001 From: "Jason_DiDonato@yahoo.com" Date: Fri, 25 Jun 2021 22:57:55 -0400 Subject: [PATCH] recovery from original angles branch, mostly concerning changes with ChangeFireStateMessage_Stop and WeaponFireMessage field info --- .../actors/session/SessionActor.scala | 122 ++++++++---------- .../objects/ballistics/Projectile.scala | 66 ++++++---- .../packet/game/WeaponFireMessage.scala | 117 +++++++++++------ .../scala/game/WeaponFireMessageTest.scala | 10 +- 4 files changed, 177 insertions(+), 138 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 679a766b..62948337 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -4120,46 +4120,24 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case msg @ ChangeFireStateMessage_Stop(item_guid) => prefire = None shootingStop = System.currentTimeMillis() - val weapon: Option[Equipment] = if (shooting.contains(item_guid)) { + if (shooting.contains(item_guid)) { shooting = None - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Stop(player.GUID, item_guid) - ) - FindEquipment - } else { - FindEquipment match { - case Some(tool: Tool) => //special cases - //the decimator does not send a ChangeFireState_Start on the last shot - 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(player.GUID, item_guid) - ) - shootingStart = System.currentTimeMillis() - 1L - } - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Stop(player.GUID, item_guid) - ) - Some(tool) - case Some(tool) => //permissible, for now - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Stop(player.GUID, item_guid) - ) - Some(tool) - case _ => - //log.warn(s"ChangeFireState_Stop: ${player.Name} never started firing item ${item_guid.guid} in the first place?") - None - } } - weapon match { + val pguid = player.GUID + FindEquipment match { 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 = System.currentTimeMillis() - 1L + } tool.FireMode match { case mode: ChargeFireModeDefinition => sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) @@ -4168,18 +4146,24 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (tool.Magazine == 0) { FireCycleCleanup(tool) } - case Some(trigger: BoomerTrigger) => - val playerGUID = player.GUID continent.AvatarEvents ! AvatarServiceMessage( continent.id, - AvatarAction.ChangeFireState_Start(playerGUID, item_guid) + AvatarAction.ChangeFireState_Stop(pguid, 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) => 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?") } progressBarUpdate.cancel() progressBarValue = None @@ -5128,19 +5112,19 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case msg @ WeaponFireMessage( - seq_time, - weapon_guid, - projectile_guid, - shot_origin, - unk1, - unk2, - unk3, - unk4, - unk5, - unk6, - unk7 - ) => - HandleWeaponFire(weapon_guid, projectile_guid, shot_origin) + _, + weapon_guid, + projectile_guid, + shot_origin, + _, + _, + _, + _, //max_distance, + _, + _, //projectile_type, + thrown_projectile_vel + ) => + HandleWeaponFire(weapon_guid, projectile_guid, shot_origin, thrown_projectile_vel.flatten) case msg @ WeaponLazeTargetPositionMessage(_, _, pos2) => log.info(s"${player.Name} is lazing the position ${continent.id}@(${pos2.x},${pos2.y},${pos2.z})") @@ -9009,7 +8993,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con middlewareActor ! MiddlewareActor.Teardown() } - def HandleWeaponFire(weaponGUID: PlanetSideGUID, projectileGUID: PlanetSideGUID, shotOrigin: Vector3): Unit = { + def HandleWeaponFire( + weaponGUID: PlanetSideGUID, + projectileGUID: PlanetSideGUID, + shotOrigin: Vector3, + shotVelocity: Option[Vector3] + ): Unit = { HandleWeaponFireAccountability(weaponGUID, projectileGUID) match { case (Some(obj), Some(tool)) => val projectileIndex = projectileGUID.guid - Projectile.baseUID @@ -9054,10 +9043,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con projectile_info, tool.Definition, tool.FireMode, - player, + PlayerSource(player), attribution, shotOrigin, - angle + angle, + shotVelocity ) val initialQuality = tool.FireMode match { case mode: ChargeFireModeDefinition => @@ -9071,18 +9061,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con s"WeaponFireMessage: ${player.Name}'s ${projectile_info.Name} is a remote projectile" ) continent.tasks ! (if (projectile.HasGUID) { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ProjectileExplodes( - player.GUID, - projectile.GUID, - projectile - ) - ) - ReregisterProjectile(projectile) - } else { - RegisterProjectile(projectile) - }) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ProjectileExplodes(player.GUID, projectile.GUID, projectile) + ) + ReregisterProjectile(projectile) + } else { + RegisterProjectile(projectile) + }) } projectilesToCleanUp(projectileIndex) = false diff --git a/src/main/scala/net/psforever/objects/ballistics/Projectile.scala b/src/main/scala/net/psforever/objects/ballistics/Projectile.scala index 36c2913d..4e400d09 100644 --- a/src/main/scala/net/psforever/objects/ballistics/Projectile.scala +++ b/src/main/scala/net/psforever/objects/ballistics/Projectile.scala @@ -12,7 +12,7 @@ import net.psforever.objects.vital.base.DamageResolution import net.psforever.types.Vector3 /** - * A summation of weapon (`Tool`) discharge. + * A summation of weapon discharge. * @see `ProjectileDefinition` * @see `ToolDefinition` * @see `FireModeDefinition` @@ -28,9 +28,10 @@ import net.psforever.types.Vector3 * if not, then it is a type of vehicle (and owner should have a positive `seated` field) * @param shot_origin where the projectile started * @param shot_angle in which direction the projectile was aimed when it was discharged + * @param shot_velocity the initial velocity coordinates of the projectile according to its world directions * @param quality na * @param id an exclusive identifier for this projectile; - * normally generated internally, but can be manually set + * normally generated internally, but can be manually set (for modifying a continuous projectile reference) * @param fire_time when the weapon discharged was recorded; * defaults to `System.currentTimeMillis()` */ @@ -42,18 +43,20 @@ final case class Projectile( attribute_to: Int, shot_origin: Vector3, shot_angle: Vector3, + shot_velocity: Option[Vector3], quality: ProjectileQuality = ProjectileQuality.Normal, id: Long = Projectile.idGenerator.getAndIncrement(), fire_time: Long = System.currentTimeMillis() ) extends PlanetSideGameObject { Position = shot_origin Orientation = shot_angle - Velocity = { - val initVel: Int = profile.InitialVelocity //initial velocity - val radAngle: Double = math.toRadians(shot_angle.y) //angle of elevation - val rise: Float = initVel * math.sin(radAngle).toFloat //z - val ground: Float = initVel * math.cos(radAngle).toFloat //base - Vector3.Rz(Vector3(0, -ground, 0), shot_angle.z) + Vector3.z(rise) + Velocity = shot_velocity.getOrElse { + val radz = math.toRadians(shot_angle.z) + Vector3.Unit(Vector3( + math.sin(radz).toFloat, + math.cos(radz).toFloat, + math.sin(math.toRadians(shot_angle.y)).toFloat + )) * profile.InitialVelocity.toFloat } /** Information about the current world coordinates and orientation of the projectile */ @@ -77,6 +80,7 @@ final case class Projectile( attribute_to, shot_origin, shot_angle, + shot_velocity, value, id, fire_time @@ -127,14 +131,14 @@ object Projectile { * @return the `Projectile` object */ def apply( - profile: ProjectileDefinition, - tool_def: ToolDefinition, - fire_mode: FireModeDefinition, - owner: PlanetSideGameObject with FactionAffinity, - shot_origin: Vector3, - shot_angle: Vector3 - ): Projectile = { - Projectile(profile, tool_def, fire_mode, SourceEntry(owner), tool_def.ObjectId, shot_origin, shot_angle) + profile: ProjectileDefinition, + tool_def: ToolDefinition, + fire_mode: FireModeDefinition, + owner: PlanetSideGameObject with FactionAffinity, + shot_origin: Vector3, + shot_angle: Vector3 + ): Projectile = { + Projectile(profile, tool_def, fire_mode, SourceEntry(owner), tool_def.ObjectId, shot_origin, shot_angle, None) } /** @@ -149,14 +153,26 @@ object Projectile { * @return the `Projectile` object */ def apply( - profile: ProjectileDefinition, - tool_def: ToolDefinition, - fire_mode: FireModeDefinition, - owner: PlanetSideGameObject with FactionAffinity, - attribute_to: Int, - shot_origin: Vector3, - shot_angle: Vector3 - ): Projectile = { - Projectile(profile, tool_def, fire_mode, SourceEntry(owner), attribute_to, shot_origin, shot_angle) + profile: ProjectileDefinition, + tool_def: ToolDefinition, + fire_mode: FireModeDefinition, + owner: PlanetSideGameObject with FactionAffinity, + attribute_to: Int, + shot_origin: Vector3, + shot_angle: Vector3 + ): Projectile = { + Projectile(profile, tool_def, fire_mode, SourceEntry(owner), attribute_to, shot_origin, shot_angle, None) + } + + def apply( + profile: ProjectileDefinition, + tool_def: ToolDefinition, + fire_mode: FireModeDefinition, + owner: SourceEntry, + attribute_to: Int, + shot_origin: Vector3, + shot_angle: Vector3 + ): Projectile = { + Projectile(profile, tool_def, fire_mode, owner, attribute_to, shot_origin, shot_angle, None) } } diff --git a/src/main/scala/net/psforever/packet/game/WeaponFireMessage.scala b/src/main/scala/net/psforever/packet/game/WeaponFireMessage.scala index a4f48e5f..79f5bd5c 100644 --- a/src/main/scala/net/psforever/packet/game/WeaponFireMessage.scala +++ b/src/main/scala/net/psforever/packet/game/WeaponFireMessage.scala @@ -1,42 +1,79 @@ // Copyright (c) 2017 PSForever package net.psforever.packet.game -import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import enumeratum.values.{IntEnum, IntEnumEntry} +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.types.{PlanetSideGUID, Vector3} import scodec.Codec import scodec.codecs._ +sealed abstract class ProjectileCharacteristics(val value: Int) extends IntEnumEntry + /** - * WeaponFireMessage seems to be sent each time a weapon actually shoots. + * Characteristics about the projectile being produced by a `WeaponFireMessage` packet. + * Not really useful outside of `WeaponFireMessage`? + */ +object ProjectileCharacteristics extends IntEnum[ProjectileCharacteristics] { + val values = findValues + + /** most common characteristic; + * utilized for both straight-fire and various arcing projectiles + */ + case object Standard extends ProjectileCharacteristics(value = 0) + /** some arcing explosive projectiles such as the Thumper's alternate fire and the Leviathan's fluxpod launcher; + * exceptions include: Punisher grenade cartridges, Grenade alternate fire (see `Thrown`), Pounder alternate fire + */ + case object DelayedExplosion extends ProjectileCharacteristics(value = 1) + /** remote client projectiles (those constructed through packets) */ + case object Guided extends ProjectileCharacteristics(value = 2) + /** grenades, and only grenades */ + case object Thrown extends ProjectileCharacteristics(value = 3) + + /** unused? */ + case object u4 extends ProjectileCharacteristics(value = 4) + /** unused? */ + case object u5 extends ProjectileCharacteristics(value = 5) + /** unused? */ + case object u6 extends ProjectileCharacteristics(value = 6) + /** unused? */ + case object u7 extends ProjectileCharacteristics(value = 7) +} + +/** + * Dispatched form the client each time a weapon discharges. * - * @param seq_time See [[PlayerStateMessageUpstream]] for explanation of seq_time. - * @param weapon_guid - * @param projectile_guid - * @param shot_origin - * @param unk1 Always zero from testing so far - * @param unk2 Seems semi-random - * @param unk3 Seems semi-random - * @param unk4 Maximum travel distance in meters - seems to be zero for decimator rockets - * @param unk5 Possibly always 255 from testing - * @param unk6 0 for bullet - * 1 for possibly delayed explosion (thumper alt fire) or thresher/leviathan flux cannon - * 2 for vs starfire (lockon type?) - * 3 for thrown (e.g. grenades) - * @param unk7 Seems to be thrown weapon velocity/direction + * @param seq_time see [[PlayerStateMessageUpstream]] for explanation of seq_time + * @param weapon_guid the weapon of discharge; + * when dispatched to a client, an unreferenced entity results in the projectile not being rendered + * @param projectile_guid the (client-local) projectile unique identifier; + * when dispatched to a client, can be unreferenced (or blanked) + * @param shot_origin the position where the projectile is first spawned + * @param unk1 na; + * always 0? + * @param spread_a related to the spread of the discharge; + * works with `spread_b` field in unknown way; + * the unmodified value is high (65535) when accurate, low (0) when not + * @param spread_b related to the spread of the discharge; + * works with `spread_a` field in unknown way + * @param max_distance maximum travel distance (m), with exceptions, e.g., decimator rockets are always 0 + * @param unk5 na; + * always 255? + * @param projectile_type the sort of projectile produced + * @param thrown_projectile_vel if a thrown projectile, its velocity */ final case class WeaponFireMessage( - seq_time: Int, - weapon_guid: PlanetSideGUID, - projectile_guid: PlanetSideGUID, - shot_origin: Vector3, - unk1: Int, - unk2: Int, - unk3: Int, - unk4: Int, - unk5: Int, - unk6: Int, - unk7: Option[Option[Vector3]] -) extends PlanetSideGamePacket { + seq_time: Int, + weapon_guid: PlanetSideGUID, + projectile_guid: PlanetSideGUID, + shot_origin: Vector3, + unk1: Int, + spread_a: Int, + spread_b: Int, + max_distance: Int, + unk5: Int, + projectile_type: ProjectileCharacteristics, + thrown_projectile_vel: Option[Option[Vector3]] + ) extends PlanetSideGamePacket { type Packet = WeaponFireMessage def opcode = GamePacketOpcode.WeaponFireMessage def encode = WeaponFireMessage.encode(this) @@ -44,17 +81,17 @@ final case class WeaponFireMessage( object WeaponFireMessage extends Marshallable[WeaponFireMessage] { implicit val codec: Codec[WeaponFireMessage] = ( - ("seq_time" | uintL(10)) :: - ("weapon_guid" | PlanetSideGUID.codec) :: - ("projectile_guid" | PlanetSideGUID.codec) :: - ("shot_origin" | Vector3.codec_pos) :: - ("unk1" | uint16L) :: - ("unk2" | uint16L) :: - ("unk3" | uint16L) :: - ("unk4" | uint16L) :: - ("unk5" | uint8L) :: - (("unk6" | uintL(3)) >>:~ { unk6_value => - conditional(unk6_value == 3, "unk7" | optional(bool, Vector3.codec_vel)).hlist + ("seq_time" | uintL(bits = 10)) :: + ("weapon_guid" | PlanetSideGUID.codec) :: + ("projectile_guid" | PlanetSideGUID.codec) :: + ("shot_origin" | Vector3.codec_pos) :: + ("unk1" | uint16L) :: + ("spread_a" | uint16L.xmap[Int]( { x => 65535 - x }, { x => 65535 - x })) :: //low when accurate, high when not + ("spread_b" | uint16L) :: + ("max_distance" | uint16L) :: + ("unk5" | uint8L) :: + ("projectile_type" | PacketHelpers.createIntEnumCodec(ProjectileCharacteristics, uint(bits = 3)) >>:~ { ptype => + ("thrown_projectile_vel" | conditional(ptype == ProjectileCharacteristics.Thrown, optional(bool, Vector3.codec_vel))).hlist }) - ).as[WeaponFireMessage] + ).as[WeaponFireMessage] } diff --git a/src/test/scala/game/WeaponFireMessageTest.scala b/src/test/scala/game/WeaponFireMessageTest.scala index 6548dbb8..8aecc3a1 100644 --- a/src/test/scala/game/WeaponFireMessageTest.scala +++ b/src/test/scala/game/WeaponFireMessageTest.scala @@ -30,12 +30,12 @@ class WeaponFireMessageTest extends Specification { projectile_guid mustEqual PlanetSideGUID(40100) shot_origin mustEqual Vector3(3675.4688f, 2726.9922f, 92.921875f) unk1 mustEqual 0 - unk2 mustEqual 64294 + unk2 mustEqual 1241 unk3 mustEqual 1502 unk4 mustEqual 200 unk5 mustEqual 255 - unk6 mustEqual 0 - unk7 mustEqual None + unk6 mustEqual ProjectileCharacteristics.Standard + unk7.isEmpty mustEqual true case _ => ko } @@ -48,11 +48,11 @@ class WeaponFireMessageTest extends Specification { PlanetSideGUID(40100), Vector3(3675.4688f, 2726.9922f, 92.921875f), 0, - 64294, + 1241, 1502, 200, 255, - 0, + ProjectileCharacteristics.Standard, None ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector