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.
*