Skip to content
Open
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
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 @@ -569,9 +569,9 @@ object GamePacketOpcode extends Enumeration {
case 0xdf => game.ZoneLockInfoMessage.decode

// OPCODES 0xe0-ef
case 0xe0 => noDecoder(SquadBindInfoMessage)
case 0xe0 => game.SquadBindInfoMessage.decode
case 0xe1 => noDecoder(AudioSequenceMessage)
case 0xe2 => noDecoder(SquadFacilityBindInfoMessage)
case 0xe2 => game.SquadFacilityBindInfoMessage.decode
case 0xe3 => game.ZoneForcedCavernConnectionsMessage.decode
case 0xe4 => noDecoder(MissionActionMessage)
case 0xe5 => noDecoder(MissionKillTriggerMessage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ object BindStatus extends Enumeration(1) {
* @param zone_number the number of the zone in which to display this spawn option;
* if `zone_number` is not the current zone, and the action is positive,
* a small map of the alternate zone with selectable spawn point will become visible
* @param unk4 na
* @param map_id the "MapID" of the facility the player is bound to;
* if spawn_group is AMS it is likely the source facility of the AMS;
* with spawn_group AMS pos does point to the AMS position;
* @param pos coordinates for any displayed deployment map icon;
* `x` and `y` determine the position
*/
Expand All @@ -69,7 +71,7 @@ final case class BindPlayerMessage(
logging: Boolean,
spawn_group: SpawnGroup,
zone_number: Long,
unk4: Long,
map_id: Long,
pos: Vector3
) extends PlanetSideGamePacket {
type Packet = BindPlayerMessage
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) 2025 PSForever
package net.psforever.packet.game

import net.psforever.packet.game.SquadBindInfoMessage.SquadBindEntry
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.Codec
import scodec.codecs._

final case class SquadBindInfoMessage(
unk0: Int, // squad/platoon index?
elements: Vector[SquadBindEntry],
) extends PlanetSideGamePacket {
type Packet = SquadBindInfoMessage
def opcode = GamePacketOpcode.SquadBindInfoMessage
def encode = SquadBindInfoMessage.encode(this)
}

object SquadBindInfoMessage extends Marshallable[SquadBindInfoMessage] {

/**
* SquadBindEntry
*
* If isBound is false unk1 and unk2 are 0.
* unk1
* @param squadMember index of squad member
* @param zoneID zone ID as in zX / mapX
* @param mapID MapID identifier in mapX.json
* @param isBound is bound to a facility
*/
final case class SquadBindEntry(
squadMember: Long,
zoneID: Long,
mapID: Int,
isBound: Boolean,
)

private implicit val squadBindEntryCodec: Codec[SquadBindEntry] = (
("squadMember" | uint32L) ::
("zoneID" | uint32L) ::
("mapID" | uint16L) ::
("isBound" | bool)
).as[SquadBindEntry]

implicit val codec: Codec[SquadBindInfoMessage] = (
("unk0" | int32L) ::
("squadBindEntries" | vectorOfN(int32L, squadBindEntryCodec))
).as[SquadBindInfoMessage]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) 2025 PSForever
package net.psforever.packet.game

import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.Codec
import scodec.codecs._

/**
* SquadFacilityBindInfoMessage
* @param unk0
* @param squadID ID of the squad this message is relating to
* @param mapID MapID of the facility the player is bound to;
* alternatively of the facility the AMS was pulled from?;
* @param zoneID ZoneID of where the bind is from and the MapID relates to
*/
final case class SquadFacilityBindInfoMessage(
unk0: Boolean,
squadID: Long,
mapID: Long,
zoneID: Long
) extends PlanetSideGamePacket {
type Packet = EmpireBenefitsMessage
def opcode = GamePacketOpcode.SquadFacilityBindInfoMessage
def encode = SquadFacilityBindInfoMessage.encode(this)
}

object SquadFacilityBindInfoMessage extends Marshallable[SquadFacilityBindInfoMessage] {

implicit val codec: Codec[SquadFacilityBindInfoMessage] = (
("unk0" | bool) ::
("squadID" | uint32L) ::
("mapID" | uint32L) ::
("zoneID" | uint32L)
).as[SquadFacilityBindInfoMessage]
}
3 changes: 2 additions & 1 deletion src/main/scala/net/psforever/types/ExperienceType.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023 PSForever
// Copyright (c) 2023, 2025 PSForever
package net.psforever.types

import enumeratum.values.{IntEnum, IntEnumEntry}
Expand All @@ -12,6 +12,7 @@ object ExperienceType extends IntEnum[ExperienceType] {
val values: IndexedSeq[ExperienceType] = findValues

case object Normal extends ExperienceType(value = 0)
case object Unk1 extends ExperienceType(value = 1) // lotsplay.gcap #33316 @ 596.403340s / asdf.gcap #26207 @ 157.773563s
case object Support extends ExperienceType(value = 2)
case object RabbitBall extends ExperienceType(value = 4)

Expand Down
160 changes: 160 additions & 0 deletions src/test/scala/game/SquadBindInfoMessageTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright (c) 2025 PSForever
package game

import net.psforever.packet._
import net.psforever.packet.game.SquadBindInfoMessage
import net.psforever.packet.game.SquadBindInfoMessage.SquadBindEntry
import org.specs2.mutable._
import scodec.bits._

class SquadBindInfoMessageTest extends Specification {

private val sample1 = hex"e0 00000000 04000000 000000000000000000000 08000000000000000000 08000000000000000000 10000000000000000000 0"
private val sample2 = hex"e0 01000000 06000000 000000000700000007008 08000000000000000000 08000000000000000000 060000000e0000000e01 05000000000000000000 04000000038000000800 4"
private val sample3 = hex"e0 01000000 08000000 000000000000000000000 08000000080000004004 08000000040000002002 06000000000000000000 04000000000000000000 02800000000000000000 01800000000000000000 00e0000000e0000000e0 1"
private val sample4 = hex"e0 ffffffff 0a000000 000000000000000000000 08000000000000000000 08000000000000000000 06000000000000000000 04000000070000000c00 82800000038000000700 41800000000000000000 00e00000000000000000 00800000000000000000 00480000003800000070 040"

"decode sample1" in {
PacketCoding.decodePacket(sample1).require match {
case SquadBindInfoMessage(u0, elements) =>
u0 mustEqual 0
elements mustEqual Vector(
SquadBindEntry(0,0,0,false),
SquadBindEntry(1,0,0,false),
SquadBindEntry(2,0,0,false),
SquadBindEntry(8,0,0,false)
)
case _ =>
ko
}
}

"encode sample1" in {
val msg = SquadBindInfoMessage(
unk0 = 0,
elements = Vector(
SquadBindEntry(0,0,0,false),
SquadBindEntry(1,0,0,false),
SquadBindEntry(2,0,0,false),
SquadBindEntry(8,0,0,false)
)
)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector

pkt mustEqual sample1
}

"decode sample2" in {
PacketCoding.decodePacket(sample2).require match {
case SquadBindInfoMessage(u0, elements) =>
u0 mustEqual 1
elements mustEqual Vector(
SquadBindEntry(0,7,7,true),
SquadBindEntry(1,0,0,false),
SquadBindEntry(2,0,0,false),
SquadBindEntry(3,7,7,true),
SquadBindEntry(5,0,0,false),
SquadBindEntry(8,7,16,true)
)
case _ =>
ko
}
}

"encode sample2" in {
val msg = SquadBindInfoMessage(
unk0 = 1,
elements = Vector(
SquadBindEntry(0,7,7,true),
SquadBindEntry(1,0,0,false),
SquadBindEntry(2,0,0,false),
SquadBindEntry(3,7,7,true),
SquadBindEntry(5,0,0,false),
SquadBindEntry(8,7,16,true)
)
)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector

pkt mustEqual sample2
}

"decode sample3" in {
PacketCoding.decodePacket(sample3).require match {
case SquadBindInfoMessage(u0, elements) =>
u0 mustEqual 1
elements mustEqual Vector(
SquadBindEntry(0,0,0,false),
SquadBindEntry(1,1,8,true),
SquadBindEntry(2,1,8,true),
SquadBindEntry(3,0,0,false),
SquadBindEntry(4,0,0,false),
SquadBindEntry(5,0,0,false),
SquadBindEntry(6,0,0,false),
SquadBindEntry(7,7,7,true)
)
case _ =>
ko
}
}

"encode sample3" in {
val msg = SquadBindInfoMessage(
unk0 = 1,
elements = Vector(
SquadBindEntry(0,0,0,false),
SquadBindEntry(1,1,8,true),
SquadBindEntry(2,1,8,true),
SquadBindEntry(3,0,0,false),
SquadBindEntry(4,0,0,false),
SquadBindEntry(5,0,0,false),
SquadBindEntry(6,0,0,false),
SquadBindEntry(7,7,7,true)
)
)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector

pkt mustEqual sample3
}

"decode sample4" in {
PacketCoding.decodePacket(sample4).require match {
case SquadBindInfoMessage(u0, elements) =>
u0 mustEqual -1
elements mustEqual Vector(
SquadBindEntry(0,0,0,false),
SquadBindEntry(1,0,0,false),
SquadBindEntry(2,0,0,false),
SquadBindEntry(3,0,0,false),
SquadBindEntry(4,7,12,true),
SquadBindEntry(5,7,14,true),
SquadBindEntry(6,0,0,false),
SquadBindEntry(7,0,0,false),
SquadBindEntry(8,0,0,false),
SquadBindEntry(9,7,14,true)
)
case _ =>
ko
}
}

"encode sample4" in {
val msg = SquadBindInfoMessage(
unk0 = -1,
elements = Vector(
SquadBindEntry(0,0,0,false),
SquadBindEntry(1,0,0,false),
SquadBindEntry(2,0,0,false),
SquadBindEntry(3,0,0,false),
SquadBindEntry(4,7,12,true),
SquadBindEntry(5,7,14,true),
SquadBindEntry(6,0,0,false),
SquadBindEntry(7,0,0,false),
SquadBindEntry(8,0,0,false),
SquadBindEntry(9,7,14,true)
)
)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector

pkt mustEqual sample4
}
}
86 changes: 86 additions & 0 deletions src/test/scala/game/SquadFacilityBindInfoMessageTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) 2025 PSForever
package game

import net.psforever.packet._
import net.psforever.packet.game.SquadFacilityBindInfoMessage
import org.specs2.mutable._
import scodec.bits._

class SquadFacilityBindInfoMessageTest extends Specification {

private val sample1 = hex"e2 0 38000000 00000000 00000000 0"
private val sample2 = hex"e2 0 20000000 80000000 38000000 0"
private val sample3 = hex"e2 0 18000000 48000000 28000000 0"

"decode sample1" in {
PacketCoding.decodePacket(sample1).require match {
case SquadFacilityBindInfoMessage(unk0, squadID, mapID, zoneID) =>
unk0 mustEqual false
squadID mustEqual 7
mapID mustEqual 0
zoneID mustEqual 0
case _ =>
ko
}
}

"encode sample1" in {
val msg = SquadFacilityBindInfoMessage(
unk0 = false,
squadID = 7,
mapID = 0,
zoneID = 0
)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector

pkt mustEqual sample1
}

"decode sample2" in {
PacketCoding.decodePacket(sample2).require match {
case SquadFacilityBindInfoMessage(unk0, squadID, mapID, zoneID) =>
unk0 mustEqual false
squadID mustEqual 4
mapID mustEqual 16
zoneID mustEqual 7
case _ =>
ko
}
}

"encode sample2" in {
val msg = SquadFacilityBindInfoMessage(
unk0 = false,
squadID = 4,
mapID = 16,
zoneID = 7
)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector

pkt mustEqual sample2
}

"decode sample3" in {
PacketCoding.decodePacket(sample3).require match {
case SquadFacilityBindInfoMessage(unk0, squadID, mapID, zoneID) =>
unk0 mustEqual false
squadID mustEqual 3
mapID mustEqual 9
zoneID mustEqual 5
case _ =>
ko
}
}

"encode sample3" in {
val msg = SquadFacilityBindInfoMessage(
unk0 = false,
squadID = 3,
mapID = 9,
zoneID = 5
)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector

pkt mustEqual sample3
}
}
Loading