diff --git a/server/src/test/scala/actor/service/AvatarServiceTest.scala b/server/src/test/scala/actor/service/AvatarServiceTest.scala
index 665c7ec7..6e0eab30 100644
--- a/server/src/test/scala/actor/service/AvatarServiceTest.scala
+++ b/server/src/test/scala/actor/service/AvatarServiceTest.scala
@@ -130,29 +130,6 @@ class EquipmentInHandTest extends ActorTest {
}
}
-class DeployItemTest extends ActorTest {
- ServiceManager.boot(system)
- val service = system.actorOf(Props(classOf[AvatarService], Zone.Nowhere), "deploy-item-test-service")
- val objDef = GlobalDefinitions.motionalarmsensor
- val obj = new SensorDeployable(objDef)
- obj.Position = Vector3(1, 2, 3)
- obj.Orientation = Vector3(4, 5, 6)
- obj.GUID = PlanetSideGUID(40)
- val pkt = ObjectCreateMessage(
- objDef.ObjectId,
- obj.GUID,
- objDef.Packet.ConstructorData(obj).get
- )
-
- "AvatarService" should {
- "pass DeployItem" in {
- service ! Service.Join("test")
- service ! AvatarServiceMessage("test", AvatarAction.DeployItem(PlanetSideGUID(10), obj))
- expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.DropItem(pkt)))
- }
- }
-}
-
class DroptItemTest extends ActorTest {
ServiceManager.boot(system)
val service = system.actorOf(Props(classOf[AvatarService], Zone.Nowhere), "release-test-service")
diff --git a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala
index 9f396191..deea975f 100644
--- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala
@@ -563,7 +563,7 @@ class GeneralOperations(
* @param unk2 na
*/
def hackObject(targetGuid: PlanetSideGUID, unk1: Long, unk2: HackState7): Unit = {
- sendResponse(HackMessage(HackState1.Unk0, targetGuid, player_guid=Service.defaultPlayerGUID, progress=100, unk1, HackState.Hacked, unk2))
+ sendResponse(HackMessage(HackState1.Unk0, targetGuid, player_guid=Service.defaultPlayerGUID, progress=100, unk1.toFloat, HackState.Hacked, unk2))
}
/**
diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
index 93ffd42f..278bf91d 100644
--- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
@@ -8,8 +8,9 @@ import akka.pattern.ask
import akka.util.Timeout
import net.psforever.actors.session.spectator.SpectatorMode
import net.psforever.login.WorldSession
-import net.psforever.objects.avatar.BattleRank
+import net.psforever.objects.avatar.{BattleRank, DeployableToolbox}
import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, SessionStatistics}
+import net.psforever.objects.definition.converter.OCM
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.serverobject.interior.Sidedness
import net.psforever.objects.serverobject.mount.Seat
@@ -28,7 +29,7 @@ import scala.util.Success
//
import net.psforever.actors.session.{AvatarActor, SessionActor}
import net.psforever.login.WorldSession.RemoveOldEquipmentFromInventory
-import net.psforever.objects.avatar.{Avatar, DeployableToolbox}
+import net.psforever.objects.avatar.Avatar
import net.psforever.objects.avatar.{Award, AwardCategory, PlayerControl, Shortcut => AvatarShortcut}
import net.psforever.objects.ce.{Deployable, DeployableCategory, DeployedItem, TelepadLike}
import net.psforever.objects.definition.SpecialExoSuitDefinition
@@ -215,104 +216,13 @@ class ZoningOperations(
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
- //find and reclaim own deployables, if any
- val foundDeployables = continent.DeployableList.filter {
- case _: BoomerDeployable => false //if we do find boomers for any reason, ignore them
- case dobj => dobj.OwnerName.contains(player.Name) && dobj.Health > 0
- }
- foundDeployables.collect {
- case obj if avatar.deployables.AddOverLimit(obj) =>
- obj.Actor ! Deployable.Ownership(player)
- }
- //render deployable objects
- val (turrets, normal) = continent.DeployableList.partition(obj =>
- DeployableToolbox.UnifiedType(obj.Definition.Item) == DeployedItem.portable_manned_turret
+ val deployables = continent.DeployableList
+ reclaimOurDeployables(deployables, name, manageDeployablesWith(player.GUID, avatar.deployables))
+ drawDeployableIconsOnMap(
+ depictDeployables(deployables).filter(_.Faction == faction)
)
- normal.foreach(obj => {
- val definition = obj.Definition
- sendResponse(
- ObjectCreateMessage(
- definition.ObjectId,
- obj.GUID,
- definition.Packet.ConstructorData(obj).get
- )
- )
- })
- turrets.foreach(obj => {
- val objGUID = obj.GUID
- val definition = obj.Definition
- sendResponse(
- ObjectCreateMessage(
- definition.ObjectId,
- objGUID,
- definition.Packet.ConstructorData(obj).get
- )
- )
- //seated players
- obj
- .asInstanceOf[Mountable]
- .Seats
- .values
- .map(_.occupant)
- .collect {
- case Some(occupant) =>
- if (occupant.isAlive) {
- val targetDefinition = occupant.avatar.definition
- sendResponse(
- ObjectCreateMessage(
- targetDefinition.ObjectId,
- occupant.GUID,
- ObjectCreateMessageParent(objGUID, 0),
- targetDefinition.Packet.ConstructorData(occupant).get
- )
- )
- }
- }
- //auto turret behavior
- (obj match {
- case turret: AutomatedTurret with JammableUnit => turret.Target
- case _ => None
- }).collect {
- target =>
- val guid = obj.GUID
- val turret = obj.asInstanceOf[AutomatedTurret]
- sendResponse(ObjectDetectedMessage(guid, guid, 0, List(target.GUID)))
- if (!obj.asInstanceOf[JammableUnit].Jammed) {
- sendResponse(ChangeFireStateMessage_Start(turret.Weapons.values.head.Equipment.get.GUID))
- }
- }
- })
- //sensor animation
- normal
- .filter(obj =>
- obj.Definition.DeployCategory == DeployableCategory.Sensors &&
- !obj.Destroyed &&
- (obj match {
- case jObj: JammableUnit => !jObj.Jammed
- case _ => true
- })
- )
- .foreach(obj => {
- sendResponse(TriggerEffectMessage(obj.GUID, "on", unk1=true, 1000))
- })
- //update the health of our faction's deployables (if necessary)
- //draw our faction's deployables on the map
- continent.DeployableList
- .filter(obj => obj.Faction == faction && !obj.Destroyed)
- .foreach(obj => {
- if (obj.Health != obj.DefaultHealth) {
- sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health))
- }
- val deployInfo = DeployableInfo(
- obj.GUID,
- Deployable.Icon(obj.Definition.Item),
- obj.Position,
- obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
- )
- sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
- })
//render Equipment that was dropped into zone before the player arrived
- continent.EquipmentOnGround.foreach(item => {
+ continent.EquipmentOnGround.foreach { item =>
val definition = item.Definition
sendResponse(
ObjectCreateMessage(
@@ -324,26 +234,19 @@ class ZoningOperations(
)
)
)
- })
+ }
//load active players in zone (excepting players who are seated or players who are us)
val live = continent.LivePlayers
live
- .filterNot(tplayer => {
+ .filterNot { tplayer =>
tplayer.GUID == player.GUID || tplayer.VehicleSeated.nonEmpty
- })
- .foreach(targetPlayer => {
- val targetDefinition = player.avatar.definition
- sendResponse(
- ObjectCreateMessage(
- targetDefinition.ObjectId,
- targetPlayer.GUID,
- targetDefinition.Packet.ConstructorData(targetPlayer).get
- )
- )
+ }
+ .foreach { targetPlayer =>
+ sendResponse(OCM.apply(targetPlayer))
if (targetPlayer.UsingSpecial == SpecialExoSuitDefinition.Mode.Anchored) {
sendResponse(PlanetsideAttributeMessage(targetPlayer.GUID, 19, 1))
}
- })
+ }
//load corpses in zone
continent.Corpses.foreach {
spawn.DepictPlayerAsCorpse
@@ -376,10 +279,7 @@ class ZoningOperations(
//active vehicles (and some wreckage)
vehicles.foreach { vehicle =>
val vguid = vehicle.GUID
- val vdefinition = vehicle.Definition
- sendResponse(
- ObjectCreateMessage(vdefinition.ObjectId, vguid, vdefinition.Packet.ConstructorData(vehicle).get)
- )
+ sendResponse(OCM.apply(vehicle))
//occupants other than driver (with exceptions)
vehicle.Seats
.filter {
@@ -393,15 +293,10 @@ class ZoningOperations(
}
.foreach {
case (index, seat) =>
- val targetPlayer = seat.occupant.get
- val targetDefiniton = targetPlayer.avatar.definition
sendResponse(
- ObjectCreateMessage(
- targetDefiniton.ObjectId,
- targetPlayer.GUID,
- ObjectCreateMessageParent(vguid, index),
- targetDefiniton.Packet.ConstructorData(targetPlayer).get
- )
+ OCM.apply(seat.occupant.get)
+ .asInstanceOf[ObjectCreateMessage]
+ .copy(parentInfo = Some(ObjectCreateMessageParent(vguid, index)))
)
}
vehicle.SubsystemMessages().foreach { sendResponse }
@@ -411,8 +306,8 @@ class ZoningOperations(
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
}
//our vehicle would have already been loaded; see NewPlayerLoaded/AvatarCreate
- usedVehicle.headOption match {
- case Some(vehicle) =>
+ usedVehicle.headOption.collect {
+ case vehicle =>
//subsystems
vehicle.Actor ! Vehicle.UpdateSubsystemStates(player.Name, Some(false))
//depict any other passengers already in this zone
@@ -430,15 +325,10 @@ class ZoningOperations(
}
.foreach {
case (index, seat) =>
- val targetPlayer = seat.occupant.get
- val targetDefinition = targetPlayer.avatar.definition
sendResponse(
- ObjectCreateMessage(
- targetDefinition.ObjectId,
- targetPlayer.GUID,
- ObjectCreateMessageParent(vguid, index),
- targetDefinition.Packet.ConstructorData(targetPlayer).get
- )
+ OCM.apply(seat.occupant.get)
+ .asInstanceOf[ObjectCreateMessage]
+ .copy(parentInfo = Some(ObjectCreateMessageParent(vguid, index)))
)
}
//since we would have only subscribed recently, we need to reload mount access states
@@ -449,10 +339,9 @@ class ZoningOperations(
if (vehicle.Shields > 0) {
sendResponse(PlanetsideAttributeMessage(vguid, vehicle.Definition.shieldUiAttribute, vehicle.Shields))
}
- case _ => () //no vehicle
}
//vehicle wreckages
- wreckages.foreach(vehicle => {
+ wreckages.foreach { vehicle =>
sendResponse(
ObjectCreateMessage(
vehicle.Definition.DestroyedModel.get.id,
@@ -460,17 +349,14 @@ class ZoningOperations(
DestroyedVehicleConverter.converter.ConstructorData(vehicle).get
)
)
- })
+ }
//cargo occupants (including our own vehicle as cargo)
allActiveVehicles.collect {
case vehicle if vehicle.CargoHolds.nonEmpty =>
vehicle.CargoHolds.collect {
case (_index, hold: Cargo) if hold.isOccupied =>
- CarrierBehavior.CargoMountBehaviorForAll(
- vehicle,
- hold.occupant.get,
- _index
- ) //CargoMountBehaviorForUs can fail to attach the cargo vehicle on some clients
+ //CargoMountBehaviorForUs can fail to attach the cargo vehicle on some clients
+ CarrierBehavior.CargoMountBehaviorForAll(vehicle, hold.occupant.get, _index)
}
}
//special deploy states
@@ -492,8 +378,8 @@ class ZoningOperations(
}
deployedVehicles.filter(_.Definition == GlobalDefinitions.router).foreach { obj =>
//the router won't work if it doesn't completely deploy
- sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deploying, 0, unk3=false, Vector3.Zero))
- sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deployed, 0, unk3=false, Vector3.Zero))
+ sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deploying, 0, unk3 = false, Vector3.Zero))
+ sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deployed, 0, unk3 = false, Vector3.Zero))
sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
}
ServiceManager.serviceManager
@@ -504,41 +390,30 @@ class ZoningOperations(
case _ =>
}
//implant terminals
- continent.map.terminalToInterface.foreach({
+ continent.map.terminalToInterface.foreach {
case (terminal_guid, interface_guid) =>
val parent_guid = PlanetSideGUID(terminal_guid)
- continent.GUID(interface_guid) match {
- case Some(obj: Terminal) =>
- val objDef = obj.Definition
+ continent.GUID(interface_guid).collect {
+ case obj: Terminal =>
sendResponse(
- ObjectCreateMessage(
- objDef.ObjectId,
- PlanetSideGUID(interface_guid),
- ObjectCreateMessageParent(parent_guid, 1),
- objDef.Packet.ConstructorData(obj).get
- )
+ OCM.apply(obj)
+ .asInstanceOf[ObjectCreateMessage]
+ .copy(parentInfo = Some(ObjectCreateMessageParent(parent_guid, 1)))
)
- case _ => ()
}
//mount terminal occupants
- continent.GUID(terminal_guid) match {
- case Some(obj: Mountable) =>
- obj.Seats(0).occupant match {
- case Some(targetPlayer: Player) =>
- val targetDefinition = targetPlayer.avatar.definition
+ continent.GUID(terminal_guid).collect {
+ case obj: Mountable =>
+ obj.Seats(0).occupant.collect {
+ case occupant: Player =>
sendResponse(
- ObjectCreateMessage(
- targetDefinition.ObjectId,
- targetPlayer.GUID,
- ObjectCreateMessageParent(parent_guid, 0),
- targetDefinition.Packet.ConstructorData(targetPlayer).get
- )
+ OCM.apply(occupant)
+ .asInstanceOf[ObjectCreateMessage]
+ .copy(parentInfo = Some(ObjectCreateMessageParent(parent_guid, 0)))
)
- case _ => ()
}
- case _ => ()
}
- })
+ }
//facility turrets
continent.map.turretToWeapon
.map { case (turret_guid: Int, _) => continent.GUID(turret_guid) }
@@ -549,14 +424,10 @@ class ZoningOperations(
if (!turret.isUpgrading) {
turret.ControlledWeapon(wepNumber = 1).foreach {
case obj: Tool =>
- val objDef = obj.Definition
sendResponse(
- ObjectCreateMessage(
- objDef.ObjectId,
- obj.GUID,
- ObjectCreateMessageParent(pguid, 1),
- objDef.Packet.ConstructorData(obj).get
- )
+ OCM.apply(obj)
+ .asInstanceOf[ObjectCreateMessage]
+ .copy(parentInfo = Some(ObjectCreateMessageParent(pguid, 1)))
)
case _ => ()
}
@@ -564,38 +435,19 @@ class ZoningOperations(
//reserved ammunition?
//TODO need to register if it exists
//mount turret occupant
- turret.Seats(0).occupant match {
- case Some(targetPlayer: Player) =>
- val targetDefinition = targetPlayer.avatar.definition
+ turret.Seats(0).occupant.collect {
+ case occupant: Player =>
sendResponse(
- ObjectCreateMessage(
- targetDefinition.ObjectId,
- targetPlayer.GUID,
- ObjectCreateMessageParent(pguid, 0),
- targetDefinition.Packet.ConstructorData(targetPlayer).get
- )
+ OCM.apply(occupant)
+ .asInstanceOf[ObjectCreateMessage]
+ .copy(parentInfo = Some(ObjectCreateMessageParent(pguid, 0)))
)
- case _ => ()
- }
- turret.Target.collect {
- target =>
- val guid = turret.GUID
- sendResponse(ObjectDetectedMessage(guid, guid, 0, List(target.GUID)))
- if (!turret.Jammed) {
- sendResponse(ChangeFireStateMessage_Start(turret.Weapons.values.head.Equipment.get.GUID))
- }
}
+ triggerAutomatedTurretFire(turret)
}
//remote projectiles and radiation clouds
continent.Projectiles.foreach { projectile =>
- val definition = projectile.Definition
- sendResponse(
- ObjectCreateMessage(
- definition.ObjectId,
- projectile.GUID,
- definition.Packet.ConstructorData(projectile).get
- )
- )
+ sendResponse(OCM.apply(projectile))
}
//spawn point update request
continent.VehicleEvents ! VehicleServiceMessage(
@@ -1166,14 +1018,7 @@ class ZoningOperations(
// sync capture flags
case llu: CaptureFlag =>
// Create LLU
- sendResponse(
- ObjectCreateMessage(
- llu.Definition.ObjectId,
- llu.GUID,
- llu.Definition.Packet.ConstructorData(llu).get
- )
- )
-
+ sendResponse(OCM.apply(llu))
// Attach it to a player if it has a carrier
if (llu.Carrier.nonEmpty) {
continent.LocalEvents ! LocalServiceMessage(
@@ -1755,6 +1600,188 @@ class ZoningOperations(
*/
def NormalKeepAlive(): Unit = {}
+ /**
+ * Find all deployables that internally keep track of a player's name as its owner
+ * and change the GUID associated with that owner (reclaim it).
+ * @param deployables list of deployables
+ * @param name owner name
+ * @param reclamationStrategy what happens to deployables to re-assign ownership
+ * @return the list of deployables whose owenrship has been re-assigned
+ */
+ def reclaimOurDeployables(
+ deployables: List[Deployable],
+ name: String,
+ reclamationStrategy: Deployable => Option[Deployable]
+ ): List[Deployable] = {
+ deployables
+ .filter {
+ case _: BoomerDeployable => false //always ignore
+ case obj => obj.OwnerName.contains(name) && !obj.Destroyed && obj.Health > 0
+ }
+ .flatMap(reclamationStrategy)
+ }
+
+ /**
+ * If the deployable was added to a management collection, reassign its internal owner GUID.
+ * Hence, it can fail.
+ * @param toGuid anticipated ownership GUID
+ * @param managedDeployables collection for logically organizing deployables
+ * @param obj deployable designated for ownership
+ * @return the deployable, if it was capable of being managed and ownership was re-assigned
+ */
+ def manageDeployablesWith(toGuid: PlanetSideGUID, managedDeployables: DeployableToolbox)(obj: Deployable): Option[Deployable] = {
+ if (managedDeployables.AddOverLimit(obj)) {
+ reassignDeployablesTo(toGuid)(obj)
+ } else {
+ None
+ }
+ }
+
+ /**
+ * Reassign the internal owner GUID on this deployable.
+ * @param toGuid anticipated ownership GUID
+ * @param obj deployable designated for ownership
+ * @return the deployable, but now assigned to the GUID
+ */
+ def reassignDeployablesTo(toGuid: PlanetSideGUID)(obj: Deployable): Option[Deployable] = {
+ obj.OwnerGuid = toGuid
+ Some(obj)
+ }
+
+ /**
+ * Render and animate all provided deployables.
+ * Animation includes occupants for mountable deployables and ongoing behaviors for automated deployables.
+ * @param deployables list of deployables
+ * @return list of working deployables
+ * @see `OCM.apply`
+ */
+ def depictDeployables(deployables: List[Deployable]): List[Deployable] = {
+ val (smallTurrets, largeTurrets, sensors, normal, brokenThings) = {
+ val (broken, working) = deployables.partition { obj =>
+ obj.Destroyed || obj.Health == 0 || (obj match {
+ case jammable: JammableUnit => jammable.Jammed
+ case _ => false
+ })
+ }
+ val (small, remainder1) = working.partition { obj => obj.Definition.DeployCategory == DeployableCategory.SmallTurrets }
+ val (large, remainder2) = remainder1.partition { obj => obj.Definition.DeployCategory == DeployableCategory.FieldTurrets }
+ val (sensor, remainder3) = remainder2.partition { obj => obj.Definition.DeployCategory == DeployableCategory.Sensors }
+ (small, large, sensor, remainder3, broken)
+ }
+ val miscThings = normal ++ sensors ++ smallTurrets
+ (brokenThings ++ miscThings).foreach { obj =>
+ sendResponse(OCM.apply(obj))
+ }
+ largeTurrets.foreach { obj =>
+ sendResponse(OCM.apply(obj))
+ //seated players
+ obj
+ .asInstanceOf[Mountable]
+ .Seats
+ .values
+ .map(_.occupant)
+ .collect {
+ case Some(occupant) if occupant.isAlive =>
+ sendResponse(
+ OCM.apply(occupant)
+ .asInstanceOf[ObjectCreateMessage]
+ .copy(parentInfo = Some(ObjectCreateMessageParent(obj.GUID, 0)))
+ )
+ }
+ }
+ triggerAutomatedTurretFire(smallTurrets)
+ triggerSensorDeployables(sensors)
+ miscThings ++ largeTurrets
+ }
+
+ /**
+ * Render and animate all provided deployables.
+ * Animation includes ongoing behaviors for automated deployables.
+ * @param deployables list of deployables
+ * @return list of working deployables
+ * @see `OCM.apply`
+ */
+ def depictDeployablesUponRevival(deployables: List[Deployable]): List[Deployable] = {
+ val (smallTurrets, sensors, normal) = {
+ val (_, working) = deployables.partition { obj =>
+ obj.Destroyed || obj.Health == 0 || (obj match {
+ case jammable: JammableUnit => jammable.Jammed
+ case _ => false
+ })
+ }
+ val (small, remainder1) = working.partition { obj => obj.Definition.DeployCategory == DeployableCategory.SmallTurrets }
+ val (sensor, remainder2) = remainder1.partition { obj => obj.Definition.DeployCategory == DeployableCategory.Sensors }
+ (small, sensor, remainder2)
+ }
+ val miscThings = normal ++ sensors ++ smallTurrets
+ miscThings.foreach { obj =>
+ sendResponse(OCM.apply(obj))
+ }
+ triggerAutomatedTurretFire(smallTurrets)
+ triggerSensorDeployables(sensors)
+ miscThings
+ }
+
+ /**
+ * Treat the deployables as sensor-types and provide appropriate animation.
+ * This animation is the glowing halo-ing effect on its sensor bulb.
+ * @param sensors list of deployables
+ * @see `TriggerEffectMessage`
+ */
+ def triggerSensorDeployables(sensors: List[Deployable]): Unit = {
+ sensors.foreach { obj =>
+ sendResponse(TriggerEffectMessage(obj.GUID, effect = "on", unk1 = true, unk2 = 1000))
+ }
+ }
+
+ /**
+ * Treat the deployables as small turret-types and provide appropriate animation.
+ * This animation is related to its automation - tracking and shooting.
+ * @param turrets list of deployables
+ */
+ def triggerAutomatedTurretFire(turrets: List[Deployable]): Unit = {
+ turrets.collect { case turret: AutomatedTurret =>
+ triggerAutomatedTurretFire(turret)
+ }
+ }
+ /**
+ * Provide appropriate animation to the small turret deployable.
+ * This animation is related to its automation - tracking and shooting.
+ * @param turret small turret deployable
+ * @see `ChangeFireStateMessage_Start`
+ * @see `ObjectDetectedMessage`
+ */
+ def triggerAutomatedTurretFire(turret: AutomatedTurret): Unit = {
+ turret.Target.foreach { target =>
+ val guid = turret.GUID
+ sendResponse(ObjectDetectedMessage(guid, guid, 0, List(target.GUID)))
+ sendResponse(ChangeFireStateMessage_Start(turret.Weapons.values.head.Equipment.get.GUID))
+ }
+ }
+
+ /**
+ * Draw or redraw deployment map icons related to deployable presence and deployable management (if the owner).
+ * Assert deployable health as a precaution.
+ * @param deployables list of deployables
+ * @see `DeployableObjectsInfoMessage`
+ */
+ def drawDeployableIconsOnMap(deployables: List[Deployable]): Unit = {
+ deployables
+ .foreach { obj =>
+ val guid = obj.GUID
+ val health = obj.Health
+ if (health != obj.DefaultHealth) {
+ sendResponse(PlanetsideAttributeMessage(guid, 0, health))
+ }
+ sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, DeployableInfo(
+ guid,
+ Deployable.Icon(obj.Definition.Item),
+ obj.Position,
+ obj.OwnerGuid.getOrElse(Service.defaultPlayerGUID)
+ )))
+ }
+ }
+
/* nested class - spawn operations */
class SpawnOperations() {
@@ -2298,10 +2325,11 @@ class ZoningOperations(
//vehicle and driver/passenger
interstellarFerry = None
val vdef = vehicle.Definition
+ val vObjectId = vdef.ObjectId
val vguid = vehicle.GUID
vehicle.Position = shiftPosition.getOrElse(vehicle.Position)
vehicle.Orientation = shiftOrientation.getOrElse(vehicle.Orientation)
- val vdata = if (seat == 0) {
+ if (seat == 0) {
//driver
if (vehicle.Zone ne continent) {
continent.Transport ! Zone.Vehicle.Spawn(vehicle)
@@ -2311,7 +2339,7 @@ class ZoningOperations(
mount.unmount(player)
player.VehicleSeated = None
val data = vdef.Packet.ConstructorData(vehicle).get
- sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, data))
+ sendResponse(ObjectCreateMessage(vObjectId, vguid, data))
mount.mount(player)
player.VehicleSeated = vguid
Vehicles.Own(vehicle, player)
@@ -2320,7 +2348,7 @@ class ZoningOperations(
.foreach { _.MountedIn = vguid }
events ! VehicleServiceMessage(
zoneid,
- VehicleAction.LoadVehicle(player.GUID, vehicle, vdef.ObjectId, vguid, data)
+ VehicleAction.LoadVehicle(player.GUID, vehicle, vObjectId, vguid, data)
)
carrierInfo match {
case (Some(carrier), Some((index, _))) =>
@@ -2329,18 +2357,15 @@ class ZoningOperations(
vehicle.MountedIn = None
}
vehicle.allowInteraction = true
- data
} else {
//passenger
//non-drivers are not rendered in the vehicle at this time
- val data = vdef.Packet.ConstructorData(vehicle).get
- sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, data))
+ sendResponse(OCM.apply(vehicle))
carrierInfo match {
case (Some(carrier), Some((index, _))) =>
CargoMountBehaviorForUs(carrier, vehicle, index)
case _ => ()
}
- data
}
val originalSeated = player.VehicleSeated
player.VehicleSeated = vguid
@@ -2357,25 +2382,26 @@ class ZoningOperations(
)
}
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
- log.debug(s"AvatarCreate (vehicle): ${player.Name}'s ${vehicle.Definition.Name}")
- log.trace(s"AvatarCreate (vehicle): ${player.Name}'s ${vehicle.Definition.Name} - $vguid -> $vdata")
+ log.debug(s"AvatarCreate (vehicle): ${player.Name}'s ${vdef.Name}")
AvatarCreateInVehicle(player, vehicle, seat)
case _ =>
player.VehicleSeated = None
- val packet = player.avatar.definition.Packet
- val data = packet.DetailedConstructorData(player).get
- val guid = player.GUID
- sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, guid, data))
+ val definition = player.avatar.definition
+ val guid = player.GUID
+ sendResponse(OCM.detailed(player))
continent.AvatarEvents ! AvatarServiceMessage(
zoneid,
- AvatarAction.LoadPlayer(guid, ObjectClass.avatar, guid, packet.ConstructorData(player).get, None)
+ AvatarAction.LoadPlayer(guid, definition.ObjectId, guid, definition.Packet.ConstructorData(player).get, None)
)
- log.debug(s"AvatarCreate: ${player.Name}")
- log.trace(s"AvatarCreate: ${player.Name} - $guid -> $data")
}
continent.Population ! Zone.Population.Spawn(avatar, player, avatarActor)
avatarActor ! AvatarActor.RefreshPurchaseTimes()
+ drawDeployableIconsOnMap(
+ depictDeployablesUponRevival(
+ reclaimOurDeployables(continent.DeployableList, player.Name, reassignDeployablesTo(player.GUID))
+ )
+ )
//begin looking for conditions to set the avatar
context.system.scheduler.scheduleOnce(delay = 250 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200))
}
@@ -2403,11 +2429,9 @@ class ZoningOperations(
val pguid = tplayer.GUID
val vguid = vehicle.GUID
tplayer.VehicleSeated = None
- val pdata = pdef.Packet.DetailedConstructorData(tplayer).get
tplayer.VehicleSeated = vguid
log.debug(s"AvatarCreateInVehicle: ${player.Name}")
- log.trace(s"AvatarCreateInVehicle: ${player.Name} - $pguid -> $pdata")
- sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata))
+ sendResponse(OCM.detailed(tplayer))
if (seat == 0 || vehicle.WeaponControlledFromSeat(seat).nonEmpty) {
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
sessionLogic.general.accessContainer(vehicle)
@@ -2459,13 +2483,10 @@ class ZoningOperations(
sessionLogic.vehicles.GetKnownVehicleAndSeat() match {
case (Some(vehicle: Vehicle), Some(seat: Int)) =>
//vehicle and driver/passenger
- val vdef = vehicle.Definition
val vguid = vehicle.GUID
- val vdata = vdef.Packet.ConstructorData(vehicle).get
- sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, vdata))
+ sendResponse(OCM.apply(vehicle))
Vehicles.ReloadAccessPermissions(vehicle, continent.id)
log.debug(s"AvatarCreate (vehicle): ${player.Name}'s ${vehicle.Definition.Name}")
- log.trace(s"AvatarCreate (vehicle): ${player.Name}'s ${vehicle.Definition.Name} - $vguid -> $vdata")
val pdef = player.avatar.definition
val pguid = player.GUID
player.VehicleSeated = None
diff --git a/src/main/scala/net/psforever/objects/BoomerDeployable.scala b/src/main/scala/net/psforever/objects/BoomerDeployable.scala
index 1679bbe3..6cf03ddb 100644
--- a/src/main/scala/net/psforever/objects/BoomerDeployable.scala
+++ b/src/main/scala/net/psforever/objects/BoomerDeployable.scala
@@ -13,6 +13,7 @@ import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones.Zone
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.PlanetSideEmpire
import scala.annotation.unused
@@ -81,8 +82,14 @@ class BoomerDeployableControl(mine: BoomerDeployable)
}
override def gainOwnership(player: Player): Unit = {
- mine.Faction = PlanetSideEmpire.NEUTRAL //force map icon redraw
+ val originalOwner = mine.OwnerName
super.gainOwnership(player, player.Faction)
+ val events = mine.Zone.LocalEvents
+ val msg = LocalAction.DeployItem(mine)
+ originalOwner.collect { name =>
+ events ! LocalServiceMessage(name, msg)
+ }
+ events ! LocalServiceMessage(player.Name, msg)
}
override def dismissDeployable() : Unit = {
diff --git a/src/main/scala/net/psforever/objects/OwnableByPlayer.scala b/src/main/scala/net/psforever/objects/OwnableByPlayer.scala
index 2653dca3..690dabe6 100644
--- a/src/main/scala/net/psforever/objects/OwnableByPlayer.scala
+++ b/src/main/scala/net/psforever/objects/OwnableByPlayer.scala
@@ -18,16 +18,11 @@ trait OwnableByPlayer {
def OwnerGuid_=(owner: Player): Option[PlanetSideGUID] = OwnerGuid_=(Some(owner.GUID))
def OwnerGuid_=(owner: Option[PlanetSideGUID]): Option[PlanetSideGUID] = {
- owner match {
- case Some(_) =>
- ownerGuid = owner
- case None =>
- ownerGuid = None
- }
+ ownerGuid = owner
OwnerGuid
}
- def OwnerName: Option[String] = owner.map { _.name }
+ def OwnerName: Option[String] = owner.map(_.name)
def OriginalOwnerName: Option[String] = originalOwnerName
@@ -47,7 +42,7 @@ trait OwnableByPlayer {
(originalOwnerName, playerOpt) match {
case (None, Some(player)) =>
owner = Some(UniquePlayer(player))
- originalOwnerName = originalOwnerName.orElse { Some(player.Name) }
+ originalOwnerName = originalOwnerName.orElse(Some(player.Name))
OwnerGuid = player
case (_, Some(player)) =>
owner = Some(UniquePlayer(player))
diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
index 4245c768..1d0746b5 100644
--- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
+++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
@@ -524,7 +524,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
log.warn(s"${player.Name} failed to pick up an item ($item_guid) from the ground because $reason")
case Player.BuildDeployable(obj: TelepadDeployable, tool: Telepad) =>
- obj.Router = tool.Router //necessary; forwards link to the router that prodcued the telepad
+ obj.Router = tool.Router //necessary; forwards link to the router that produced the telepad
setupDeployable(obj, tool)
case Player.BuildDeployable(obj, tool) =>
@@ -534,7 +534,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
deployablePair match {
case Some((deployable, tool)) if deployable eq obj =>
val zone = player.Zone
- //boomers
val trigger = new BoomerTrigger
trigger.Companion = obj.GUID
obj.Trigger = trigger
@@ -552,7 +551,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
TaskWorkflow.execute(PutNewEquipmentInInventoryOrDrop(player)(trigger))
}
Players.buildCooldownReset(zone, player.Name, obj)
- case _ => ;
+ case _ => ()
}
deployablePair = None
@@ -569,7 +568,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
TelepadControl.TelepadError(zone, player.Name, msg = "@Telepad_NoDeploy_RouterLost")
}
Players.buildCooldownReset(zone, player.Name, obj)
- case _ => ;
+ case _ => ()
}
deployablePair = None
@@ -584,7 +583,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case None =>
log.warn(s"${player.Name} should have destroyed a ${tool.Definition.Name} here, but could not find it")
}
- case _ => ;
+ case _ => ()
}
deployablePair = None
@@ -701,16 +700,27 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
if (deployables.Valid(obj) &&
!deployables.Contains(obj) &&
Players.deployableWithinBuildLimits(player, obj)) {
+ //deployables, upon construction, may display an animation effect
tool.Definition match {
- case GlobalDefinitions.ace | /* animation handled in deployable lifecycle */
- GlobalDefinitions.router_telepad => ; /* no special animation */
+ case GlobalDefinitions.router_telepad => () /* no special animation */
+ case GlobalDefinitions.ace
+ if obj.Definition.deployAnimation == DeployAnimation.Standard =>
+ zone.LocalEvents ! LocalServiceMessage(
+ zone.id,
+ LocalAction.TriggerEffectLocation(
+ obj.OwnerGuid.getOrElse(Service.defaultPlayerGUID),
+ "spawn_object_effect",
+ obj.Position,
+ obj.Orientation
+ )
+ )
case GlobalDefinitions.advanced_ace
if obj.Definition.deployAnimation == DeployAnimation.Fdu =>
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PutDownFDU(player.GUID))
case _ =>
- org.log4s.getLogger(name = "Deployables").warn(
- s"not sure what kind of construction item to animate - ${tool.Definition.Name}"
- )
+ org.log4s
+ .getLogger(name = "Deployables")
+ .warn(s"not sure what kind of construction item to animate - ${tool.Definition.Name}")
}
deployablePair = Some((obj, tool))
obj.Faction = player.Faction
@@ -1158,10 +1168,14 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
zone.GUID(trigger.Companion) match {
case Some(obj: BoomerDeployable) =>
val deployables = player.avatar.deployables
- if (deployables.Valid(obj)) {
+ if (!deployables.Contains(obj) && deployables.Valid(obj)) {
+ events ! AvatarServiceMessage(toChannel, AvatarAction.SendResponse(
+ Service.defaultPlayerGUID,
+ GenericObjectAction2Message(1, player.GUID, trigger.GUID)
+ ))
Players.gainDeployableOwnership(player, obj, deployables.AddOverLimit)
}
- case _ => ;
+ case _ => ()
}
case citem: ConstructionItem
@@ -1172,7 +1186,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
}
Deployables.initializeConstructionItem(player.avatar.certifications, citem)
- case _ => ;
+ case _ => ()
}
events ! AvatarServiceMessage(
toChannel,
diff --git a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala
index d6a551a0..b363b63e 100644
--- a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala
+++ b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala
@@ -4,7 +4,6 @@ package net.psforever.objects.ce
import akka.actor.{Actor, ActorRef, Cancellable}
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects._
-import net.psforever.objects.definition.DeployAnimation
import net.psforever.objects.zones.Zone
import net.psforever.packet.game._
import net.psforever.services.Service
@@ -75,7 +74,7 @@ trait DeployableBehavior {
}
case Deployable.Ownership(Some(player))
- if !DeployableObject.Destroyed && DeployableObject.OwnerGuid.isEmpty =>
+ if !DeployableObject.Destroyed /*&& DeployableObject.OwnerGuid.isEmpty*/ =>
if (constructed.contains(true)) {
gainOwnership(player)
} else {
@@ -105,6 +104,7 @@ trait DeployableBehavior {
def loseOwnership(obj: Deployable, toFaction: PlanetSideEmpire.Value): Unit = {
DeployableBehavior.changeOwnership(
obj,
+ toOwner = "",
toFaction,
DeployableInfo(obj.GUID, Deployable.Icon.apply(obj.Definition.Item), obj.Position, Service.defaultPlayerGUID)
)
@@ -138,18 +138,18 @@ trait DeployableBehavior {
*/
def gainOwnership(player: Player, toFaction: PlanetSideEmpire.Value): Unit = {
val obj = DeployableObject
- obj.AssignOwnership(player)
decay.cancel()
DeployableBehavior.changeOwnership(
obj,
+ player.Name,
toFaction,
- DeployableInfo(obj.GUID, Deployable.Icon.apply(obj.Definition.Item), obj.Position, obj.OwnerGuid.get)
+ DeployableInfo(obj.GUID, Deployable.Icon.apply(obj.Definition.Item), obj.Position, player.GUID)
)
+ obj.AssignOwnership(player)
}
/**
* The first stage of the deployable build process, to put the formal process in motion.
- * Deployables, upon construction, may display an animation effect.
* Parameters are required to be passed onto the next stage of the build process and are not used here.
* @see `DeployableDefinition.deployAnimation`
* @see `DeployableDefinition.DeployTime`
@@ -157,21 +157,9 @@ trait DeployableBehavior {
* @param callback an `ActorRef` used for confirming the deployable's completion of the process
*/
def setupDeployable(callback: ActorRef): Unit = {
+ import scala.concurrent.ExecutionContext.Implicits.global
val obj = DeployableObject
constructed = Some(false)
- if (obj.Definition.deployAnimation == DeployAnimation.Standard) {
- val zone = obj.Zone
- zone.LocalEvents ! LocalServiceMessage(
- zone.id,
- LocalAction.TriggerEffectLocation(
- obj.OwnerGuid.getOrElse(Service.defaultPlayerGUID),
- "spawn_object_effect",
- obj.Position,
- obj.Orientation
- )
- )
- }
- import scala.concurrent.ExecutionContext.Implicits.global
setup = context.system.scheduler.scheduleOnce(
obj.Definition.DeployTime milliseconds,
self,
@@ -185,7 +173,7 @@ trait DeployableBehavior {
* Nothing dangerous happens if it does not begin to decay, but, because it is not under a player's management,
* the deployable will not properly transition to a decay state for another reason
* and can linger in the zone ownerless for as long as it is not destroyed.
- * @see `AvatarAction.DeployItem`
+ * @see `LocalAction.DeployItem`
* @see `DeploymentAction`
* @see `DeployableInfo`
* @see `LocalAction.DeployableMapIcon`
@@ -197,27 +185,22 @@ trait DeployableBehavior {
setup = Default.Cancellable
constructed = Some(true)
val obj = DeployableObject
- val zone = obj.Zone
+ val zone = obj.Zone
val localEvents = zone.LocalEvents
- val owner = obj.OwnerGuid.getOrElse(Service.defaultPlayerGUID)
- obj.OwnerName match {
- case Some(_) =>
- case None =>
- import scala.concurrent.ExecutionContext.Implicits.global
- decay = context.system.scheduler.scheduleOnce(
- Deployable.decay,
- self,
- Deployable.Deconstruct()
- )
+ obj.OwnerName.orElse {
+ import scala.concurrent.ExecutionContext.Implicits.global
+ decay = context.system.scheduler.scheduleOnce(Deployable.decay, self, Deployable.Deconstruct())
+ None
}
//zone build
- zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.DeployItem(Service.defaultPlayerGUID, obj))
+ localEvents ! LocalServiceMessage(zone.id, LocalAction.DeployItem(obj))
//zone map icon
localEvents ! LocalServiceMessage(
obj.Faction.toString,
LocalAction.DeployableMapIcon(
Service.defaultPlayerGUID,
- DeploymentAction.Build, DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, owner)
+ DeploymentAction.Build,
+ DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, obj.OwnerGuid.getOrElse(Service.defaultPlayerGUID))
)
)
//local build management
@@ -290,28 +273,28 @@ object DeployableBehavior {
* @param toFaction na
* @param info na
*/
- def changeOwnership(obj: Deployable, toFaction: PlanetSideEmpire.Value, info: DeployableInfo): Unit = {
+ def changeOwnership(obj: Deployable, toOwner: String, toFaction: PlanetSideEmpire.Value, info: DeployableInfo): Unit = {
+ val dGuid = obj.GUID
val originalFaction = obj.Faction
+ val zone = obj.Zone
+ val localEvents = zone.LocalEvents
if (originalFaction != toFaction) {
- val guid = obj.GUID
- val zone = obj.Zone
- val localEvents = zone.LocalEvents
obj.Faction = toFaction
//visual tells in regards to ownership by faction
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
- AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, toFaction)
+ AvatarAction.SetEmpire(Service.defaultPlayerGUID, dGuid, toFaction)
)
//remove knowledge by the previous owner's faction
localEvents ! LocalServiceMessage(
originalFaction.toString,
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info)
)
- //display to the given faction
- localEvents ! LocalServiceMessage(
- toFaction.toString,
- LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info)
- )
}
+ //display to the given faction
+ localEvents ! LocalServiceMessage(
+ toFaction.toString,
+ LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info)
+ )
}
}
diff --git a/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala
index 8bbb5249..f27672d7 100644
--- a/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala
+++ b/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala
@@ -24,7 +24,7 @@ class SmallTurretConverter extends ObjectCreateConverter[TurretDeployable]() {
v1 = true,
None,
obj.Jammed,
- Some(true),
+ None,
None,
obj.OwnerGuid match {
case Some(owner) => owner
@@ -67,7 +67,7 @@ object SmallTurretConverter {
private def MakeMountings(obj: WeaponTurret): List[InventoryItemData.InventoryItem] = {
obj.Weapons
.map({
- case ((index, slot)) =>
+ case (index, slot) =>
val equip: Equipment = slot.Equipment.get
val equipDef = equip.Definition
InventoryItemData(equipDef.ObjectId, equip.GUID, index, equipDef.Packet.ConstructorData(equip).get)
diff --git a/src/main/scala/net/psforever/objects/definition/converter/TRAPConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/TRAPConverter.scala
index 621fde9a..a24527f1 100644
--- a/src/main/scala/net/psforever/objects/definition/converter/TRAPConverter.scala
+++ b/src/main/scala/net/psforever/objects/definition/converter/TRAPConverter.scala
@@ -19,10 +19,10 @@ class TRAPConverter extends ObjectCreateConverter[TrapDeployable]() {
obj.Faction,
bops = false,
alternate = false,
- true,
+ v1 = true,
+ None,
+ jammered = false,
None,
- false,
- Some(true),
None,
obj.OwnerGuid match {
case Some(owner) => owner
@@ -42,9 +42,9 @@ class TRAPConverter extends ObjectCreateConverter[TrapDeployable]() {
obj.Faction,
bops = false,
alternate = true,
- true,
+ v1 = true,
None,
- false,
+ jammered = false,
Some(true),
None,
PlanetSideGUID(0)
diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index bea0c62e..6bc06e6d 100644
--- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -455,7 +455,7 @@ object GamePacketOpcode extends Enumeration {
case 0x7f => game.AvatarStatisticsMessage.decode
// OPCODES 0x80-8f
- case 0x80 => noDecoder(GenericObjectAction2Message)
+ case 0x80 => game.GenericObjectAction2Message.decode
case 0x81 => game.DestroyDisplayMessage.decode
case 0x82 => noDecoder(TriggerBotAction)
case 0x83 => game.SquadWaypointRequest.decode
diff --git a/src/main/scala/net/psforever/packet/game/GenericObjectAction2Message.scala b/src/main/scala/net/psforever/packet/game/GenericObjectAction2Message.scala
new file mode 100644
index 00000000..728101d4
--- /dev/null
+++ b/src/main/scala/net/psforever/packet/game/GenericObjectAction2Message.scala
@@ -0,0 +1,33 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.GamePacketOpcode.Type
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import net.psforever.types.PlanetSideGUID
+import scodec.bits.BitVector
+import scodec.codecs._
+import scodec.{Attempt, Codec}
+
+/**
+ * na
+ * @param unk na
+ * @param guid1 na
+ * @param guid2 na
+ */
+final case class GenericObjectAction2Message(
+ unk: Int,
+ guid1: PlanetSideGUID,
+ guid2: PlanetSideGUID
+ ) extends PlanetSideGamePacket {
+ type Packet = GenericObjectActionMessage
+ def opcode: Type = GamePacketOpcode.GenericObjectAction2Message
+ def encode: Attempt[BitVector] = GenericObjectAction2Message.encode(this)
+}
+
+object GenericObjectAction2Message extends Marshallable[GenericObjectAction2Message] {
+ implicit val codec: Codec[GenericObjectAction2Message] = (
+ ("unk" | uint(bits = 3)) :: //dword_D32FC0
+ ("guid1" | PlanetSideGUID.codec) ::
+ ("guid2" | PlanetSideGUID.codec)
+ ).as[GenericObjectAction2Message]
+}
diff --git a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
index a6fb9070..aaf908d1 100644
--- a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
+++ b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
@@ -1,10 +1,12 @@
// Copyright (c) 2016 PSForever.net to present
package net.psforever.packet.game
+import net.psforever.packet.GamePacketOpcode.Type
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.PlanetSideGUID
-import scodec.Codec
+import scodec.bits.BitVector
+import scodec.{Attempt, Codec}
import scodec.codecs._
/**
@@ -68,8 +70,8 @@ import scodec.codecs._
* `17 - BEP. Value seems to be the same as BattleExperienceMessage`
* `18 - CEP.`
* `19 - Anchors. Value is 0 to disengage, 1 to engage.`
- * `20 - Control console hacking, affects CC timer, yellow base warning lights and message "The FactionName has hacked into BaseName".
- * Format is: Time left - 2 bytes, faction - 1 byte (1-4), isResecured - 1 byte (0-1)`
+ * `20 - Control console hacking, affects CC timer, yellow base warning lights and message "The FactionName has hacked into BaseName".`
+ * Format is: Time left - 2 bytes, faction - 1 byte (1-4), isResecured - 1 byte (0-1)
*