diff --git a/common/src/main/scala/net/psforever/packet/ControlPacketOpcode.scala b/common/src/main/scala/net/psforever/packet/ControlPacketOpcode.scala index 7c63e573..69bc25ed 100644 --- a/common/src/main/scala/net/psforever/packet/ControlPacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/ControlPacketOpcode.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.packet -import net.psforever.packet.control.SlottedMetaPacket +import net.psforever.packet.control.{RelatedA, RelatedB, SlottedMetaPacket} import scodec.bits.BitVector import scodec.{Attempt, Codec, DecodeResult, Err} import scodec.codecs._ @@ -74,15 +74,15 @@ object ControlPacketOpcode extends Enumeration { // OPCODES 0x10-1e case 0x10 => SlottedMetaPacket.decodeWithOpcode(SlottedMetaPacket7) - case 0x11 => control.RelatedA0.decode - case 0x12 => noDecoder(RelatedA1) - case 0x13 => noDecoder(RelatedA2) - case 0x14 => noDecoder(RelatedA3) - case 0x15 => control.RelatedB0.decode - case 0x16 => noDecoder(RelatedB1) - case 0x17 => noDecoder(RelatedB2) + case 0x11 => RelatedA.decodeWithOpcode(RelatedA0) + case 0x12 => RelatedA.decodeWithOpcode(RelatedA1) + case 0x13 => RelatedA.decodeWithOpcode(RelatedA2) + case 0x14 => RelatedA.decodeWithOpcode(RelatedA3) + case 0x15 => RelatedB.decodeWithOpcode(RelatedB0) + case 0x16 => RelatedB.decodeWithOpcode(RelatedB1) + case 0x17 => RelatedB.decodeWithOpcode(RelatedB2) // 0x18 - case 0x18 => noDecoder(RelatedB3) + case 0x18 => RelatedB.decodeWithOpcode(RelatedB3) case 0x19 => control.MultiPacketEx.decode case 0x1a => noDecoder(Unknown26) case 0x1b => noDecoder(Unknown27) diff --git a/common/src/main/scala/net/psforever/packet/control/MultiPacketCollector.scala b/common/src/main/scala/net/psforever/packet/control/MultiPacketCollector.scala new file mode 100644 index 00000000..16afb22c --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/control/MultiPacketCollector.scala @@ -0,0 +1,129 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.control + +import net.psforever.packet.PlanetSidePacket + +/** + * Message for holding a series of packets being moved through the system (server), + * eventually be bundled into a `MultiPacketEx` and dispatched to the client. + * Invalid packets are eliminated at the time of creation. + * At least one packet is necessary. + * @param packets a series of packets to be bundled together; + * this list is effectively immutable; + * the only way to access these packets is through pattern matching + */ +final case class MultiPacketBundle(private var packets : List[PlanetSidePacket]) { + MultiPacketBundle.collectValidPackets(packets) match { + case Nil => + throw new IllegalArgumentException("can not create with zero packets") + case list => + packets = list + } + + def +(t : MultiPacketBundle) : MultiPacketBundle = t match { + case MultiPacketBundle(list) => + MultiPacketBundle(packets ++ list) + case _ => + MultiPacketBundle(packets) + } +} + +object MultiPacketBundle { + /** + * Accept a series of packets of a specific supertype (`PlanetSidePacket`) + * and filter out subtypes that should be excluded. + * Show a generic disclaimer if any packets were filtered. + * Two of the four subclasses of `PlanetSidePacket` are accepted - `PlanetSideGamePacket` and `PlanetSideControlPacket`. + * @param packets a series of packets + * @return the accepted packets from the original group + */ + def collectValidPackets(packets : List[PlanetSidePacket]) : List[PlanetSidePacket] = { + import net.psforever.packet.{PlanetSideGamePacket, PlanetSideControlPacket} + val (good, bad) = packets.partition( { + case _ : PlanetSideGamePacket => true + case _ : PlanetSideControlPacket => true + case _ => false + }) + if(bad.nonEmpty) { + org.log4s.getLogger("MultiPacketBundle") + .warn(s"attempted to include packet types that are not in the whitelist; ${bad.size} items have been excluded") + } + good + } +} + +/** + * Accumulator for packets that will eventually be bundled and submitted for composing a `MultiPacketEx` packet. + */ +class MultiPacketCollector() { + private var bundle : List[PlanetSidePacket] = List.empty + + def Add(t : PlanetSidePacket) : Unit = Add(List(t)) + + def Add(t : MultiPacketBundle) : Unit = t match { + case MultiPacketBundle(list) => + Add(list) + } + + def Add(t : List[PlanetSidePacket]) : Unit = { + if(t.nonEmpty) { + bundle = bundle ++ t + } + } + + /** + * Retrieve the internal collection of packets. + * Reset the internal list of packets by clearing it. + * @return a loaded `MultiPacketBundle` object + */ + def Bundle : MultiPacketBundle = { + try { + val out = MultiPacketBundle(bundle) + bundle = List.empty + out + } + catch { + case _ : Exception => //catch and rethrow the exception + throw new RuntimeException("no packets") + } + } + + /** + * A safer `Bundle` that consumes any` Exceptions` that might be thrown in the process of producing output. + * @see `Bundle` + * @return a loaded `MultiPacketBundle` object, or `None` + */ + def BundleOption : Option[MultiPacketBundle] = { + try { + Some(Bundle) + } + catch { + case _ : Exception => + None + } + } +} + +object MultiPacketCollector { + /** + * Overload constructor that accepts initial packets. + * @param bundle previously bundled packets + * @return a `MultiPacketCollector` object + */ + def apply(bundle : MultiPacketBundle) : MultiPacketCollector = { + val obj = new MultiPacketCollector() + obj.Add(bundle) + obj + } + + /** + * Overload constructor that accepts initial packets. + * @param packets a series of packets + * @return a `MultiPacketCollector` object + */ + def apply(packets : List[PlanetSidePacket]) : MultiPacketCollector = { + val obj = new MultiPacketCollector() + obj.Add(packets) + obj + } +} diff --git a/common/src/main/scala/net/psforever/packet/control/RelatedA.scala b/common/src/main/scala/net/psforever/packet/control/RelatedA.scala new file mode 100644 index 00000000..08b33501 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/control/RelatedA.scala @@ -0,0 +1,38 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.control + +import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket} +import scodec.Codec +import scodec.bits.BitVector +import scodec.codecs._ + +/** + * Dispatched from the client in regards to errors trying to process prior `ControlPackets`. + * Explains which packet was in error by sending back its `subslot` number. + * @param slot the type of `ResultA` packet; + * valid types are integers 0-3 + * @param subslot identification of a control packet + */ +final case class RelatedA(slot : Int, subslot : Int) extends PlanetSideControlPacket { + type Packet = RelatedA + if(slot < 0 || slot > 3) { + throw new IllegalArgumentException(s"slot number is out of range - $slot") + } + + def opcode = { + val base = ControlPacketOpcode.RelatedA0.id + ControlPacketOpcode(base + slot) + } + def encode = RelatedA.encode(this).map(vect => vect.drop(8)) +} + +object RelatedA extends Marshallable[RelatedA] { + implicit val codec : Codec[RelatedA] = ( + ("slot" | uint8L.xmap[Int](a => a - ControlPacketOpcode.RelatedA0.id, a=>a) ) :: + ("subslot" | uint16) // the slot is big endian. see 0x00A42F76 + ).as[RelatedA] + + def decodeWithOpcode(slot : ControlPacketOpcode.Value)(bits : BitVector) = { + decode(ControlPacketOpcode.codec.encode(slot).require ++ bits) + } +} diff --git a/common/src/main/scala/net/psforever/packet/control/RelatedA0.scala b/common/src/main/scala/net/psforever/packet/control/RelatedA0.scala deleted file mode 100644 index 0bd34517..00000000 --- a/common/src/main/scala/net/psforever/packet/control/RelatedA0.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.packet.control - -import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket} -import scodec.Codec -import scodec.codecs._ - -/** - * Dispatched from the client in regards to errors trying to process prior `ControlPackets`. - * Explains which packet was in error by sending back its `subslot` number. - * @param subslot identification of a control packet - */ -final case class RelatedA0(subslot : Int) - extends PlanetSideControlPacket { - type Packet = RelatedA0 - def opcode = ControlPacketOpcode.RelatedA0 - def encode = RelatedA0.encode(this) -} - -object RelatedA0 extends Marshallable[RelatedA0] { - implicit val codec : Codec[RelatedA0] = ("subslot" | uint16).as[RelatedA0] -} diff --git a/common/src/main/scala/net/psforever/packet/control/RelatedB.scala b/common/src/main/scala/net/psforever/packet/control/RelatedB.scala new file mode 100644 index 00000000..2b71c9a2 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/control/RelatedB.scala @@ -0,0 +1,39 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.control + +import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket} +import scodec.Codec +import scodec.bits.BitVector +import scodec.codecs._ + +/** + * Dispatched to coordinate information regarding `ControlPacket` packets between the client and server. + * When dispatched by the client, it relates the current (or last received) `SlottedMetaPacket` `subslot` number back to the server. + * When dispatched by the server, it relates ??? + * @param slot the type of `ResultB` packet; + * valid types are integers 0-3 + * @param subslot identification of a control packet + */ +final case class RelatedB(slot : Int, subslot : Int) extends PlanetSideControlPacket { + type Packet = RelatedB + if(slot < 0 || slot > 3) { + throw new IllegalArgumentException(s"slot number is out of range - $slot") + } + + def opcode = { + val base = ControlPacketOpcode.RelatedB0.id + ControlPacketOpcode(base + slot) + } + def encode = RelatedB.encode(this).map(vect => vect.drop(8)) +} + +object RelatedB extends Marshallable[RelatedB] { + implicit val codec : Codec[RelatedB] = ( + ("slot" | uint8L.xmap[Int](a => a - ControlPacketOpcode.RelatedB0.id, a=>a) ) :: + ("subslot" | uint16) // the slot is big endian. see 0x00A42F76 + ).as[RelatedB] + + def decodeWithOpcode(slot : ControlPacketOpcode.Value)(bits : BitVector) = { + decode(ControlPacketOpcode.codec.encode(slot).require ++ bits) + } +} diff --git a/common/src/main/scala/net/psforever/packet/control/RelatedB0.scala b/common/src/main/scala/net/psforever/packet/control/RelatedB0.scala deleted file mode 100644 index 98372963..00000000 --- a/common/src/main/scala/net/psforever/packet/control/RelatedB0.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.packet.control - -import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket} -import scodec.Codec -import scodec.codecs._ - -/** - * Dispatched to coordinate information regarding `ControlPacket` packets between the client and server. - * When dispatched by the client, it relates the current (or last received) `SlottedMetaPacket` `subslot` number back to the server. - * When dispatched by the server, it relates ??? - * @param subslot identification of a control packet - */ -final case class RelatedB0(subslot : Int) - extends PlanetSideControlPacket { - type Packet = RelatedB0 - def opcode = ControlPacketOpcode.RelatedB0 - def encode = RelatedB0.encode(this) -} - -object RelatedB0 extends Marshallable[RelatedB0] { - implicit val codec : Codec[RelatedB0] = ("subslot" | uint16).as[RelatedB0] -} diff --git a/common/src/main/scala/net/psforever/packet/control/SlottedMetaAck.scala b/common/src/main/scala/net/psforever/packet/control/SlottedMetaAck.scala deleted file mode 100644 index 31c940e9..00000000 --- a/common/src/main/scala/net/psforever/packet/control/SlottedMetaAck.scala +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.packet.control - -import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket} -import scodec.Codec -import scodec.bits.BitVector -import scodec.codecs._ - -final case class SlottedMetaAck(slot : Int, subslot : Int) - extends PlanetSideControlPacket { - type Packet = SlottedMetaAck - - assert(slot >= 0 && slot <= 7, s"Slot number ($slot) is out of range") //TODO 7 types of SlottedMeta, 4 types of ResultB? - - def opcode = { - val base = ControlPacketOpcode.RelatedB0.id - ControlPacketOpcode(base + slot % 4) - } - - // XXX: a nasty hack to ignore the "slot" field - // There is so much wrong with this it's not even funny. Why scodec, whyyyy... - // I've never had a library make me feel so stupid and smart at the same time - def encode = SlottedMetaAck.encode(this).map(vect => vect.drop(8)) -} - -object SlottedMetaAck extends Marshallable[SlottedMetaAck] { - implicit val codec : Codec[SlottedMetaAck] = ( - ("slot" | uint8L.xmap[Int](a => a - ControlPacketOpcode.RelatedB0.id, a=>a) ) :: - ("subslot" | uint16) - ).as[SlottedMetaAck] - - def decodeWithOpcode(slot : ControlPacketOpcode.Value)(bits : BitVector) = { - decode(ControlPacketOpcode.codec.encode(slot).require ++ bits) - } -} \ No newline at end of file diff --git a/common/src/test/scala/control/MultiPacketCollectorTest.scala b/common/src/test/scala/control/MultiPacketCollectorTest.scala new file mode 100644 index 00000000..38d7e9f9 --- /dev/null +++ b/common/src/test/scala/control/MultiPacketCollectorTest.scala @@ -0,0 +1,183 @@ +// Copyright (c) 2017 PSForever +package control + +import org.specs2.mutable._ +import net.psforever.packet.control.{ControlSync, MultiPacketBundle, MultiPacketCollector} +import net.psforever.packet.crypto.{ClientFinished, ServerFinished} +import net.psforever.packet.game.{ObjectDeleteMessage, PlanetSideGUID} + +class MultiPacketCollectorTest extends Specification { + val packet1 = ObjectDeleteMessage(PlanetSideGUID(1103), 2) + + "MultiPacketBundle" should { + import scodec.bits._ + val packet2 = ControlSync(21096, 0x4d, 0x52, 0x4d, 0x7c, 0x4d, 0x276, 0x275) + + "construct" in { + MultiPacketBundle(List(packet1)) + ok + } + + "fail to construct if not initialized with PlanetSidePackets" in { + MultiPacketBundle(Nil) must throwA[IllegalArgumentException] + } + + "concatenate bundles into a new bundle" in { + val obj1 = MultiPacketBundle(List(packet1)) + val obj2 = MultiPacketBundle(List(packet2)) + val obj3 = obj1 + obj2 + obj3 match { + case MultiPacketBundle(list) => + list.size mustEqual 2 + list.head mustEqual packet1 + list(1) mustEqual packet2 + case _ => + ko + } + } + + "accept PlanetSideGamePackets and PlanetSideControlPackets" in { + MultiPacketBundle(List(packet2, packet1)) match { + case MultiPacketBundle(list) => + list.size mustEqual 2 + list.head mustEqual packet2 + list(1) mustEqual packet1 + case _ => + ko + } + } + + "ignore other types of PlanetSideContainerPackets" in { + val param = List(packet2, ClientFinished(hex"", hex""), packet1, ServerFinished(hex"")) + MultiPacketBundle(param) match { //warning message will display in log + case MultiPacketBundle(list) => + list.size mustEqual 2 + list.head mustEqual param.head + list(1) mustEqual param(2) + case _ => + ko + } + } + } + + "MultiPacketCollector" should { + val packet2 = ObjectDeleteMessage(PlanetSideGUID(1105), 2) + val packet3 = ObjectDeleteMessage(PlanetSideGUID(1107), 2) + + "construct" in { + new MultiPacketCollector() + ok + } + + "construct with initial packets" in { + MultiPacketCollector(List(packet1, packet2)) + ok + } + + "can retrieve a bundle packets" in { + val obj = MultiPacketCollector(List(packet1, packet2)) + obj.Bundle match { + case MultiPacketBundle(list) => + list.size mustEqual 2 + list.head mustEqual packet1 + list(1) mustEqual packet2 + case _ => + ko + } + } + + "can not retrieve a bundle of non-existent packets" in { + val obj = new MultiPacketCollector() + obj.Bundle must throwA[RuntimeException] + } + + "can safely retrieve a bundle of potential packets" in { + val obj1 = new MultiPacketCollector() + obj1.BundleOption match { + case Some(_) => + ko + case _ => ; + } + + val obj2 = MultiPacketCollector(List(packet1, packet2)) + obj2.BundleOption match { + case None => + ko + case Some(MultiPacketBundle(list)) => + list.size mustEqual 2 + list.head mustEqual packet1 + list(1) mustEqual packet2 + } + } + + "clear packets after being asked to bundle" in { + val list = List(packet1, packet2) + val obj = MultiPacketCollector(list) + obj.Bundle mustEqual MultiPacketBundle(list) + obj.Bundle must throwA[RuntimeException] + } + + "add a packet" in { + val obj = new MultiPacketCollector() + obj.Add(packet1) + obj.Bundle match { + case MultiPacketBundle(list) => + list.size mustEqual 1 + list.head mustEqual packet1 + case _ => + ko + } + } + + "add packets" in { + val obj = new MultiPacketCollector() + obj.Add(List(packet1, packet2)) + obj.Bundle match { + case MultiPacketBundle(list) => + list.size mustEqual 2 + list.head mustEqual packet1 + list(1) mustEqual packet2 + case _ => + ko + } + } + + "concatenate bundles (1)" in { + + val obj1 = new MultiPacketCollector() + obj1.Add(List(packet1, packet2)) + val bundle1 = obj1.Bundle + + val obj2 = MultiPacketCollector(bundle1) + obj2.Add(packet3) + obj2.Bundle match { + case MultiPacketBundle(list) => + list.size mustEqual 3 + list.head mustEqual packet1 + list(1) mustEqual packet2 + list(2) mustEqual packet3 + case _ => + ko + } + } + + "concatenate bundles (2)" in { + val obj1 = new MultiPacketCollector() + obj1.Add(List(packet1, packet2)) + val bundle1 = obj1.Bundle + + val obj2 = new MultiPacketCollector() + obj2.Add(packet3) + obj2.Add(bundle1) + obj2.Bundle match { + case MultiPacketBundle(list) => + list.size mustEqual 3 + list.head mustEqual packet3 + list(1) mustEqual packet1 + list(2) mustEqual packet2 + case _ => + ko + } + } + } +} diff --git a/common/src/test/scala/control/RelatedATest.scala b/common/src/test/scala/control/RelatedATest.scala index 6e28c955..b62b2210 100644 --- a/common/src/test/scala/control/RelatedATest.scala +++ b/common/src/test/scala/control/RelatedATest.scala @@ -3,24 +3,80 @@ package control import org.specs2.mutable._ import net.psforever.packet._ -import net.psforever.packet.control._ +import net.psforever.packet.control.RelatedA import scodec.bits._ class RelatedATest extends Specification { val string0 = hex"00 11 01 04" + val string1 = hex"00 12 01 04" + val string2 = hex"00 13 01 04" + val string3 = hex"00 14 01 04" "decode (0)" in { PacketCoding.DecodePacket(string0).require match { - case RelatedA0(slot) => - slot mustEqual 260 + case RelatedA(slot, subslot) => + slot mustEqual 0 + subslot mustEqual 260 + case _ => + ko + } + } + + "decode (1)" in { + PacketCoding.DecodePacket(string1).require match { + case RelatedA(slot, subslot) => + slot mustEqual 1 + subslot mustEqual 260 + case _ => + ko + } + } + + "decode (2)" in { + PacketCoding.DecodePacket(string2).require match { + case RelatedA(slot, subslot) => + slot mustEqual 2 + subslot mustEqual 260 + case _ => + ko + } + } + + "decode (3)" in { + PacketCoding.DecodePacket(string3).require match { + case RelatedA(slot, subslot) => + slot mustEqual 3 + subslot mustEqual 260 case _ => ko } } "encode (0)" in { - val pkt = RelatedA0(260) + val pkt = RelatedA(0, 260) val msg = PacketCoding.EncodePacket(pkt).require.toByteVector msg mustEqual string0 } + + "encode (1)" in { + val pkt = RelatedA(1, 260) + val msg = PacketCoding.EncodePacket(pkt).require.toByteVector + msg mustEqual string1 + } + + "encode (2)" in { + val pkt = RelatedA(2, 260) + val msg = PacketCoding.EncodePacket(pkt).require.toByteVector + msg mustEqual string2 + } + + "encode (3)" in { + val pkt = RelatedA(3, 260) + val msg = PacketCoding.EncodePacket(pkt).require.toByteVector + msg mustEqual string3 + } + + "encode (n)" in { + RelatedA(4, 260) must throwA[IllegalArgumentException] + } } diff --git a/common/src/test/scala/control/RelatedBTest.scala b/common/src/test/scala/control/RelatedBTest.scala index f9dbe56e..3b29cf91 100644 --- a/common/src/test/scala/control/RelatedBTest.scala +++ b/common/src/test/scala/control/RelatedBTest.scala @@ -3,24 +3,80 @@ package control import org.specs2.mutable._ import net.psforever.packet._ -import net.psforever.packet.control._ +import net.psforever.packet.control.RelatedB import scodec.bits._ class RelatedBTest extends Specification { val string0 = hex"00 15 01 04" + val string1 = hex"00 16 01 04" + val string2 = hex"00 17 01 04" + val string3 = hex"00 18 01 04" "decode (0)" in { PacketCoding.DecodePacket(string0).require match { - case RelatedB0(slot) => - slot mustEqual 260 + case RelatedB(slot, subslot) => + slot mustEqual 0 + subslot mustEqual 260 + case _ => + ko + } + } + + "decode (1)" in { + PacketCoding.DecodePacket(string1).require match { + case RelatedB(slot, subslot) => + slot mustEqual 1 + subslot mustEqual 260 + case _ => + ko + } + } + + "decode (2)" in { + PacketCoding.DecodePacket(string2).require match { + case RelatedB(slot, subslot) => + slot mustEqual 2 + subslot mustEqual 260 + case _ => + ko + } + } + + "decode (3)" in { + PacketCoding.DecodePacket(string3).require match { + case RelatedB(slot, subslot) => + slot mustEqual 3 + subslot mustEqual 260 case _ => ko } } "encode (0)" in { - val pkt = RelatedB0(260) + val pkt = RelatedB(0, 260) val msg = PacketCoding.EncodePacket(pkt).require.toByteVector msg mustEqual string0 } + + "encode (1)" in { + val pkt = RelatedB(1, 260) + val msg = PacketCoding.EncodePacket(pkt).require.toByteVector + msg mustEqual string1 + } + + "encode (2)" in { + val pkt = RelatedB(2, 260) + val msg = PacketCoding.EncodePacket(pkt).require.toByteVector + msg mustEqual string2 + } + + "encode (3)" in { + val pkt = RelatedB(3, 260) + val msg = PacketCoding.EncodePacket(pkt).require.toByteVector + msg mustEqual string3 + } + + "encode (n)" in { + RelatedB(4, 260) must throwA[IllegalArgumentException] + } } diff --git a/common/src/test/scala/control/SlottedMetaAckTest.scala b/common/src/test/scala/control/SlottedMetaAckTest.scala deleted file mode 100644 index 161ba526..00000000 --- a/common/src/test/scala/control/SlottedMetaAckTest.scala +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2017 PSForever -package control - -import org.specs2.mutable._ -import net.psforever.packet._ -import net.psforever.packet.control._ -import scodec.bits._ - -class SlottedMetaAckTest extends Specification { - val string = hex"00150da4" - - "decode" in { - PacketCoding.DecodePacket(string).require match { - case SlottedMetaAck(_, _) => - ko - case RelatedB0(subslot) => //important! - subslot mustEqual 3492 - case _ => - ko - } - } - - "encode" in { - val pkt = SlottedMetaAck(0, 3492) - val msg = PacketCoding.EncodePacket(pkt).require.toByteVector - - msg mustEqual string - } -} diff --git a/pslogin/src/main/scala/LoginSessionActor.scala b/pslogin/src/main/scala/LoginSessionActor.scala index 9010e043..6260563c 100644 --- a/pslogin/src/main/scala/LoginSessionActor.scala +++ b/pslogin/src/main/scala/LoginSessionActor.scala @@ -10,6 +10,7 @@ import scodec.bits._ import MDCContextAware.Implicits._ import com.github.mauricio.async.db.{Connection, QueryResult, RowData} import com.github.mauricio.async.db.mysql.exceptions.MySQLException +import net.psforever.objects.DefaultCancellable import net.psforever.types.PlanetSideEmpire import scala.concurrent.{Await, Future} @@ -25,7 +26,7 @@ class LoginSessionActor extends Actor with MDCContextAware { var leftRef : ActorRef = ActorRef.noSender var rightRef : ActorRef = ActorRef.noSender - var updateServerListTask : Cancellable = LoginSessionActor.DefaultCancellable + var updateServerListTask : Cancellable = DefaultCancellable.obj override def postStop() = { if(updateServerListTask != null) @@ -54,59 +55,22 @@ class LoginSessionActor extends Actor with MDCContextAware { def Started : Receive = { case UpdateServerList() => updateServerList() - case ctrl @ ControlPacket(_, _) => - handlePktContainer(ctrl) - case game @ GamePacket(_, _, _) => - handlePktContainer(game) - case default => failWithError(s"Invalid packet class received: $default") - } - - def handlePkt(pkt : PlanetSidePacket) : Unit = pkt match { - case ctrl : PlanetSideControlPacket => + case ControlPacket(_, ctrl) => handleControlPkt(ctrl) - case game : PlanetSideGamePacket => + case GamePacket(_, _, game) => handleGamePkt(game) case default => failWithError(s"Invalid packet class received: $default") } - def handlePktContainer(pkt : PlanetSidePacketContainer) : Unit = pkt match { - case ctrl @ ControlPacket(opcode, ctrlPkt) => - handleControlPkt(ctrlPkt) - case game @ GamePacket(opcode, seq, gamePkt) => - handleGamePkt(gamePkt) - case default => failWithError(s"Invalid packet container class received: $default") - } - def handleControlPkt(pkt : PlanetSideControlPacket) = { pkt match { - case SlottedMetaPacket(slot, subslot, innerPacket) => - // Meta packets are like TCP packets - then need to be ACKed to the client - sendResponse(PacketCoding.CreateControlPacket(SlottedMetaAck(slot, subslot))) - - // Decode the inner packet and handle it or error - PacketCoding.DecodePacket(innerPacket).fold({ - error => log.error(s"Failed to decode inner packet of SlottedMetaPacket: $error") - }, { - handlePkt(_) - }) /// TODO: figure out what this is what what it does for the PS client /// I believe it has something to do with reliable packet transmission and resending - case sync @ ControlSync(diff, unk, f1, f2, f3, f4, fa, fb) => + case sync @ ControlSync(diff, _, _, _, _, _, fa, fb) => log.trace(s"SYNC: $sync") - val serverTick = Math.abs(System.nanoTime().toInt) // limit the size to prevent encoding error - sendResponse(PacketCoding.CreateControlPacket(ControlSyncResp(diff, serverTick, - fa, fb, fb, fa))) - case MultiPacket(packets) => + sendResponse(PacketCoding.CreateControlPacket(ControlSyncResp(diff, serverTick, fa, fb, fb, fa))) - /// Extract out each of the subpackets in the MultiPacket and handle them or raise a packet error - packets.foreach { pkt => - PacketCoding.DecodePacket(pkt).fold({ error => - log.error(s"Failed to decode inner packet of MultiPacket: $error") - }, { - handlePkt(_) - }) - } case default => log.error(s"Unhandled ControlPacket $default") } @@ -119,7 +83,6 @@ class LoginSessionActor extends Actor with MDCContextAware { // TESTING CODE FOR ACCOUNT LOOKUP def accountLookup(username : String, password : String) : Boolean = { val connection: Connection = DatabaseConnector.getAccountsConnection - Await.result(connection.connect, 5 seconds) // create account @@ -135,13 +98,13 @@ class LoginSessionActor extends Actor with MDCContextAware { case Some(resultSet) => val row : RowData = resultSet.head row(0) - case None => -1 - } - ) + case None => + -1 + }) try { // XXX: remove awaits - val result = Await.result( mapResult, 5 seconds ) + Await.result( mapResult, 5 seconds ) return true } catch { case e : MySQLException => @@ -151,7 +114,6 @@ class LoginSessionActor extends Actor with MDCContextAware { } finally { connection.disconnect } - false } @@ -190,22 +152,25 @@ class LoginSessionActor extends Actor with MDCContextAware { log.info(s"Failed login to account $username") sendResponse(PacketCoding.CreateGamePacket(0, response)) } + case ConnectToWorldRequestMessage(name, _, _, _, _, _, _) => log.info(s"Connect to world request for '$name'") - val response = ConnectToWorldMessage(serverName, serverAddress.getHostString, serverAddress.getPort) sendResponse(PacketCoding.CreateGamePacket(0, response)) sendResponse(DropSession(sessionId, "user transferring to world")) - case default => log.debug(s"Unhandled GamePacket $pkt") + + case _ => + log.debug(s"Unhandled GamePacket $pkt") } def updateServerList() = { val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ", Vector( - WorldInformation(serverName, WorldStatus.Up, ServerType.Beta, - Vector(WorldConnectionInfo(serverAddress)), PlanetSideEmpire.VS) - )) - + WorldInformation( + serverName, WorldStatus.Up, ServerType.Beta, Vector(WorldConnectionInfo(serverAddress)), PlanetSideEmpire.VS + ) + ) + ) sendResponse(PacketCoding.CreateGamePacket(0, msg)) } @@ -216,22 +181,13 @@ class LoginSessionActor extends Actor with MDCContextAware { def sendResponse(cont : Any) = { log.trace("LOGIN SEND: " + cont) - MDC("sessionId") = sessionId.toString rightRef !> cont } def sendRawResponse(pkt : ByteVector) = { log.trace("LOGIN SEND RAW: " + pkt) - MDC("sessionId") = sessionId.toString rightRef !> RawPacket(pkt) } } - -object LoginSessionActor { - final val DefaultCancellable = new Cancellable() { - def isCancelled : Boolean = true - def cancel : Boolean = true - } -} diff --git a/pslogin/src/main/scala/PacketCodingActor.scala b/pslogin/src/main/scala/PacketCodingActor.scala index 9d597ba6..5a5b06b6 100644 --- a/pslogin/src/main/scala/PacketCodingActor.scala +++ b/pslogin/src/main/scala/PacketCodingActor.scala @@ -5,7 +5,10 @@ import scodec.Attempt.{Failure, Successful} import scodec.bits._ import org.log4s.MDC import MDCContextAware.Implicits._ -import net.psforever.packet.control.{HandleGamePacket, SlottedMetaPacket} +import net.psforever.packet.control.{HandleGamePacket, _} + +import scala.annotation.tailrec +import scala.collection.mutable /** * In between the network side and the higher functioning side of the simulation: @@ -30,13 +33,14 @@ import net.psforever.packet.control.{HandleGamePacket, SlottedMetaPacket} */ class PacketCodingActor extends Actor with MDCContextAware { private var sessionId : Long = 0 - private var subslot : Int = 0 + private var subslotOutbound : Int = 0 + private var subslotInbound : Int = 0 private var leftRef : ActorRef = ActorRef.noSender private var rightRef : ActorRef = ActorRef.noSender private[this] val log = org.log4s.getLogger override def postStop() = { - subslot = 0 //in case this `Actor` restarts + subslotOutbound = 0 //in case this `Actor` restarts super.postStop() } @@ -68,12 +72,7 @@ class PacketCodingActor extends Actor with MDCContextAware { mtuLimit(msg) } else {//from network, to LSA, WSA, etc. - decode - PacketCoding.unmarshalPayload(0, msg) match { //TODO is it safe for this to always be 0? - case Successful(packet) => - sendResponseRight(packet) - case Failure(ex) => - log.info(s"Failed to marshal a packet: $ex") - } + UnmarshalInnerPacket(msg, "a packet") } //known elevated packet type case ctrl @ ControlPacket(_, packet) => @@ -88,7 +87,7 @@ class PacketCodingActor extends Actor with MDCContextAware { else { //deprecated; ControlPackets should not be coming from this direction log.warn(s"DEPRECATED CONTROL PACKET SEND: $ctrl") MDC("sessionId") = sessionId.toString - sendResponseRight(ctrl) + handlePacketContainer(ctrl) //sendResponseRight } //known elevated packet type case game @ GamePacket(_, _, packet) => @@ -105,33 +104,36 @@ class PacketCodingActor extends Actor with MDCContextAware { MDC("sessionId") = sessionId.toString sendResponseRight(game) } + //bundling packets into a SlottedMetaPacket0/MultiPacketEx + case msg @ MultiPacketBundle(list) => + log.trace(s"BUNDLE PACKET REQUEST SEND, LEFT (always): $msg") + handleBundlePacket(list) //etc case msg => - log.trace(s"PACKET SEND, LEFT: $msg") if(sender == rightRef) { + log.trace(s"BASE CASE PACKET SEND, LEFT: $msg") MDC("sessionId") = sessionId.toString leftRef !> msg } else { + log.trace(s"BASE CASE PACKET SEND, RIGHT: $msg") MDC("sessionId") = sessionId.toString rightRef !> msg } -// case default => -// failWithError(s"Invalid message '$default' received in state Established") } /** * Retrieve the current subslot number. * Increment the `subslot` for the next time it is needed. - * @return a 16u number starting at 0 + * @return a `16u` number starting at 0 */ def Subslot : Int = { - if(subslot == 65536) { //TODO what is the actual wrap number? - subslot = 0 - subslot + if(subslotOutbound == 65536) { //TODO what is the actual wrap number? + subslotOutbound = 0 + subslotOutbound } else { - val curr = subslot - subslot += 1 + val curr = subslotOutbound + subslotOutbound += 1 curr } } @@ -173,16 +175,74 @@ class PacketCodingActor extends Actor with MDCContextAware { def handleSplitPacket(data : ByteVector) : Unit = { val lim = PacketCodingActor.MTU_LIMIT_BYTES - 4 //4 bytes is the base size of SlottedMetaPacket data.grouped(lim).foreach(bvec => { - val pkt = PacketCoding.CreateControlPacket(SlottedMetaPacket(4, Subslot, bvec)) - PacketCoding.EncodePacket(pkt.packet) match { + PacketCoding.EncodePacket(SlottedMetaPacket(4, Subslot, bvec)) match { case Successful(bdata) => sendResponseLeft(bdata.toByteVector) - case f @ Failure(_) => + case f : Failure => log.error(s"$f") } }) } + /** + * Accept a `List` of packets and sequentially re-package the elements from the list into multiple container packets.
+ *
+ * The original packets are encoded then paired with their encoding lengths plus extra space to prefix the length. + * Encodings from these pairs are drawn from the list until into buckets that fit a maximum byte stream length. + * The size limitation on any bucket is the MTU limit. + * less by the base sizes of `MultiPacketEx` (2) and of `SlottedMetaPacket` (4). + * @param bundle the packets to be bundled + */ + def handleBundlePacket(bundle : List[PlanetSidePacket]) : Unit = { + val packets : List[ByteVector] = recursiveEncode(bundle.iterator) + recursiveFillPacketBuckets(packets.iterator, PacketCodingActor.MTU_LIMIT_BYTES - 6) + .foreach( list => { + handleBundlePacket(list.toVector) + }) + } + + /** + * Accept a `Vector` of encoded packets and re-package them. + * The normal order is to package the elements of the vector into a `MultiPacketEx`. + * If the vector only has one element, it will get packaged by itself in a `SlottedMetaPacket`. + * If that one element risks being too big for the MTU, however, it will be handled off to be split. + * Splitting should preserve `Subslot` ordering with the rest of the bundling. + * @param vec a specific number of byte streams + */ + def handleBundlePacket(vec : Vector[ByteVector]) : Unit = { + if(vec.size == 1) { + val elem = vec.head + if(elem.length > PacketCodingActor.MTU_LIMIT_BYTES - 4) { + handleSplitPacket(PacketCoding.CreateControlPacket(HandleGamePacket(elem))) + } + else { + handleBundlePacket(elem) + } + } + else { + PacketCoding.EncodePacket(MultiPacketEx(vec)) match { + case Successful(bdata) => + handleBundlePacket(bdata.toByteVector) + case Failure(e) => + log.warn(s"bundling failed on MultiPacketEx creation: - $e") + } + } + } + + /** + * Accept `ByteVector` data and package it into a `SlottedMetaPacket`. + * Send it (towards the network) upon successful encoding. + * @param data an encoded packet + */ + def handleBundlePacket(data : ByteVector) : Unit = { + PacketCoding.EncodePacket(SlottedMetaPacket(0, Subslot, data)) match { + case Successful(bdata) => + sendResponseLeft(bdata.toByteVector) + case Failure(e) => + log.warn(s"bundling failed on SlottedMetaPacket creation: - $e") + } + } + /** * Encoded sequence of data going towards the network. * @param cont the data @@ -193,6 +253,72 @@ class PacketCodingActor extends Actor with MDCContextAware { leftRef !> RawPacket(cont) } + /** + * Transform data into a container packet and re-submit that container to the process that handles the packet. + * @param data the packet data + * @param description an explanation of the input `data` + */ + def UnmarshalInnerPacket(data : ByteVector, description : String) : Unit = { + PacketCoding.unmarshalPayload(0, data) match { //TODO is it safe for this to always be 0? + case Successful(packet) => + handlePacketContainer(packet) + case Failure(ex) => + log.info(s"Failed to unmarshal $description: $ex") + } + } + + /** + * Sort and redirect a container packet bound for the server by type of contents. + * `GamePacket` objects can just onwards without issue. + * `ControlPacket` objects may need to be dequeued. + * All other container types are invalid. + * @param container the container packet + */ + def handlePacketContainer(container : PlanetSidePacketContainer) : Unit = { + container match { + case _ : GamePacket => + sendResponseRight(container) + case ControlPacket(_, ctrlPkt) => + handleControlPacket(container, ctrlPkt) + case default => + log.warn(s"Invalid packet container class received: ${default.getClass.getName}") //do not spill contents in log + } + } + + /** + * Process a control packet or determine that it does not need to be processed at this level. + * Primarily, if the packet is of a type that contains another packet that needs be be unmarshalled, + * that/those packet must be unwound.
+ *
+ * The subslot information is used to identify these nested packets after arriving at their destination, + * to establish order for sequential packets and relation between divided packets. + * @param container the original container packet + * @param packet the packet that was extracted from the container + */ + def handleControlPacket(container : PlanetSidePacketContainer, packet : PlanetSideControlPacket) = { + packet match { + case SlottedMetaPacket(slot, subslot, innerPacket) => + subslotInbound = subslot + self.tell(PacketCoding.CreateControlPacket(RelatedB(slot, subslot)), rightRef) //will go to the network + UnmarshalInnerPacket(innerPacket, "the inner packet of a SlottedMetaPacket") + + case MultiPacket(packets) => + packets.foreach { UnmarshalInnerPacket(_, "the inner packet of a MultiPacket") } + + case MultiPacketEx(packets) => + packets.foreach { UnmarshalInnerPacket(_, "the inner packet of a MultiPacketEx") } + + case RelatedA(slot, subslot) => + log.error(s"result $slot: subslot $subslot was in error") + + case RelatedB(slot, subslot) => + log.trace(s"result $slot: subslot $subslot accepted") + + case _ => + sendResponseRight(container) + } + } + /** * Decoded packet going towards the simulation. * @param cont the packet @@ -202,6 +328,71 @@ class PacketCodingActor extends Actor with MDCContextAware { MDC("sessionId") = sessionId.toString rightRef !> cont } + + /** + * Accept a series of packets and transform it into a series of packet encodings. + * Packets that do not encode properly are simply excluded for the product. + * This is not treated as an error or exception; a warning will mrely be logged. + * @param iter the `Iterator` for a series of packets + * @param out updated series of byte stream data produced through successful packet encoding; + * defaults to an empty list + * @return a series of byte stream data produced through successful packet encoding + */ + @tailrec private def recursiveEncode(iter : Iterator[PlanetSidePacket], out : List[ByteVector] = List()) : List[ByteVector] = { + if(!iter.hasNext) { + out + } + else { + import net.psforever.packet.{PlanetSideControlPacket, PlanetSideGamePacket} + iter.next match { + case msg : PlanetSideGamePacket => + PacketCoding.EncodePacket(msg) match { + case Successful(bytecode) => + recursiveEncode(iter, out :+ bytecode.toByteVector) + case Failure(e) => + log.warn(s"game packet $msg, part of a bundle, did not encode - $e") + recursiveEncode(iter, out) + } + case msg : PlanetSideControlPacket => + PacketCoding.EncodePacket(msg) match { + case Successful(bytecode) => + recursiveEncode(iter, out :+ bytecode.toByteVector) + case Failure(e) => + log.warn(s"control packet $msg, part of a bundle, did not encode - $e") + recursiveEncode(iter, out) + } + case _ => + recursiveEncode(iter, out) + } + } + } + + /** + * Accept a series of byte stream data and sort into sequential size-limited buckets of the same byte streams. + * Note that elements that exceed `lim` by themselves are always sorted into their own buckets. + * @param iter an `Iterator` of a series of byte stream data + * @param lim the maximum stream length permitted + * @param curr the stream length of the current bucket + * @param out updated series of byte stream data stored in buckets + * @return a series of byte stream data stored in buckets + */ + @tailrec private def recursiveFillPacketBuckets(iter : Iterator[ByteVector], lim : Int, curr : Int = 0, out : List[mutable.ListBuffer[ByteVector]] = List(mutable.ListBuffer())) : List[mutable.ListBuffer[ByteVector]] = { + if(!iter.hasNext) { + out + } + else { + val data = iter.next + var len = data.length.toInt + len = len + (if(len < 256) { 1 } else if(len < 65536) { 2 } else { 4 }) //space for the prefixed length byte(s) + if(curr + len > lim && out.last.nonEmpty) { //bucket must have something in it before swapping + recursiveFillPacketBuckets(iter, lim, len, out :+ mutable.ListBuffer(data)) + } + else { + out.last += data + recursiveFillPacketBuckets(iter, lim, curr + len, out) + } + } + } } object PacketCodingActor { diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index cbd3698d..509a202e 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -140,10 +140,10 @@ class WorldSessionActor extends Actor with MDCContextAware { galaxy = endpoint log.info("ID: " + sessionId + " Got galaxy service " + endpoint) - case ctrl @ ControlPacket(_, _) => - handlePktContainer(ctrl) - case game @ GamePacket(_, _, _) => - handlePktContainer(game) + case ControlPacket(_, ctrl) => + handleControlPkt(ctrl) + case GamePacket(_, _, pkt) => + handleGamePkt(pkt) // temporary hack to keep the client from disconnecting case PokeClient() => sendResponse(KeepAliveMessage()) @@ -1110,61 +1110,12 @@ class WorldSessionActor extends Actor with MDCContextAware { log.warn(s"Invalid packet class received: $default") } - def handlePkt(pkt : PlanetSidePacket) : Unit = pkt match { - case ctrl : PlanetSideControlPacket => - handleControlPkt(ctrl) - case game : PlanetSideGamePacket => - handleGamePkt(game) - case default => log.error(s"Invalid packet class received: $default") - } - - def handlePktContainer(pkt : PlanetSidePacketContainer) : Unit = pkt match { - case ctrl @ ControlPacket(opcode, ctrlPkt) => - handleControlPkt(ctrlPkt) - case game @ GamePacket(opcode, seq, gamePkt) => - handleGamePkt(gamePkt) - case default => log.warn(s"Invalid packet container class received: $default") - } - def handleControlPkt(pkt : PlanetSideControlPacket) = { pkt match { - case SlottedMetaPacket(slot, subslot, innerPacket) => - sendResponse(SlottedMetaAck(slot, subslot)) - - PacketCoding.DecodePacket(innerPacket) match { - case Failure(e) => - log.error(s"Failed to decode inner packet of SlottedMetaPacket: $e") - case Successful(v) => - handlePkt(v) - } - case sync @ ControlSync(diff, unk, f1, f2, f3, f4, fa, fb) => + case sync @ ControlSync(diff, _, _, _, _, _, fa, fb) => log.debug(s"SYNC: $sync") val serverTick = Math.abs(System.nanoTime().toInt) // limit the size to prevent encoding error sendResponse(ControlSyncResp(diff, serverTick, fa, fb, fb, fa)) - case MultiPacket(packets) => - packets.foreach { pkt => - PacketCoding.DecodePacket(pkt) match { - case Failure(e) => - log.error(s"Failed to decode inner packet of MultiPacket: $e") - case Successful(v) => - handlePkt(v) - } - } - case MultiPacketEx(packets) => - packets.foreach { pkt => - PacketCoding.DecodePacket(pkt) match { - case Failure(e) => - log.error(s"Failed to decode inner packet of MultiPacketEx: $e") - case Successful(v) => - handlePkt(v) - } - } - - case RelatedA0(subslot) => - log.error(s"Client not ready for last control packet with subslot $subslot; potential system disarray") - - case RelatedB0(subslot) => - log.trace(s"Good control packet received $subslot") case TeardownConnection(_) => log.info("Good bye") @@ -3190,6 +3141,11 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(cont.asInstanceOf[Any]) } + def sendResponse(cont : MultiPacketBundle) : Unit = { + log.trace("WORLD SEND: " + cont) + sendResponse(cont.asInstanceOf[Any]) + } + def sendResponse(msg : Any) : Unit = { MDC("sessionId") = sessionId.toString rightRef !> msg diff --git a/pslogin/src/test/scala/PacketCodingActorTest.scala b/pslogin/src/test/scala/PacketCodingActorTest.scala index 1e7325d6..5cd58b0b 100644 --- a/pslogin/src/test/scala/PacketCodingActorTest.scala +++ b/pslogin/src/test/scala/PacketCodingActorTest.scala @@ -2,10 +2,11 @@ import akka.actor.{ActorRef, Props} import akka.testkit.TestProbe -import net.psforever.packet.control.ControlSync -import net.psforever.packet.game.objectcreate.ObjectClass -import net.psforever.packet.{ControlPacket, GamePacket, PacketCoding} +import net.psforever.packet.control.{ControlSync, MultiPacketBundle, SlottedMetaPacket} +import net.psforever.packet.{ControlPacket, GamePacket, GamePacketOpcode, PacketCoding} import net.psforever.packet.game._ +import net.psforever.packet.game.objectcreate.ObjectClass +import net.psforever.types._ import scodec.bits._ import scala.concurrent.duration._ @@ -364,6 +365,398 @@ class PacketCodingActorDTest extends ActorTest { } } +class PacketCodingActorETest extends ActorTest { + "PacketCodingActor" should { + "unwind l-originating hexadecimal data into multiple r-facing packets (MultiPacket -> 2 PlayerStateMessageUpstream)" in { + val string_hex = RawPacket(hex"00 03 18 BD E8 04 5C 02 60 E3 F9 19 0E C1 41 27 00 04 02 60 20 0C 58 0B 20 00 00 18 BD E8 04 86 02 62 13 F9 19 0E D8 40 4D 00 04 02 60 20 0C 78 0A 80 00 00") + val string_obj1 = GamePacket(GamePacketOpcode.PlayerStateMessageUpstream, 0, PlayerStateMessageUpstream(PlanetSideGUID(1256),Vector3(3076.7188f,4734.1094f,56.390625f),Some(Vector3(4.0625f,4.59375f,0.0f)),36.5625f,357.1875f,0.0f,866,0,false,false,false,false,178,0)) + val string_obj2 = GamePacket(GamePacketOpcode.PlayerStateMessageUpstream, 0, PlayerStateMessageUpstream(PlanetSideGUID(1256),Vector3(3077.0469f,4734.258f,56.390625f),Some(Vector3(5.5f,1.1875f,0.0f)),36.5625f,357.1875f,0.0f,867,0,false,false,false,false,168,0)) + + val probe1 = TestProbe() + val probe2 = system.actorOf(Props(classOf[ActorTest.MDCTestProbe], probe1), "mdc-probe") + val pca : ActorRef = system.actorOf(Props[PacketCodingActor], "pca") + pca ! HelloFriend(135, List(probe2).iterator) + probe1.receiveOne(100 milli) //consume + + pca ! string_hex + val reply = probe1.receiveN(2, 200 milli) + assert(reply.head == string_obj1) + assert(reply(1) == string_obj2) + probe1.expectNoMsg(100 milli) + } + } +} + +class PacketCodingActorFTest extends ActorTest { + "PacketCodingActor" should { + "unwind l-originating hexadecimal data into an r-facing packet (MultiPacket -> RelatedB + GenericObjectStateMsg)" in { + val string_hex = RawPacket(hex"00 03 04 00 15 02 98 0B 00 09 0C 0A 1D F2 00 10 00 00 00") + val string_obj = GamePacket(GamePacketOpcode.GenericObjectStateMsg, 0, GenericObjectStateMsg(PlanetSideGUID(242), 16)) + + val probe1 = TestProbe() + val probe2 = system.actorOf(Props(classOf[ActorTest.MDCTestProbe], probe1), "mdc-probe") + val pca : ActorRef = system.actorOf(Props[PacketCodingActor], "pca") + pca ! HelloFriend(135, List(probe2).iterator) + probe1.receiveOne(100 milli) //consume + + pca ! string_hex + val reply = probe1.receiveN(1, 200 milli) + assert(reply.head == string_obj) + //the RelatedB message - 00 15 02 98 - is consumed by pca + probe1.expectNoMsg(100 milli) + } + } +} + +class PacketCodingActorGTest extends ActorTest { + "PacketCodingActor" should { + "unwind l-originating hexadecimal data into an r-facing packet (MultiPacketEx -> RelatedA + GenericObjectStateMsg)" in { + val string_hex = RawPacket(hex"00 19 04 00 11 02 98 0B 00 09 0C 0A 1D F2 00 10 00 00 00") + val string_obj = GamePacket(GamePacketOpcode.GenericObjectStateMsg, 0, GenericObjectStateMsg(PlanetSideGUID(242), 16)) + + val probe1 = TestProbe() + val probe2 = system.actorOf(Props(classOf[ActorTest.MDCTestProbe], probe1), "mdc-probe") + val pca : ActorRef = system.actorOf(Props[PacketCodingActor], "pca") + pca ! HelloFriend(135, List(probe2).iterator) + probe1.receiveOne(100 milli) //consume + + pca ! string_hex + val reply = probe1.receiveN(1, 200 milli) + assert(reply.head == string_obj) + //the RelatedA message - 00 11 02 98 - is consumed by pca; should see error log message in console + probe1.expectNoMsg(100 milli) + } + } +} + +class PacketCodingActorHTest extends ActorTest { + "PacketCodingActor" should { + "unwind l-originating hexadecimal data into two r-facing packets (SlottedMetaPacket/MultiPacketEx -> 2 ObjectDeleteMessage)" in { + val string_hex = RawPacket(hex"00 09 0A E1 00 19 04 19 4F 04 40 04 19 51 04 40") + val string_obj1 = GamePacket(GamePacketOpcode.ObjectDeleteMessage, 0, ObjectDeleteMessage(PlanetSideGUID(1103), 2)) + val string_obj2 = GamePacket(GamePacketOpcode.ObjectDeleteMessage, 0, ObjectDeleteMessage(PlanetSideGUID(1105), 2)) + + val probe1 = TestProbe() + val probe2 = system.actorOf(Props(classOf[ActorTest.MDCTestProbe], probe1), "mdc-probe") + val pca : ActorRef = system.actorOf(Props[PacketCodingActor], "pca") + pca ! HelloFriend(135, List(probe2).iterator) + probe1.receiveOne(100 milli) //consume + + pca ! string_hex + val reply = probe1.receiveN(2, 200 milli) + assert(reply.head == string_obj1) + assert(reply(1) == string_obj2) + probe1.expectNoMsg(100 milli) + } + } +} + +class PacketCodingActorITest extends ActorTest { + "PacketCodingActor" should { + "bundle an r-originating packet into an l-facing SlottedMetaPacket byte stream data (SlottedMetaPacket)" in { + import net.psforever.packet.game.objectcreate._ + val obj = DetailedCharacterData( + CharacterAppearanceData( + PlacementData(Vector3.Zero, Vector3.Zero), + BasicCharacterData("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1), + 3, + false, + false, + ExoSuitType.Standard, + "", + 0, + false, + 2.8125f, 210.9375f, + true, + GrenadeState.None, + false, + false, + false, + RibbonBars() + ), + 0, + 0, + 100, 100, + 50, + 1, 7, 7, + 100, 100, + List(CertificationType.StandardAssault,CertificationType.MediumAssault,CertificationType.ATV,CertificationType.Harasser,CertificationType.StandardExoSuit,CertificationType.AgileExoSuit,CertificationType.ReinforcedExoSuit), + List(), + List(), + List.empty, + None, + Some(InventoryData(Nil)), + DrawnSlot.None + ) + val pkt = MultiPacketBundle(List(ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj))) + val string_hex = hex"000900001879060000bc84b000000000000000000002040000097049006c006c006c004900490049006c006c006c0049006c0049006c006c0049006c006c006c0049006c006c0049008452700000000000000000000000000000002000000fe6a703fffffffffffffffffffffffffffffffc00000000000000000000000000000000000000019001900064000001007ec800c80000000000000000000000000000000000000001c00042c54686c7000000000000000000000000000000000000000000000000000000000000000000000000200700" + + val probe1 = TestProbe() + val probe2 = system.actorOf(Props(classOf[ActorTest.MDCTestProbe], probe1), "mdc-probe") + val pca : ActorRef = system.actorOf(Props[PacketCodingActor], "pca") + pca ! HelloFriend(135, List(probe2).iterator) + probe1.receiveOne(100 milli) //consume + + probe2 ! pkt + val reply1 = receiveN(1, 200 milli) //we get a MdcMsg message back + probe1.receiveN(1, 200 milli) //flush contents + probe2 ! reply1.head //by feeding the MdcMsg into the actor, we get normal output on the probe + probe1.receiveOne(100 milli) match { + case RawPacket(data) => + assert(data == string_hex) + PacketCoding.DecodePacket(data).require match { + case _ : SlottedMetaPacket => + assert(true) + case _ => + assert(false) + } + case e => + assert(false) + } + } + } +} + +class PacketCodingActorJTest extends ActorTest { + "PacketCodingActor" should { + "bundle r-originating packets into a number of MTU-acceptable l-facing byte streams (1 packets into 1)" in { + val pkt = MultiPacketBundle( + List(ObjectDeleteMessage(PlanetSideGUID(1103), 2), ObjectDeleteMessage(PlanetSideGUID(1105), 2), ObjectDeleteMessage(PlanetSideGUID(1107), 2)) + ) + val string_hex = hex"00090000001904194f044004195104400419530440" + + val probe1 = TestProbe() + val probe2 = system.actorOf(Props(classOf[ActorTest.MDCTestProbe], probe1), "mdc-probe") + val pca : ActorRef = system.actorOf(Props[PacketCodingActor], "pca") + pca ! HelloFriend(135, List(probe2).iterator) + probe1.receiveOne(100 milli) //consume + + probe2 ! pkt + val reply1 = receiveN(1, 200 milli) //we get a MdcMsg message back + probe1.receiveN(1, 200 milli) //flush contents + probe2 ! reply1.head //by feeding the MdcMsg into the actor, we get normal output on the probe + probe1.receiveOne(100 milli) match { + case RawPacket(data) => + assert(data == string_hex) + case e => + assert(false) + } + } + } +} + +class PacketCodingActorKTest extends ActorTest { + import net.psforever.packet.game.objectcreate._ + val obj = DetailedCharacterData( + CharacterAppearanceData( + PlacementData(Vector3.Zero, Vector3.Zero), + BasicCharacterData("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1), + 3, + false, + false, + ExoSuitType.Standard, + "", + 0, + false, + 2.8125f, 210.9375f, + true, + GrenadeState.None, + false, + false, + false, + RibbonBars() + ), + 0, + 0, + 100, 100, + 50, + 1, 7, 7, + 100, 100, + List(CertificationType.StandardAssault, CertificationType.MediumAssault, CertificationType.ATV, CertificationType.Harasser, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit, CertificationType.ReinforcedExoSuit), + List(), + List("xpe_sanctuary_help", "xpe_th_firemodes", "used_beamer", "map13"), + List.empty, + None, + Some(InventoryData(Nil)), + DrawnSlot.None + ) + val list = List( + ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj), + ObjectDeleteMessage(PlanetSideGUID(1103), 2), + ObjectDeleteMessage(PlanetSideGUID(1105), 2), + ObjectCreateDetailedMessage(0x79, PlanetSideGUID(175), obj), + ObjectCreateDetailedMessage(0x79, PlanetSideGUID(275), obj), + ObjectDeleteMessage(PlanetSideGUID(1107), 2) + ) + + "PacketCodingActor" should { + "bundle r-originating packets into a number of MTU-acceptable l-facing byte streams (6 packets into 2)" in { + val pkt = MultiPacketBundle(list) + + val probe1 = TestProbe() + val probe2 = system.actorOf(Props(classOf[ActorTest.MDCTestProbe], probe1), "mdc-probe") + val pca : ActorRef = system.actorOf(Props[PacketCodingActor], "pca") + pca ! HelloFriend(135, List(probe2).iterator) + probe1.receiveOne(100 milli) //consume + + probe2 ! pkt + val reply1 = receiveN(2, 200 milli) + probe1.receiveN(1, 200 milli) //flush contents + probe2 ! reply1.head //by feeding the MdcMsg into the actor, we get normal output on the probe + val reply3 = probe1.receiveOne(100 milli).asInstanceOf[RawPacket] + + pca ! reply3 //reconstruct original three packets from the first bundle + val reply4 = probe1.receiveN(3, 200 milli) + var i = 0 + reply4.foreach{ + case GamePacket(_, _, packet) => + assert(packet == list(i)) + i += 1 + case _ => + assert(false) + } + } + } +} + +class PacketCodingActorLTest extends ActorTest { + val string_obj = PropertyOverrideMessage( + List( + GamePropertyScope(0, + GamePropertyTarget(GamePropertyTarget.game_properties, List( + "purchase_exempt_vs" -> "", + "purchase_exempt_tr" -> "", + "purchase_exempt_nc" -> "" + ) + )), + GamePropertyScope(17, + GamePropertyTarget(ObjectClass.katana, "allowed" -> "false") + ), + GamePropertyScope(18, + GamePropertyTarget(ObjectClass.katana, "allowed" -> "false") + ), + GamePropertyScope(19, + GamePropertyTarget(ObjectClass.katana, "allowed" -> "false") + ), + GamePropertyScope(20, + GamePropertyTarget(ObjectClass.katana, "allowed" -> "false") + ), + GamePropertyScope(21, + GamePropertyTarget(ObjectClass.katana, "allowed" -> "false") + ), + GamePropertyScope(22, + GamePropertyTarget(ObjectClass.katana, "allowed" -> "false") + ), + GamePropertyScope(29, List( + GamePropertyTarget(ObjectClass.aphelion_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.aphelion_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.aurora, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.battlewagon, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.colossus_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.colossus_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.flail, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.galaxy_gunship, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.lasher, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.liberator, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.lightgunship, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.maelstrom, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.magrider, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.mini_chaingun, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.peregrine_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.peregrine_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.prowler, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.r_shotgun, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.thunderer, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.vanguard, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.vulture, "allowed" -> "false") + )), + GamePropertyScope(30, List( + GamePropertyTarget(ObjectClass.aphelion_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.aphelion_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.aurora, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.battlewagon, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.colossus_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.colossus_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.flail, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.galaxy_gunship, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.lasher, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.liberator, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.lightgunship, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.maelstrom, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.magrider, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.mini_chaingun, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.peregrine_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.peregrine_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.prowler, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.r_shotgun, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.thunderer, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.vanguard, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.vulture, "allowed" -> "false") + )), + GamePropertyScope(31, List( + GamePropertyTarget(ObjectClass.aphelion_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.aphelion_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.aurora, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.battlewagon, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.colossus_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.colossus_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.flail, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.galaxy_gunship, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.lasher, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.liberator, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.lightgunship, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.maelstrom, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.magrider, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.mini_chaingun, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.peregrine_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.peregrine_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.prowler, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.r_shotgun, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.thunderer, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.vanguard, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.vulture, "allowed" -> "false") + )), + GamePropertyScope(32, List( + GamePropertyTarget(ObjectClass.aphelion_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.aphelion_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.aurora, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.battlewagon, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.colossus_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.colossus_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.flail, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.galaxy_gunship, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.lasher, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.liberator, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.lightgunship, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.maelstrom, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.magrider, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.mini_chaingun, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.peregrine_flight, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.peregrine_gunner, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.prowler, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.r_shotgun, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.thunderer, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.vanguard, "allowed" -> "false"), + GamePropertyTarget(ObjectClass.vulture, "allowed" -> "false") + )) + ) + ) + + "PacketCodingActor" should { + "split, rather than bundle, r-originating packets into a number of MTU-acceptable l-facing byte streams" in { + val probe1 = TestProbe() + val probe2 = system.actorOf(Props(classOf[ActorTest.MDCTestProbe], probe1), "mdc-probe") + val pca : ActorRef = system.actorOf(Props[PacketCodingActor], "pca") + pca ! HelloFriend(135, List(probe2).iterator) + probe1.receiveOne(100 milli) //consume + + val msg = MultiPacketBundle(List(string_obj)) + pca ! msg + receiveN(4) + } + } +} + object PacketCodingActorTest { //decoy }