From a54ee2f0b75e38867dfb534acef148c2a36fa2ae Mon Sep 17 00:00:00 2001 From: Chord Date: Mon, 16 Dec 2019 13:32:54 -0500 Subject: [PATCH] Refine VNL multi-world with new vector codec --- .../newcodecs/PrefixedVectorCodec.scala | 34 ++++++++++ .../net/psforever/newcodecs/package.scala | 12 +++- .../packet/game/VNLWorldStatusMessage.scala | 12 ++-- .../game/VNLWorldStatusMessageTest.scala | 62 ++++++++++++++++--- .../src/main/scala/LoginSessionActor.scala | 7 +-- 5 files changed, 103 insertions(+), 24 deletions(-) create mode 100644 common/src/main/scala/net/psforever/newcodecs/PrefixedVectorCodec.scala diff --git a/common/src/main/scala/net/psforever/newcodecs/PrefixedVectorCodec.scala b/common/src/main/scala/net/psforever/newcodecs/PrefixedVectorCodec.scala new file mode 100644 index 00000000..dc875031 --- /dev/null +++ b/common/src/main/scala/net/psforever/newcodecs/PrefixedVectorCodec.scala @@ -0,0 +1,34 @@ +// Copyright (c) 2019 PSForever +package net.psforever.newcodecs + +import scodec._ +import scodec.bits.BitVector + +final class PrefixedVectorCodec[A](firstCodec: Codec[A], codec: Codec[A], limit: Option[Int] = None) extends Codec[Vector[A]] { + + def sizeBound = limit match { + case None => SizeBound.unknown + case Some(lim) => codec.sizeBound * lim.toLong + } + + def encode(vector: Vector[A]) = Encoder.encodeSeq(firstCodec)(vector.slice(0,1)).map { bits => + if (vector.length > 1) + bits ++ (Encoder.encodeSeq(codec)(vector.tail) getOrElse BitVector.empty) + else + bits + } + + def decode(buffer: BitVector) : scodec.Attempt[scodec.DecodeResult[Vector[A]]] = { + Decoder.decodeCollect[Vector, A](firstCodec, Some(1))(buffer) match { + case Attempt.Successful(firstValue) => + Decoder.decodeCollect[Vector, A](codec, limit map { _ - 1 })(firstValue.remainder) match { + case Attempt.Successful(secondValue) => + Attempt.successful(DecodeResult(firstValue.value ++ secondValue.value, secondValue.remainder)) + case Attempt.Failure(e) => Attempt.failure(e) + } + case Attempt.Failure(e) => Attempt.failure(e) + } + } + + override def toString = s"vector($codec)" +} diff --git a/common/src/main/scala/net/psforever/newcodecs/package.scala b/common/src/main/scala/net/psforever/newcodecs/package.scala index 75ebd1e9..103dced8 100644 --- a/common/src/main/scala/net/psforever/newcodecs/package.scala +++ b/common/src/main/scala/net/psforever/newcodecs/package.scala @@ -3,14 +3,22 @@ package net.psforever.newcodecs import scodec.Attempt import scodec.Attempt.{Failure, Successful} -import scodec.Codec +import scodec._ +import scodec.bits.BitVector package object newcodecs { - def q_double(min: Double, max: Double, bits: Int): Codec[Double] = new QuantizedDoubleCodec(min, max, bits) def q_float(min : Double, max : Double, bits : Int): Codec[Float] = q_double(min, max, bits).narrow(v => Attempt.successful(v.toFloat), _.toDouble) def binary_choice[A](choice: Boolean, codec_true: => Codec[A], codec_false: => Codec[A]): Codec[A] = new BinaryChoiceCodec(choice, codec_true, codec_false) + def prefixedVectorOfN[A](countCodec: Codec[Int], firstValueCodec: Codec[A], valueCodec: Codec[A]): Codec[Vector[A]] = + countCodec. + flatZip { count => new PrefixedVectorCodec(firstValueCodec, valueCodec, Some(count)) }. + narrow[Vector[A]]({ case (cnt, xs) => + if (xs.size == cnt) Attempt.successful(xs) + else Attempt.failure(Err(s"Insufficient number of elements: decoded ${xs.size} but should have decoded $cnt")) + }, xs => (xs.size, xs)). + withToString(s"vectorOfN($countCodec, $valueCodec)") } diff --git a/common/src/main/scala/net/psforever/packet/game/VNLWorldStatusMessage.scala b/common/src/main/scala/net/psforever/packet/game/VNLWorldStatusMessage.scala index f11b9850..c6c2371e 100644 --- a/common/src/main/scala/net/psforever/packet/game/VNLWorldStatusMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/VNLWorldStatusMessage.scala @@ -5,6 +5,7 @@ import java.net.{InetAddress, InetSocketAddress} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.types.PlanetSideEmpire +import net.psforever.newcodecs.newcodecs._ import scodec._ import scodec.bits._ import scodec.codecs._ @@ -30,8 +31,7 @@ final case class WorldInformation(name : String, status : WorldStatus.Value, connections : Vector[WorldConnectionInfo], empireNeed : PlanetSideEmpire.Value) -final case class VNLWorldStatusMessage(welcomeMessage : String, world_count : Int, world : WorldInformation, - other_worlds : Vector[WorldInformation] = Vector()) +final case class VNLWorldStatusMessage(welcomeMessage : String, worlds : Vector[WorldInformation]) extends PlanetSideGamePacket { type Packet = VNLWorldStatusMessage def opcode = GamePacketOpcode.VNLWorldStatusMessage @@ -119,9 +119,7 @@ object VNLWorldStatusMessage extends Marshallable[VNLWorldStatusMessage] { )).as[WorldInformation] implicit val codec : Codec[VNLWorldStatusMessage] = ( - ("welcome_message" | PacketHelpers.encodedWideString) :: - (("num_worlds" | uint8L) flatPrepend { num_worlds => - ("primary_world" | world_codec) :: - ("extra_worlds" | vectorOfN(provide(num_worlds-1), world_codec_aligned) - )})).as[VNLWorldStatusMessage] + ("welcome_message" | PacketHelpers.encodedWideString) :: + ("worlds" | prefixedVectorOfN(uint8L, world_codec, world_codec_aligned)) + ).as[VNLWorldStatusMessage] } diff --git a/common/src/test/scala/game/VNLWorldStatusMessageTest.scala b/common/src/test/scala/game/VNLWorldStatusMessageTest.scala index 23a5a789..6d0f1fd7 100644 --- a/common/src/test/scala/game/VNLWorldStatusMessageTest.scala +++ b/common/src/test/scala/game/VNLWorldStatusMessageTest.scala @@ -16,9 +16,12 @@ class VNLWorldStatusMessageTest extends Specification { "decode" in { PacketCoding.DecodePacket(string).require match { - case VNLWorldStatusMessage(message, _, world, extra_worlds) => - extra_worlds.length mustEqual 0 + case VNLWorldStatusMessage(message, worlds) => message mustEqual "Welcome to PlanetSide! " + + worlds.length mustEqual 1 + + val world = worlds(0) world.name mustEqual "gemini" world.empireNeed mustEqual PlanetSideEmpire.NC world.status mustEqual WorldStatus.Up @@ -36,13 +39,14 @@ class VNLWorldStatusMessageTest extends Specification { } "encode" in { - val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ", 1, - WorldInformation("gemini", WorldStatus.Up, ServerType.Released, + val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ", + Vector(WorldInformation("gemini", WorldStatus.Up, ServerType.Released, Vector( WorldConnectionInfo(new InetSocketAddress(InetAddress.getByName("64.37.158.69"), 30007)) ), PlanetSideEmpire.NC - ) + )) ) + //0100 04 00 01459e2540377540 val pkt = PacketCoding.EncodePacket(msg).require.toByteVector @@ -50,18 +54,56 @@ class VNLWorldStatusMessageTest extends Specification { pkt mustEqual string } + "encode and decode empty messages" in { + val string = hex"0584410041004100410000" + val empty_msg = VNLWorldStatusMessage("AAAA", Vector()) + val empty_pkt = PacketCoding.EncodePacket(empty_msg).require.toByteVector + + empty_pkt mustEqual string + + PacketCoding.DecodePacket(string).require match { + case VNLWorldStatusMessage(message, worlds) => + message mustEqual "AAAA" + worlds.length mustEqual 0 + case _ => + ko + } + } + "encode and decode multiple worlds" in { - val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ", 2, - WorldInformation("ABCDABCD1", WorldStatus.Up, ServerType.Released, Vector(), PlanetSideEmpire.NC), + var string = hex" 0597570065006c0063006f006d006500200074006f00200050006c0061006e0065007400530069006400650021002000028941424344414243443101000300006240414243444142434432000002020000" + + val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ", Vector( + WorldInformation("ABCDABCD1", WorldStatus.Up, ServerType.Released, Vector(), PlanetSideEmpire.NC), WorldInformation("ABCDABCD2", WorldStatus.Down, ServerType.Beta, Vector(), PlanetSideEmpire.TR) )) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector - println(pkt) + pkt mustEqual string - // TODO: actually test something - ok + string = hex" 0597570065006c0063006f006d006500200074006f00200050006c0061006e0065007400530069006400650021002000028941424344414243443101000300006240414243444142434432000002020000" + + PacketCoding.DecodePacket(string).require match { + case VNLWorldStatusMessage(message, worlds) => + message mustEqual "Welcome to PlanetSide! " + + worlds.length mustEqual 2 + + worlds(0).name mustEqual "ABCDABCD1" + worlds(0).empireNeed mustEqual PlanetSideEmpire.NC + worlds(0).status mustEqual WorldStatus.Up + worlds(0).serverType mustEqual ServerType.Released + worlds(0).connections.length mustEqual 0 + + worlds(1).name mustEqual "ABCDABCD2" + worlds(1).empireNeed mustEqual PlanetSideEmpire.TR + worlds(1).status mustEqual WorldStatus.Down + worlds(1).serverType mustEqual ServerType.Beta + worlds(1).connections.length mustEqual 0 + case _ => + ko + } } } diff --git a/pslogin/src/main/scala/LoginSessionActor.scala b/pslogin/src/main/scala/LoginSessionActor.scala index e9f49171..55e234d6 100644 --- a/pslogin/src/main/scala/LoginSessionActor.scala +++ b/pslogin/src/main/scala/LoginSessionActor.scala @@ -164,13 +164,10 @@ class LoginSessionActor extends Actor with MDCContextAware { } def updateServerList() = { - val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ", 2, - WorldInformation( - serverName, WorldStatus.Up, ServerType.Beta, Vector(WorldConnectionInfo(serverAddress)), PlanetSideEmpire.VS - ), + val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ", Vector( WorldInformation( - serverName + "A", WorldStatus.Up, ServerType.Beta, Vector(WorldConnectionInfo(serverAddress)), PlanetSideEmpire.TR + serverName, WorldStatus.Up, ServerType.Beta, Vector(WorldConnectionInfo(serverAddress)), PlanetSideEmpire.VS ) ) )