Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/net/psforever/packet/GamePacketOpcode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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, PacketHelpers, 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}

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 ModuleIconPulseColor(val value: Int) extends IntEnumEntry

object ModuleIconPulseColor extends IntEnum[ModuleIconPulseColor] {
val values: IndexedSeq[ModuleIconPulseColor] = findValues

/** Red module */
case object Red extends ModuleIconPulseColor(value = 0)
/** Green module */
case object Green extends ModuleIconPulseColor(value = 3)

implicit val codec: Codec[ModuleIconPulseColor] = uint2.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: ModuleIcon, pulse: ModuleIconPulseColor, u3: Long, u4: Boolean, u5: Boolean, pos_xy: Vector3)

object ModuleInfo {
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)
}
}

/**
* 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" | ModuleIcon.codec) ::
("pulse" | ModuleIconPulseColor.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 :: pulse :: u3 :: u4 :: u5 :: x :: y :: HNil =>
ModuleInfo(module_type, pulse, u3, u4, u5, Vector3(x, y, 0f))
},
{
case ModuleInfo(module_type, pulse, u3, u4, u5, Vector3(x, y, _)) =>
module_type :: pulse :: u3 :: u4 :: u5 :: x :: y :: HNil
}
)

implicit val codec: Codec[VanuModuleUpdateMessage] = (
("zone_number" | uint16L) ::
("info" | listOfN(uint(bits = 6), moduleInfoCodec))
).as[VanuModuleUpdateMessage]
}
1 change: 0 additions & 1 deletion src/test/scala/game/MailMessageTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand Down
29 changes: 29 additions & 0 deletions src/test/scala/game/ModuleLimitsMessageTest.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
58 changes: 58 additions & 0 deletions src/test/scala/game/VanuModuleUpdateMessageTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) 2026 PSForever
package game

import net.psforever.packet._
import net.psforever.packet.game.packets.{ModuleIcon, ModuleIconPulseColor, 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(ModuleIcon.NonPowered, ModuleIconPulseColor.Green, 0L, false, false, Vector3(2048, 2048, 0)) => ok
case _ => ko
}
info(1) match {
case ModuleInfo(ModuleIcon.Weapon, ModuleIconPulseColor.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(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

pkt mustEqual string_info
}
}
2 changes: 1 addition & 1 deletion src/test/scala/objects/ZoneTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading