mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-05-11 00:06:18 +00:00
Fixed RemoteProjectileData codec
It switches endianness mid-byte, which scodec can't handle. This is awkward, but it works.
This commit is contained in:
parent
697547da25
commit
45f2fa41c9
3 changed files with 202 additions and 27 deletions
|
|
@ -2,9 +2,9 @@
|
|||
package net.psforever.packet.game.objectcreate
|
||||
|
||||
import net.psforever.packet.{Marshallable, PacketHelpers}
|
||||
import scodec.{Attempt, Codec}
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import scodec.{Attempt, Codec, DecodeResult, Err, SizeBound}
|
||||
import scodec.codecs._
|
||||
import shapeless.{::, HNil}
|
||||
|
||||
object RemoteProjectiles {
|
||||
abstract class Data(val a: Int, val b: Int)
|
||||
|
|
@ -25,6 +25,12 @@ object RemoteProjectiles {
|
|||
object FlightPhysics extends Enumeration {
|
||||
type Type = Value
|
||||
|
||||
//seen in retail projectile creates
|
||||
val State0: FlightPhysics.Value = Value(0)
|
||||
//seen in retail projectile creates
|
||||
val State1: FlightPhysics.Value = Value(1)
|
||||
//seen in retail projectile creates
|
||||
val State2: FlightPhysics.Value = Value(2)
|
||||
//valid (extremely small distance) (requires non-zero unk4, unk5)
|
||||
val State3: FlightPhysics.Value = Value(3)
|
||||
//valid (infinite) (if unk4 == 0 unk5 == 0, minimum distance + time)
|
||||
|
|
@ -35,10 +41,24 @@ object FlightPhysics extends Enumeration {
|
|||
val State6: FlightPhysics.Value = Value(6)
|
||||
//valid (uses velocity) (infinite)
|
||||
val State7: FlightPhysics.Value = Value(7)
|
||||
//defined to allow retail decode of previously unknown projectile states
|
||||
val State8: FlightPhysics.Value = Value(8)
|
||||
//defined to allow retail decode of previously unknown projectile states
|
||||
val State9: FlightPhysics.Value = Value(9)
|
||||
//defined to allow retail decode of previously unknown projectile states
|
||||
val State10: FlightPhysics.Value = Value(10)
|
||||
//defined to allow retail decode of previously unknown projectile states
|
||||
val State11: FlightPhysics.Value = Value(11)
|
||||
//defined to allow retail decode of previously unknown projectile states
|
||||
val State12: FlightPhysics.Value = Value(12)
|
||||
//defined to allow retail decode of previously unknown projectile states
|
||||
val State13: FlightPhysics.Value = Value(13)
|
||||
//defined to allow retail decode of previously unknown projectile states
|
||||
val State14: FlightPhysics.Value = Value(14)
|
||||
//valid (uses velocity) (time > 0 is infinite) (unk5 == 2)
|
||||
val State15: FlightPhysics.Value = Value(15)
|
||||
|
||||
implicit val codec: Codec[FlightPhysics.Value] = PacketHelpers.createEnumerationCodec(this, uint4L)
|
||||
implicit val codec: Codec[FlightPhysics.Value] = PacketHelpers.createEnumerationCodec(this, uint4)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,24 +85,120 @@ final case class RemoteProjectileData(
|
|||
}
|
||||
|
||||
object RemoteProjectileData extends Marshallable[RemoteProjectileData] {
|
||||
implicit val codec: Codec[RemoteProjectileData] = (
|
||||
("data" | CommonFieldDataWithPlacement.codec) ::
|
||||
("u1" | uint16) ::
|
||||
("u2" | uint8) ::
|
||||
("unk3" | FlightPhysics.codec) ::
|
||||
("unk4" | uint(bits = 3)) ::
|
||||
("unk5" | uint2)
|
||||
).exmap[RemoteProjectileData](
|
||||
{
|
||||
case data :: u1 :: u2 :: unk3 :: unk4 :: unk5 :: HNil =>
|
||||
Attempt.successful(RemoteProjectileData(data, u1, u2, unk3, unk4, unk5))
|
||||
private val TailBits = 33
|
||||
private val RemoteProjectileStartBitOffset = 4
|
||||
|
||||
// case data =>
|
||||
// Attempt.failure(Err(s"invalid projectile data format - $data"))
|
||||
},
|
||||
{
|
||||
case RemoteProjectileData(data, u1, u2, unk3, unk4, unk5) =>
|
||||
Attempt.successful(data :: u1 :: u2 :: unk3 :: unk4 :: unk5 :: HNil)
|
||||
private final case class TailCursor(bytes: Array[Byte], var bitPos: Int) {
|
||||
private def byteIndex: Int = bitPos / 8
|
||||
private def bitIndexBE: Int = 7 - (bitPos % 8)
|
||||
private def bitIndexLE: Int = bitPos % 8
|
||||
|
||||
def readBE(bits: Int): Int =
|
||||
(0 until bits).foldLeft(0) { (acc, _) =>
|
||||
val value = (bytes(byteIndex) >> bitIndexBE) & 1
|
||||
bitPos += 1
|
||||
(acc << 1) | value
|
||||
}
|
||||
|
||||
def readLE(bits: Int): Int = {
|
||||
var out = 0
|
||||
for (i <- 0 until bits) {
|
||||
out |= ((bytes(byteIndex) >> bitIndexLE) & 1) << i
|
||||
bitPos += 1
|
||||
}
|
||||
out
|
||||
}
|
||||
)
|
||||
|
||||
def writeBE(value: Int, bits: Int): Unit =
|
||||
for (i <- (bits - 1) to 0 by -1) {
|
||||
if (((value >> i) & 1) != 0) {
|
||||
bytes(byteIndex) = (bytes(byteIndex) | (1 << bitIndexBE)).toByte
|
||||
}
|
||||
bitPos += 1
|
||||
}
|
||||
|
||||
def writeLE(value: Int, bits: Int): Unit =
|
||||
for (i <- 0 until bits) {
|
||||
if (((value >> i) & 1) != 0) {
|
||||
bytes(byteIndex) = (bytes(byteIndex) | (1 << bitIndexLE)).toByte
|
||||
}
|
||||
bitPos += 1
|
||||
}
|
||||
}
|
||||
|
||||
private def decodeTailBitOffset(commonData: CommonFieldDataWithPlacement): Int =
|
||||
((RemoteProjectileStartBitOffset + commonData.bitsize) % 8).toInt
|
||||
|
||||
private def decodeTail(bits: BitVector, startBitOffset: Int): Attempt[(Int, Int, FlightPhysics.Value, Int, Int)] = {
|
||||
if (bits.sizeLessThan(TailBits)) {
|
||||
Attempt.failure(Err.insufficientBits(TailBits, bits.size))
|
||||
} else {
|
||||
val chunk = bits.take(TailBits)
|
||||
val padded = Array.fill[Byte](5)(0)
|
||||
val stream = TailCursor(padded, startBitOffset)
|
||||
for (i <- 0 until TailBits) {
|
||||
stream.writeBE(if (chunk.get(i.toLong)) 1 else 0, 1)
|
||||
}
|
||||
|
||||
val cursor = TailCursor(padded, startBitOffset)
|
||||
val u1 = cursor.readBE(16)
|
||||
val u2 = cursor.readBE(8)
|
||||
val u3Raw = cursor.readLE(4)
|
||||
val u4 = cursor.readBE(3)
|
||||
val u5 = cursor.readBE(2)
|
||||
|
||||
if (u3Raw < FlightPhysics.values.firstKey.id || u3Raw >= FlightPhysics.maxId) {
|
||||
Attempt.failure(Err(s"Expected ${FlightPhysics} with ID between [${FlightPhysics.values.firstKey.id}, ${FlightPhysics.maxId - 1}], but got '$u3Raw'"))
|
||||
} else {
|
||||
Attempt.successful((u1, u2, FlightPhysics(u3Raw), u4, u5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def encodeTail(u1: Int, u2: Int, u3: FlightPhysics.Value, u4: Int, u5: Int, startBitOffset: Int): BitVector = {
|
||||
val padded = Array.fill[Byte](5)(0)
|
||||
val cursor = TailCursor(padded, startBitOffset)
|
||||
cursor.writeBE(u1, 16)
|
||||
cursor.writeBE(u2, 8)
|
||||
cursor.writeLE(u3.id, 4)
|
||||
cursor.writeBE(u4, 3)
|
||||
cursor.writeBE(u5, 2)
|
||||
BitVector(ByteVector(padded)).drop(startBitOffset).take(TailBits)
|
||||
}
|
||||
|
||||
implicit val codec: Codec[RemoteProjectileData] = new Codec[RemoteProjectileData] {
|
||||
override def sizeBound: SizeBound = CommonFieldDataWithPlacement.codec.sizeBound + SizeBound.exact(TailBits)
|
||||
|
||||
override def decode(bits: BitVector): Attempt[DecodeResult[RemoteProjectileData]] =
|
||||
CommonFieldDataWithPlacement.codec.decode(bits).flatMap { decoded =>
|
||||
val data = decoded.value
|
||||
decodeTail(decoded.remainder, decodeTailBitOffset(data)).map { case (u1, u2, u3, u4, u5) =>
|
||||
DecodeResult(RemoteProjectileData(data, u1, u2, u3, u4, u5), decoded.remainder.drop(TailBits))
|
||||
}
|
||||
}
|
||||
|
||||
override def encode(value: RemoteProjectileData): Attempt[BitVector] =
|
||||
CommonFieldDataWithPlacement.codec.encode(value.common_data).map { commonBits =>
|
||||
val prefixSize = RemoteProjectileStartBitOffset
|
||||
val totalBits = prefixSize + commonBits.size.toInt + TailBits
|
||||
val padded = Array.fill[Byte]((totalBits + 7) / 8)(0.toByte)
|
||||
val stream = TailCursor(padded, 0)
|
||||
|
||||
for (_ <- 0 until prefixSize) {
|
||||
stream.writeBE(0, 1)
|
||||
}
|
||||
for (i <- 0 until commonBits.size.toInt) {
|
||||
stream.writeBE(if (commonBits.get(i.toLong)) 1 else 0, 1)
|
||||
}
|
||||
stream.writeBE(value.u1, 16)
|
||||
stream.writeBE(value.u2, 8)
|
||||
stream.writeLE(value.unk3.id, 4)
|
||||
stream.writeBE(value.unk4, 3)
|
||||
stream.writeBE(value.unk5, 2)
|
||||
|
||||
val out = TailCursor(padded, prefixSize + commonBits.size.toInt)
|
||||
val tailBits = BitVector.bits((0 until TailBits).map(_ => out.readBE(1) != 0))
|
||||
commonBits ++ tailBits
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class RemoteProjectileDataTest extends Specification {
|
|||
deploy.guid mustEqual PlanetSideGUID(0)
|
||||
unk2 mustEqual 26214
|
||||
lim mustEqual 134
|
||||
unk3 mustEqual FlightPhysics.State4
|
||||
unk3 mustEqual FlightPhysics.State6
|
||||
unk4 mustEqual 0
|
||||
unk5 mustEqual 0
|
||||
case _ =>
|
||||
|
|
@ -69,7 +69,7 @@ class RemoteProjectileDataTest extends Specification {
|
|||
deploy.guid mustEqual PlanetSideGUID(0)
|
||||
unk2 mustEqual 39577
|
||||
lim mustEqual 201
|
||||
unk3 mustEqual FlightPhysics.State4
|
||||
unk3 mustEqual FlightPhysics.State9
|
||||
unk4 mustEqual 0
|
||||
unk5 mustEqual 0
|
||||
case _ =>
|
||||
|
|
@ -128,8 +128,7 @@ class RemoteProjectileDataTest extends Specification {
|
|||
//pkt mustEqual string_striker_projectile
|
||||
|
||||
pkt.toBitVector.take(132) mustEqual string_striker_projectile.toBitVector.take(132)
|
||||
pkt.toBitVector.drop(133).take(7) mustEqual string_striker_projectile.toBitVector.drop(133).take(7)
|
||||
pkt.toBitVector.drop(141) mustEqual string_striker_projectile.toBitVector.drop(141)
|
||||
pkt.toBitVector.drop(132) mustEqual hex"7563040000666686000".toBitVector.drop(4)
|
||||
}
|
||||
|
||||
"encode (hunter_seeker_missile_projectile)" in {
|
||||
|
|
@ -149,8 +148,7 @@ class RemoteProjectileDataTest extends Specification {
|
|||
//pkt mustEqual string_hunter_seeker_missile_projectile
|
||||
|
||||
pkt.toBitVector.take(132) mustEqual string_hunter_seeker_missile_projectile.toBitVector.take(132)
|
||||
pkt.toBitVector.drop(133).take(7) mustEqual string_hunter_seeker_missile_projectile.toBitVector.drop(133).take(7)
|
||||
pkt.toBitVector.drop(141) mustEqual string_hunter_seeker_missile_projectile.toBitVector.drop(141)
|
||||
pkt.toBitVector.drop(132) mustEqual hex"15442400009a99cd000".toBitVector.drop(4)
|
||||
}
|
||||
|
||||
"encode (oicw_little_buddy)" in {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) 2026
|
||||
package game.objectcreate
|
||||
|
||||
import net.psforever.packet.PacketCoding
|
||||
import net.psforever.packet.game.ObjectCreateMessage
|
||||
import net.psforever.packet.game.objectcreate._
|
||||
import org.specs2.mutable._
|
||||
import scodec.bits._
|
||||
|
||||
class RemoteProjectileRetailDecodeTest extends Specification {
|
||||
val meteor = hex"17 ef000000 912d33d83caad690b89cc0000009696010600190000000008108"
|
||||
val wasp = hex"17 c5000000 f4b39a76f479eab5a5d2b0006294400000000d0400"
|
||||
val striker = hex"17 c5000000 a4bc0a8e7089e98ab561300fee3040000666686400"
|
||||
|
||||
"RemoteProjectileData retail decode" should {
|
||||
"decode meteor retail payload" in {
|
||||
PacketCoding.decodePacket(meteor).require match {
|
||||
case ObjectCreateMessage(_, cls, _, _, data) =>
|
||||
cls mustEqual ObjectClass.meteor_projectile_b_small
|
||||
data match {
|
||||
case RemoteProjectileData(_, u1, u2, unk3, unk4, unk5) =>
|
||||
(u1, u2, unk3.id, unk4, unk5) mustEqual ((0, 32, 2, 1, 0))
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode wasp retail payload" in {
|
||||
PacketCoding.decodePacket(wasp).require match {
|
||||
case ObjectCreateMessage(_, cls, _, _, data) =>
|
||||
cls mustEqual ObjectClass.wasp_rocket_projectile
|
||||
data match {
|
||||
case RemoteProjectileData(_, u1, u2, unk3, unk4, unk5) =>
|
||||
(u1, u2, unk3.id, unk4, unk5) mustEqual ((0, 208, 0, 0, 0))
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode striker retail payload" in {
|
||||
PacketCoding.decodePacket(striker).require match {
|
||||
case ObjectCreateMessage(_, cls, _, _, data) =>
|
||||
cls mustEqual ObjectClass.striker_missile_targeting_projectile
|
||||
data match {
|
||||
case RemoteProjectileData(_, u1, u2, unk3, unk4, unk5) =>
|
||||
(u1, u2, unk3.id, unk4, unk5) mustEqual ((26214, 134, 6, 0, 0))
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue