From 1f4697407b3968f2900067894d9bfa18a10bed45 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 17 Sep 2026 01:33:14 -0400 Subject: [PATCH 1/2] packet work for ModuleLimitsMessage and VanuModuleUpdateMessage; includes tests --- .../psforever/packet/GamePacketOpcode.scala | 4 +- .../game/packets/ModuleLimitsMessage.scala | 32 +++++++ .../packets/VanuModuleUpdateMessage.scala | 96 +++++++++++++++++++ src/test/scala/game/MailMessageTest.scala | 1 - .../scala/game/ModuleLimitsMessageTest.scala | 29 ++++++ .../game/VanuModuleUpdateMessageTest.scala | 58 +++++++++++ src/test/scala/objects/ZoneTest.scala | 2 +- 7 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 src/main/scala/net/psforever/packet/game/packets/ModuleLimitsMessage.scala create mode 100644 src/main/scala/net/psforever/packet/game/packets/VanuModuleUpdateMessage.scala create mode 100644 src/test/scala/game/ModuleLimitsMessageTest.scala create mode 100644 src/test/scala/game/VanuModuleUpdateMessageTest.scala diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index f714c1f30..846460ff0 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -538,7 +538,7 @@ object GamePacketOpcode extends Enumeration { // OPCODES 0xc0-cf case 0xc0 => packets.CaptureFlagUpdateMessage.decode - case 0xc1 => noDecoder(VanuModuleUpdateMessage) + case 0xc1 => packets.VanuModuleUpdateMessage.decode case 0xc2 => packets.FacilityBenefitShieldChargeRequestMessage.decode case 0xc3 => packets.ProximityTerminalUseMessage.decode case 0xc4 => packets.QuantityDeltaUpdateMessage.decode @@ -547,7 +547,7 @@ object GamePacketOpcode extends Enumeration { case 0xc7 => packets.LongRangeProjectileInfoMessage.decode // 0xc8 case 0xc8 => packets.WeaponLazeTargetPositionMessage.decode - case 0xc9 => noDecoder(ModuleLimitsMessage) + case 0xc9 => packets.ModuleLimitsMessage.decode case 0xca => noDecoder(OutfitBenefitMessage) case 0xcb => noDecoder(EmpireChangeTimeMessage) case 0xcc => noDecoder(ClockCalibrationMessage) diff --git a/src/main/scala/net/psforever/packet/game/packets/ModuleLimitsMessage.scala b/src/main/scala/net/psforever/packet/game/packets/ModuleLimitsMessage.scala new file mode 100644 index 000000000..f8251d68a --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/packets/ModuleLimitsMessage.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2026 PSForever +package net.psforever.packet.game.packets + +import net.psforever.packet.GamePacketOpcode.Type +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.{Attempt, Codec} +import scodec.bits.BitVector +import scodec.codecs._ + +/** + * na + * @param u1 na + * @param u2 na + * @param u3 na + */ +final case class ModuleLimitsMessage( + u1: Int, + u2: Int, + u3: Int + ) extends PlanetSideGamePacket { + type Packet = ModuleLimitsMessage + def opcode: Type = GamePacketOpcode.ModuleLimitsMessage + def encode: Attempt[BitVector] = ModuleLimitsMessage.encode(this) +} + +object ModuleLimitsMessage extends Marshallable[ModuleLimitsMessage] { + implicit val codec: Codec[ModuleLimitsMessage] = ( + ("u1" | uint8) :: + ("u2" | uint(bits = 6)) :: + ("u3" | uint(bits = 3)) + ).as[ModuleLimitsMessage] +} diff --git a/src/main/scala/net/psforever/packet/game/packets/VanuModuleUpdateMessage.scala b/src/main/scala/net/psforever/packet/game/packets/VanuModuleUpdateMessage.scala new file mode 100644 index 000000000..9b5dfef5c --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/packets/VanuModuleUpdateMessage.scala @@ -0,0 +1,96 @@ +// Copyright (c) 2026 PSForever +package net.psforever.packet.game.packets + +import enumeratum.values.{IntEnum, IntEnumEntry} +import net.psforever.newcodecs.newcodecs +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.packet.GamePacketOpcode.Type +import net.psforever.types.Vector3 +import scodec.{Attempt, Codec} +import scodec.bits.BitVector +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * Whether the vanu module icon on the continental map alternates red or green in between displays of it's distinguishing color. + * There are four values - 0 to 3 - but 3 is the only green color and all of the other values result in red. + * @param value charge state as an integer value + */ +sealed abstract class PulseState(val value: Int) extends IntEnumEntry + +object PulseState extends IntEnum[PulseState] { + val values: IndexedSeq[PulseState] = findValues + + /** Red module */ + case object Red extends PulseState(value = 0) + /** Green module */ + case object Green extends PulseState(value = 3) + + implicit val codec: Codec[PulseState] = uint(bits = 2).xmap( + { + case 3 => Green + case _ => Red + }, + state => state.value + ) +} + +/** + * na + * When `u4 = true` and `u5 = false`, the icons do not display on the continental map. + * All other value combinations display correctly, though there's no apparent distinction between those combinations. + * @param module_type na + * @param pulse na + * @param u3 na + * @param u4 na + * @param u5 na + * @param pos_xy na + */ +final case class ModuleInfo(module_type: Int, pulse: PulseState, u3: Long, u4: Boolean, u5: Boolean, pos_xy: Vector3) + +object ModuleInfo { + def apply(module_type: Int, pulseGreen: Boolean, u3: Long, pos_xy: Vector3): ModuleInfo = { + val intCharge: PulseState = if (pulseGreen) PulseState.Green else PulseState.Red + new ModuleInfo(module_type, intCharge, u3, u4 = false, u5 = false, pos_xy) + } +} + +/** + * na + * @param zone_number zone's number + * @param info list of module information for this zone + */ +case class VanuModuleUpdateMessage( + zone_number: Int, + info: List[ModuleInfo] + ) extends PlanetSideGamePacket { + type Packet = VanuModuleUpdateMessage + def opcode: Type = GamePacketOpcode.VanuModuleUpdateMessage + def encode: Attempt[BitVector] = VanuModuleUpdateMessage.encode(this) +} + +object VanuModuleUpdateMessage extends Marshallable[VanuModuleUpdateMessage] { + private val moduleInfoCodec: Codec[ModuleInfo] = ( + ("module_type" | uint4) :: + ("pulse" | PulseState.codec) :: + ("u3" | uint32L) :: + ("u4" | bool) :: + ("u5" | bool) :: + newcodecs.q_float(min = 0.0, max = 8192.0, bits = 20) :: + newcodecs.q_float(min = 0.0, max = 8192.0, bits = 20) + ).xmap[ModuleInfo]( + { + case module_type :: charged :: u3 :: u4 :: u5 :: x :: y :: HNil => + ModuleInfo(module_type, charged, u3, u4, u5, Vector3(x, y, 0f)) + }, + { + case ModuleInfo(module_type, charged, u3, u4, u5, Vector3(x, y, _)) => + module_type :: charged :: u3 :: u4 :: u5 :: x :: y :: HNil + } + ) + + implicit val codec: Codec[VanuModuleUpdateMessage] = ( + ("zone_number" | uint16L) :: + ("info" | listOfN(uint(bits = 6), moduleInfoCodec)) + ).as[VanuModuleUpdateMessage] +} diff --git a/src/test/scala/game/MailMessageTest.scala b/src/test/scala/game/MailMessageTest.scala index 4d1e62d41..da4a8d800 100644 --- a/src/test/scala/game/MailMessageTest.scala +++ b/src/test/scala/game/MailMessageTest.scala @@ -3,7 +3,6 @@ package game import org.specs2.mutable._ import net.psforever.packet._ -import net.psforever.packet.game.packets._ import net.psforever.packet.game.packets.MailMessage import scodec.bits._ diff --git a/src/test/scala/game/ModuleLimitsMessageTest.scala b/src/test/scala/game/ModuleLimitsMessageTest.scala new file mode 100644 index 000000000..f01e9089d --- /dev/null +++ b/src/test/scala/game/ModuleLimitsMessageTest.scala @@ -0,0 +1,29 @@ +// Copyright (c) 2026 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game.packets.ModuleLimitsMessage +import scodec.bits._ +import org.specs2.mutable.Specification + +class ModuleLimitsMessageTest extends Specification { + val string = hex"C9 FF3D80" + + "decode" in { + PacketCoding.decodePacket(string).require match { + case ModuleLimitsMessage(u1, u2, u3) => + u1 mustEqual 255 + u2 mustEqual 15 + u3 mustEqual 3 + case _ => + ko + } + } + + "encode" in { + val msg = ModuleLimitsMessage(255, 15, 3) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} diff --git a/src/test/scala/game/VanuModuleUpdateMessageTest.scala b/src/test/scala/game/VanuModuleUpdateMessageTest.scala new file mode 100644 index 000000000..78fb34314 --- /dev/null +++ b/src/test/scala/game/VanuModuleUpdateMessageTest.scala @@ -0,0 +1,58 @@ +// Copyright (c) 2026 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game.packets.{PulseState, ModuleInfo, VanuModuleUpdateMessage} +import net.psforever.types.Vector3 +import scodec.bits._ +import org.specs2.mutable.Specification + +class VanuModuleUpdateMessageTest extends Specification { + val string = hex"C1 020000" + val string_info = hex"c1 0200087000000000000100001140000000000006000008" //manufactured, not from packet captures + + "decode" in { + PacketCoding.decodePacket(string).require match { + case VanuModuleUpdateMessage(zone_number, info) => + zone_number mustEqual 2 + info.isEmpty mustEqual true + case _ => + ko + } + } + + "decode (info)" in { + PacketCoding.decodePacket(string_info).require match { + case VanuModuleUpdateMessage(zone_number, info) => + zone_number mustEqual 2 + info.size mustEqual 2 + info.head match { + case ModuleInfo(1, PulseState.Green, 0L, false, false, Vector3(2048, 2048, 0)) => ok + case _ => ko + } + info(1) match { + case ModuleInfo(5, PulseState.Red, 0L, false, false, Vector3(4098, 1024, 0)) => ok + case _ => ko + } + case _ => + ko + } + } + + "encode" in { + val msg = VanuModuleUpdateMessage(2, Nil) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string + } + + "encode (info)" in { + val msg = VanuModuleUpdateMessage(2, List( + ModuleInfo(module_type = 1, pulseGreen = true, u3 = 0L, Vector3(2048, 2048, 0)), + ModuleInfo(module_type = 5, pulseGreen = false, u3 = 0L, Vector3(4098, 1024, 0)) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string_info + } +} diff --git a/src/test/scala/objects/ZoneTest.scala b/src/test/scala/objects/ZoneTest.scala index 5e4a8a506..d3771dcb6 100644 --- a/src/test/scala/objects/ZoneTest.scala +++ b/src/test/scala/objects/ZoneTest.scala @@ -248,7 +248,7 @@ class ZonePopulationTest extends ActorTest { assert(zone.Players.size == 1) assert(zone.Players.head == avatar) - zone.Population ! Zone.Population.Leave(avatar, _) + zone.Population ! Zone.Population.Leave(avatar, null) val reply = receiveOne(Duration.create(100, "ms")) assert(reply.isInstanceOf[Zone.Population.PlayerHasLeft]) assert(zone.Players.isEmpty) From 330d5efa2da6a7effd84cce186717b05a3e24521 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 21 Sep 2026 13:17:47 -0400 Subject: [PATCH 2/2] special type for the modules; renamed fields to better suit the purpose; default packets during zone initialization --- .../session/support/ZoningOperations.scala | 6 +-- .../packets/VanuModuleUpdateMessage.scala | 52 +++++++++++++------ .../game/VanuModuleUpdateMessageTest.scala | 10 ++-- 3 files changed, 44 insertions(+), 24 deletions(-) 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 1cd058402..0831790b7 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -22,7 +22,7 @@ import net.psforever.objects.vehicles.control.{CargoBehavior, CarrierBehavior} import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity} import net.psforever.objects.zones.blockmap.BlockMapEntity import net.psforever.packet.game.packets.GenericAction.FirstPersonViewWithEffect -import net.psforever.packet.game.packets.{CampaignStatistic, ChangeFireStateMessage_Start, CloudInfo, CreateShortcutMessage, DeployableInfo, Friend, GenericActionMessage, GenericObjectActionEnum, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, StormInfo, TrainingZoneMessage, TriggeredSound, WeatherMessage} +import net.psforever.packet.game.packets.{CampaignStatistic, ChangeFireStateMessage_Start, CloudInfo, CreateShortcutMessage, DeployableInfo, Friend, GenericActionMessage, GenericObjectActionEnum, HackState7, MailMessage, ModuleLimitsMessage, ObjectDetectedMessage, SessionStatistic, StormInfo, TrainingZoneMessage, TriggeredSound, VanuModuleUpdateMessage, WeatherMessage} import net.psforever.services.avatar.support.{CorpseEnvelope, ReleaseEnvelope} import net.psforever.services.base.envelope.{BundledEnvelope, MessageEnvelope} import net.psforever.services.base.message.{GenericObjectAction, ObjectDelete, PlanetsideAttribute, SendResponse} @@ -623,8 +623,8 @@ class ZoningOperations( else sendResponse(ContinentalLockUpdateMessage(continentNumber, zone.lockedBy)) //CaptureFlagUpdateMessage() - //VanuModuleUpdateMessage() - //ModuleLimitsMessage() + sendResponse(VanuModuleUpdateMessage(continentNumber, Nil)) + sendResponse(ModuleLimitsMessage(255, 15, 3)) val isCavern = zone.map.cavern if (!isCavern) { sendResponse(ZoneInfoMessage(continentNumber, empire_status = true, 0L)) diff --git a/src/main/scala/net/psforever/packet/game/packets/VanuModuleUpdateMessage.scala b/src/main/scala/net/psforever/packet/game/packets/VanuModuleUpdateMessage.scala index 9b5dfef5c..a1e63a499 100644 --- a/src/main/scala/net/psforever/packet/game/packets/VanuModuleUpdateMessage.scala +++ b/src/main/scala/net/psforever/packet/game/packets/VanuModuleUpdateMessage.scala @@ -3,7 +3,7 @@ package net.psforever.packet.game.packets import enumeratum.values.{IntEnum, IntEnumEntry} import net.psforever.newcodecs.newcodecs -import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.packet.GamePacketOpcode.Type import net.psforever.types.Vector3 import scodec.{Attempt, Codec} @@ -11,22 +11,42 @@ import scodec.bits.BitVector import scodec.codecs._ import shapeless.{::, HNil} +sealed abstract class ModuleIcon(val value: Int) extends IntEnumEntry + +object ModuleIcon extends IntEnum[ModuleIcon] { + val values: IndexedSeq[ModuleIcon] = findValues + + case object NonPowered extends ModuleIcon(value = 1) + case object Speed extends ModuleIcon(value = 2) + case object Defender extends ModuleIcon(value = 3) + case object Vehicle extends ModuleIcon(value = 4) + case object Weapon extends ModuleIcon(value = 5) + case object Healing extends ModuleIcon(value = 6) + case object Pain extends ModuleIcon(value = 7) + + case object TR extends ModuleIcon(value = 10) + case object NC extends ModuleIcon(value = 11) + case object VS extends ModuleIcon(value = 12) + + implicit val codec: Codec[ModuleIcon] = PacketHelpers.createIntEnumCodec(ModuleIcon, uint4) +} + /** * Whether the vanu module icon on the continental map alternates red or green in between displays of it's distinguishing color. * There are four values - 0 to 3 - but 3 is the only green color and all of the other values result in red. * @param value charge state as an integer value */ -sealed abstract class PulseState(val value: Int) extends IntEnumEntry +sealed abstract class ModuleIconPulseColor(val value: Int) extends IntEnumEntry -object PulseState extends IntEnum[PulseState] { - val values: IndexedSeq[PulseState] = findValues +object ModuleIconPulseColor extends IntEnum[ModuleIconPulseColor] { + val values: IndexedSeq[ModuleIconPulseColor] = findValues /** Red module */ - case object Red extends PulseState(value = 0) + case object Red extends ModuleIconPulseColor(value = 0) /** Green module */ - case object Green extends PulseState(value = 3) + case object Green extends ModuleIconPulseColor(value = 3) - implicit val codec: Codec[PulseState] = uint(bits = 2).xmap( + implicit val codec: Codec[ModuleIconPulseColor] = uint2.xmap( { case 3 => Green case _ => Red @@ -46,11 +66,11 @@ object PulseState extends IntEnum[PulseState] { * @param u5 na * @param pos_xy na */ -final case class ModuleInfo(module_type: Int, pulse: PulseState, u3: Long, u4: Boolean, u5: Boolean, pos_xy: Vector3) +final case class ModuleInfo(module_type: ModuleIcon, pulse: ModuleIconPulseColor, u3: Long, u4: Boolean, u5: Boolean, pos_xy: Vector3) object ModuleInfo { - def apply(module_type: Int, pulseGreen: Boolean, u3: Long, pos_xy: Vector3): ModuleInfo = { - val intCharge: PulseState = if (pulseGreen) PulseState.Green else PulseState.Red + def apply(module_type: ModuleIcon, pulseGreen: Boolean, u3: Long, pos_xy: Vector3): ModuleInfo = { + val intCharge: ModuleIconPulseColor = if (pulseGreen) ModuleIconPulseColor.Green else ModuleIconPulseColor.Red new ModuleInfo(module_type, intCharge, u3, u4 = false, u5 = false, pos_xy) } } @@ -71,8 +91,8 @@ case class VanuModuleUpdateMessage( object VanuModuleUpdateMessage extends Marshallable[VanuModuleUpdateMessage] { private val moduleInfoCodec: Codec[ModuleInfo] = ( - ("module_type" | uint4) :: - ("pulse" | PulseState.codec) :: + ("module_type" | ModuleIcon.codec) :: + ("pulse" | ModuleIconPulseColor.codec) :: ("u3" | uint32L) :: ("u4" | bool) :: ("u5" | bool) :: @@ -80,12 +100,12 @@ object VanuModuleUpdateMessage extends Marshallable[VanuModuleUpdateMessage] { newcodecs.q_float(min = 0.0, max = 8192.0, bits = 20) ).xmap[ModuleInfo]( { - case module_type :: charged :: u3 :: u4 :: u5 :: x :: y :: HNil => - ModuleInfo(module_type, charged, u3, u4, u5, Vector3(x, y, 0f)) + case module_type :: pulse :: u3 :: u4 :: u5 :: x :: y :: HNil => + ModuleInfo(module_type, pulse, u3, u4, u5, Vector3(x, y, 0f)) }, { - case ModuleInfo(module_type, charged, u3, u4, u5, Vector3(x, y, _)) => - module_type :: charged :: u3 :: u4 :: u5 :: x :: y :: HNil + case ModuleInfo(module_type, pulse, u3, u4, u5, Vector3(x, y, _)) => + module_type :: pulse :: u3 :: u4 :: u5 :: x :: y :: HNil } ) diff --git a/src/test/scala/game/VanuModuleUpdateMessageTest.scala b/src/test/scala/game/VanuModuleUpdateMessageTest.scala index 78fb34314..c91ca5fd6 100644 --- a/src/test/scala/game/VanuModuleUpdateMessageTest.scala +++ b/src/test/scala/game/VanuModuleUpdateMessageTest.scala @@ -2,7 +2,7 @@ package game import net.psforever.packet._ -import net.psforever.packet.game.packets.{PulseState, ModuleInfo, VanuModuleUpdateMessage} +import net.psforever.packet.game.packets.{ModuleIcon, ModuleIconPulseColor, ModuleInfo, VanuModuleUpdateMessage} import net.psforever.types.Vector3 import scodec.bits._ import org.specs2.mutable.Specification @@ -27,11 +27,11 @@ class VanuModuleUpdateMessageTest extends Specification { zone_number mustEqual 2 info.size mustEqual 2 info.head match { - case ModuleInfo(1, PulseState.Green, 0L, false, false, Vector3(2048, 2048, 0)) => ok + case ModuleInfo(ModuleIcon.NonPowered, ModuleIconPulseColor.Green, 0L, false, false, Vector3(2048, 2048, 0)) => ok case _ => ko } info(1) match { - case ModuleInfo(5, PulseState.Red, 0L, false, false, Vector3(4098, 1024, 0)) => ok + case ModuleInfo(ModuleIcon.Weapon, ModuleIconPulseColor.Red, 0L, false, false, Vector3(4098, 1024, 0)) => ok case _ => ko } case _ => @@ -48,8 +48,8 @@ class VanuModuleUpdateMessageTest extends Specification { "encode (info)" in { val msg = VanuModuleUpdateMessage(2, List( - ModuleInfo(module_type = 1, pulseGreen = true, u3 = 0L, Vector3(2048, 2048, 0)), - ModuleInfo(module_type = 5, pulseGreen = false, u3 = 0L, Vector3(4098, 1024, 0)) + ModuleInfo(ModuleIcon.NonPowered, pulseGreen = true, u3 = 0L, Vector3(2048, 2048, 0)), + ModuleInfo(ModuleIcon.Weapon, pulseGreen = false, u3 = 0L, Vector3(4098, 1024, 0)) )) val pkt = PacketCoding.encodePacket(msg).require.toByteVector