diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 8942a681..09168082 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -3,15 +3,12 @@ package net.psforever.packet import java.nio.charset.Charset -import scodec.Attempt.Successful -import scodec.{Attempt, Codec, DecodeResult, Err} +import scodec.{DecodeResult, Err, Codec, Attempt} import scodec.bits._ import scodec.codecs._ import scodec._ import shapeless._ -import scala.util.Success - /** The base of all packets */ sealed trait PlanetSidePacket extends Serializable { def encode : Attempt[BitVector] @@ -64,7 +61,7 @@ final case class PlanetSidePacketFlags(packetType : PacketType.Value, secured : /** Codec for [[PlanetSidePacketFlags]] */ object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] { implicit val codec : Codec[PlanetSidePacketFlags] = ( - ("packet_type" | PacketType.codec) :: // first 4-bits + ("packet_type" | PacketType.codec) :: // first 4-bits ("unused" | constant(bin"0")) :: ("secured" | bool) :: ("advanced" | constant(bin"1")) :: // we only support "advanced packets" @@ -77,38 +74,35 @@ object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] { object PacketHelpers { /** Used in certain instances where Codec defintions are stubbed out */ def emptyCodec[T](instance : T) = { - def to(pkt: T) = HNil - def from(a: HNil) = instance + def to(pkt : T) = HNil + def from(a : HNil) = instance Codec[HNil].xmap[T](from, to) } - - /** Create a Codec for an enumeration type that can correctly represent its value - * - * @param enum the enumeration type to create a codec for + * @param enum the enumeration type to create a codec for * @param storageCodec the Codec used for actually representing the value * @tparam E The inferred type * @return Generated codec */ def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = { type Struct = Int :: HNil - val struct: Codec[Struct] = storageCodec.hlist + val struct : Codec[Struct] = storageCodec.hlist val primitiveLimit = Math.pow(2, storageCodec.sizeBound.exact.get) // Assure that the enum will always be able to fit in a N-bit int assert(enum.maxId <= primitiveLimit, enum.getClass.getCanonicalName + s": maxId exceeds primitive type (limit of $primitiveLimit, maxId ${enum.maxId})") - def to(pkt: E#Value): Struct = { + def to(pkt : E#Value) : Struct = { pkt.id :: HNil } - def from(struct: Struct): Attempt[E#Value] = struct match { + def from(struct : Struct) : Attempt[E#Value] = struct match { case enumVal :: HNil => // verify that this int can match the enum val first = enum.values.firstKey.id - val last = enum.maxId-1 + val last = enum.maxId - 1 if(enumVal >= first && enumVal <= last) Attempt.successful(enum(enumVal)) @@ -147,13 +141,11 @@ object PacketHelpers { /** Codec for how PlanetSide represents strings on the wire */ def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii) - /** Same as [[encodedString]] but with a bit adjustment * * This comes in handy when a PlanetSide string is decoded on a non-byte boundary. The PlanetSide client * will byte align after decoding the string lenght, but BEFORE the string itself. Scodec doesn't like this * variability and there doesn't appear to be a way to fix this issue. - * * @param adjustment The adjustment amount in bits * @return Generated string decoding codec with adjustment */ @@ -168,15 +160,15 @@ object PacketHelpers { * input string. We use xmap to transform the [[encodedString]] codec as this change is just a division and multiply */ def encodedWideString : Codec[String] = variableSizeBytes(encodedStringSize.xmap( - insize => insize*2, // number of symbols -> number of bytes (decode) - outSize => outSize/2 // number of bytes -> number of symbols (encode) + insize => insize * 2, // number of symbols -> number of bytes (decode) + outSize => outSize / 2 // number of bytes -> number of symbols (encode) ), utf16) /** Same as [[encodedWideString]] but with a bit alignment after the decoded size */ def encodedWideStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment).xmap( - insize => insize*2, - outSize => outSize/2 + insize => insize * 2, + outSize => outSize / 2 ), utf16) // TODO: make the function below work as there are places it should be used @@ -185,7 +177,6 @@ object PacketHelpers { exmap[Int]( (a : Either[Int, Int]) => { val result = a.fold[Int](a => a, a => a) - if(result > limit) Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit")) else @@ -195,7 +186,6 @@ object PacketHelpers { if(a > limit) return Attempt.failure(Err("adsf")) //return Left(Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit"))) - if(a > 0x7f) return Attempt.successful(Left(a)) else @@ -203,48 +193,13 @@ object PacketHelpers { } ) } - def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii) */ - /** - * Codec that encodes/decodes a list of `n` elements, where `n` is known at compile time.
- *
- * This function is copied almost verbatim from its source, with exception of swapping the parameter that is normally a `Nat` `literal`. - * The modified function takes a normal unsigned `Integer` and assures that the parameter is non-negative before further processing. - * It casts to a `Long` and passes onto an overloaded method. - * @param size the known size of the `List` - * @param codec a codec that describes each of the contents of the `List` - * @tparam A the type of the `List` contents - * @see codec\package.scala, sizedList - * @see codec\package.scala, listOfN - * @return a codec that works on a List of A but excludes the size from the encoding - */ - def listOfNSized[A](size : Int, codec : Codec[A]) : Codec[List[A]] = listOfNSized(if(size < 0) 0L else size.asInstanceOf[Long], codec) - - /** - * Codec that encodes/decodes a list of `n` elements, where `n` is known at compile time.
- *
- * This function is copied almost verbatim from its source, with exception of swapping the parameter that is normally a `Nat` `literal`. - * The modified function takes a normal unsigned `Long` and assures that the parameter is non-negative before further processing. - * @param size the known size of the `List` - * @param codec a codec that describes each of the contents of the `List` - * @tparam A the type of the `List` contents - * @see codec\package.scala, sizedList - * @see codec\package.scala, listOfN - * @see codec\package.scala, provide - * @return a codec that works on a List of A but excludes the size from the encoding - */ - def listOfNSized[A](size : Long, codec : Codec[A]) : Codec[List[A]] = listOfNAligned(provide(if(size < 0) 0 else size), 0, codec) - /** * Encode and decode a byte-aligned `List`.
*
- * This function is copied almost verbatim from its source, but swapping the normal `ListCodec` for a new `AlignedListCodec`. - * It also changes the type of the list length `Codec` from `Int` to `Long`. - * Due to type erasure, this method can not be overloaded for both `Codec[Int]` and `Codec[Long]`. - * The compiler would resolve both internally into type `Codec[T]` and their function definitions would be identical. - * For the purposes of use, `longL(n)` will cast to an `Int` for the same acceptable values of `n` as in `uintL(n)`. + * This function is copied almost verbatim from its source, with exception of swapping the normal `ListCodec` for a new `AlignedListCodec`. * @param countCodec the codec that represents the prefixed size of the `List` * @param alignment the number of bits padded between the `List` size and the `List` contents * @param valueCodec a codec that describes each of the contents of the `List` @@ -254,33 +209,34 @@ object PacketHelpers { */ def listOfNAligned[A](countCodec : Codec[Long], alignment : Int, valueCodec : Codec[A]) : Codec[List[A]] = { countCodec. - flatZip { - count => - new AlignedListCodec(countCodec, valueCodec, alignment, Some(count)) - }. - narrow[List[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) - } - ). + flatZip { count => new AlignedListCodec(countCodec, valueCodec, alignment, Some(count)) }. + narrow[List[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"listOfN($countCodec, $valueCodec)") } + + /** + * Codec that encodes/decodes a list of `n` elements, where `n` is known at compile time.
+ *
+ * This function is copied almost verbatim from its source, with exception of swapping the parameter that is normally a `Nat` `literal`. + * The modified function takes a normal unsigned `Integer` and assures that the parameter is non-negative before further processing. + * @param size the known size of the `List` + * @param codec a codec that describes each of the contents of the `List` + * @tparam A the type of the `List` contents + * @see codec\package.scala, sizedList + * @see codec\package.scala, listOfN + * @see codec\package.scala, provides + * @return a codec that works on a List of A but excludes the size from the encoding + */ + def listOfNSized[A](size : Long, codec : Codec[A]) : Codec[List[A]] = PacketHelpers.listOfNAligned(provide(if(size < 0) 0 else size), 0, codec) } /** - * The greater `Codec` class that encodes and decodes a byte-aligned `List`.
+ * The codec that encodes and decodes a byte-aligned `List`.
*
- * This class is copied almost verbatim from its source, with two major modifications. - * First, heavy modifications to its `encode` process account for the alignment value. - * Second, the length field is parsed as a `Codec[Long]` value and type conversion is accounted for at several points. + * This class is copied almost verbatim from its source, with only heavy modifications to its `encode` process. * @param countCodec the codec that represents the prefixed size of the `List` * @param valueCodec a codec that describes each of the contents of the `List` * @param alignment the number of bits padded between the `List` size and the `List` contents (on successful) @@ -294,18 +250,19 @@ private class AlignedListCodec[A](countCodec : Codec[Long], valueCodec: Codec[A] *
* Bit padding after the encoded size of the `List` is only added if the `alignment` value is greater than zero and the initial encoding process was successful. * The padding is rather heavy-handed and a completely different `BitVector` is returned if successful. + * Performance hits for this complexity are not expected to be significant. * @param list the `List` to be encoded * @return the `BitVector` encoding, if successful */ override def encode(list : List[A]) : Attempt[BitVector] = { - var solve : Attempt[BitVector] = Encoder.encodeSeq(valueCodec)(list) + val solve : Attempt[BitVector] = Encoder.encodeSeq(valueCodec)(list) if(alignment > 0) { solve match { case Attempt.Successful(vector) => val countCodecSize : Long = countCodec.sizeBound.lowerBound - solve = Attempt.successful(vector.take(countCodecSize) ++ BitVector.fill(alignment)(false) ++ vector.drop(countCodecSize)) + return Attempt.successful(vector.take(countCodecSize) ++ BitVector.fill(alignment)(false) ++ vector.drop(countCodecSize)) case _ => - solve = Attempt.failure(Err("failed to create a list")) + return Attempt.failure(Err("failed to create a list")) } } solve @@ -316,26 +273,27 @@ private class AlignedListCodec[A](countCodec : Codec[Long], valueCodec: Codec[A] * @param buffer the encoded bits in the `List`, preceded by the alignment bits * @return the decoded `List` */ - override def decode(buffer: BitVector) = { + def decode(buffer: BitVector) = { val lim = Option( if(limit.isDefined) limit.get.asInstanceOf[Int] else 0 ) //TODO potentially unsafe size conversion Decoder.decodeCollect[List, A](valueCodec, lim)(buffer.drop(alignment)) } /** - * The size of the encoded `List`. + * The size of the encoded `List`.
+ *
+ * Unchanged from original. * @return the size as calculated by the size of each element for each element */ - override def sizeBound = limit match { - case None => - SizeBound.unknown - case Some(lim : Long) => - valueCodec.sizeBound * lim + def sizeBound = limit match { + case None => SizeBound.unknown + case Some(lim) => valueCodec.sizeBound * lim } /** - * Get a `String` representation of this `List`. + * Get a `String` representation of this `List`.
+ *
* Unchanged from original. * @return the `String` representation */ override def toString = s"list($valueCodec)" -} +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala index 2fcb5c23..9db2022d 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -1,57 +1,239 @@ +// Copyright (c) 2016 PSForever.net to present package net.psforever.packet.game +import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass, StreamBitSize} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import scodec.bits._ -import scodec.{Attempt, Codec, Err} +import scodec.bits.BitVector +import scodec.{Attempt, Codec, DecodeResult, Err} import scodec.codecs._ -import shapeless._ +import shapeless.{::, HNil} -case class ObjectCreateMessageParent(guid : Int, slot : Int) +/** + * The parent information of a created object.
+ *
+ * Rather than a created-parent with a created-child relationship, the whole of the packet still only creates the child. + * The parent is a pre-existing object into which the (created) child is attached. + * The slot is encoded as a string length integer, following PlanetSide Classic convention for slot numbering. + * It is either a 0-127 eight bit number, or a 128-32767 sixteen bit number. + * @param guid the GUID of the parent object + * @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent + */ +final case class ObjectCreateMessageParent(guid : PlanetSideGUID, + slot : Int) -case class ObjectCreateMessage(streamLength : Long, // in bits - objectClass : Int, - guid : Int, - parentInfo : Option[ObjectCreateMessageParent], - stream : BitVector - ) +/** + * Communicate with the client that a certain object with certain properties is to be created. + * The object may also have primitive assignment (attachment) properties.
+ *
+ * In normal packet data order, the parent object is specified before the actual object is specified. + * This is most likely a method of early correction. + * "Does this parent object exist?" + * "Is this new object something that can be attached to this parent?" + * "Does the parent have the appropriate attachment slot?" + * There is no fail-safe method for any of these circumstances being false, however, and the object will simply not be created. + * In instance where the parent data does not exist, the object-specific data is immediately encountered.
+ *
+ * The object's GUID is assigned by the server. + * The clients are required to adhere to this new GUID referring to the object. + * There is no fail-safe for a conflict between what the server thinks is a new GUID and what any client thinks is an already-assigned GUID. + * Likewise, there is no fail-safe between a client failing or refusing to create an object and the server thinking an object has been created. + * (The GM-level command `/sync` tests for objects that "do not match" between the server and the client. + * It's implementation and scope are undefined.)
+ *
+ * Knowing the object's class is essential for parsing the specific information passed by the `data` parameter. + * @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding + * @param objectClass the code for the type of object being constructed + * @param guid the GUID this object will be assigned + * @param parentInfo if defined, the relationship between this object and another object (its parent) + * @param data the data used to construct this type of object; + * on decoding, set to `None` if the process failed + * @see ObjectClass.selectDataCodec + */ +final case class ObjectCreateMessage(streamLength : Long, + objectClass : Int, + guid : PlanetSideGUID, + parentInfo : Option[ObjectCreateMessageParent], + data : Option[ConstructorData]) extends PlanetSideGamePacket { - def opcode = GamePacketOpcode.ObjectCreateMessage def encode = ObjectCreateMessage.encode(this) } object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { + /** + * An abbreviated constructor for creating `ObjectCreateMessages`, ignoring the optional aspect of some fields. + * @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding + * @param objectClass the code for the type of object being constructed + * @param guid the GUID this object will be assigned + * @param parentInfo the relationship between this object and another object (its parent) + * @param data the data used to construct this type of object + * @return an ObjectCreateMessage + */ + def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage = + ObjectCreateMessage(streamLength, objectClass, guid, Some(parentInfo), Some(data)) - type Pattern = Int :: Int :: Option[ObjectCreateMessageParent] :: HNil - type ChoicePattern = Either[Pattern, Pattern] + /** + * An abbreviated constructor for creating `ObjectCreateMessages`, ignoring `parentInfo`. + * @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding + * @param objectClass the code for the type of object being constructed + * @param guid the GUID this object will be assigned + * @param data the data used to construct this type of object + * @return an ObjectCreateMessage + */ + def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateMessage = + ObjectCreateMessage(streamLength, objectClass, guid, None, Some(data)) - val noParent : Codec[Pattern] = (("object_class" | uintL(0xb)) :: - ("guid" | uint16L)).xmap[Pattern]( { - case cls :: guid :: HNil => cls :: guid :: None :: HNil - }, { - case cls :: guid :: None :: HNil => cls :: guid :: HNil - }) - val parent : Codec[Pattern] = (("parent_guid" | uint16L) :: - ("object_class" | uintL(0xb)) :: - ("guid" | uint16L) :: - ("parent_slot_index" | PacketHelpers.encodedStringSize)).xmap[Pattern]( { - case pguid :: cls :: guid :: slot :: HNil => - cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil - }, { - case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil => - pguid :: cls :: guid :: slot :: HNil - }) + type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil + type outPattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil + /** + * Codec for formatting around the lack of parent data in the stream. + */ + private val noParent : Codec[Pattern] = ( + ("objectClass" | uintL(0xb)) :: //11u + ("guid" | PlanetSideGUID.codec) //16u + ).xmap[Pattern]( + { + case cls :: guid :: HNil => + cls :: guid :: None :: HNil + }, { + case cls :: guid :: None :: HNil => + cls :: guid :: HNil + } + ) + /** + * Codec for reading and formatting parent data from the stream. + */ + private val parent : Codec[Pattern] = ( + ("parentGuid" | PlanetSideGUID.codec) :: //16u + ("objectClass" | uintL(0xb)) :: //11u + ("guid" | PlanetSideGUID.codec) :: //16u + ("parentSlotIndex" | PacketHelpers.encodedStringSize) //8u or 16u + ).xmap[Pattern]( + { + case pguid :: cls :: guid :: slot :: HNil => + cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil + }, { + case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil => + pguid :: cls :: guid :: slot :: HNil + } + ) + + /** + * Take bit data and transform it into an object that expresses the important information of a game piece. + * This function is fail-safe because it catches errors involving bad parsing of the bitstream data. + * Generally, the `Exception` messages themselves are not useful here. + * The important parts are what the packet thought the object class should be and what it actually processed. + * @param objectClass the code for the type of object being constructed + * @param data the bitstream data + * @return the optional constructed object + */ + private def decodeData(objectClass : Int, data : BitVector) : Option[ConstructorData] = { + var out : Option[ConstructorData] = None + try { + val outOpt : Option[DecodeResult[_]] = ObjectClass.selectDataCodec(objectClass).decode(data).toOption + if(outOpt.isDefined) + out = outOpt.get.value.asInstanceOf[ConstructorData.genericPattern] + } + catch { + case ex : Exception => + //catch and release, any sort of parse error + } + out + } + + /** + * Take the important information of a game piece and transform it into bit data. + * This function is fail-safe because it catches errors involving bad parsing of the object data. + * Generally, the `Exception` messages themselves are not useful here. + * @param objClass the code for the type of object being deconstructed + * @param obj the object data + * @return the bitstream data + */ + private def encodeData(objClass : Int, obj : ConstructorData) : BitVector = { + var out = BitVector.empty + try { + val outOpt : Option[BitVector] = ObjectClass.selectDataCodec(objClass).encode(Some(obj.asInstanceOf[ConstructorData])).toOption + if(outOpt.isDefined) + out = outOpt.get + } + catch { + case ex : Exception => + //catch and release, any sort of parse error + } + out + } + + /** + * Calculate the stream length in number of bits by factoring in the whole message in two portions. + * This process automates for: object encoding.
+ *
+ * Ignoring the parent data, constant field lengths have already been factored into the results. + * That includes: + * the length of the stream length field (32u), + * the object's class (11u), + * the object's GUID (16u), + * and the bit to determine if there will be parent data. + * In total, these fields form a known fixed length of 60u. + * @param parentInfo if defined, the relationship between this object and another object (its parent); + * information about the parent adds either 24u or 32u + * @param data if defined, the data used to construct this type of object; + * the data length is indeterminate until it is walked-through; + * note: the type is `StreamBitSize` as opposed to `ConstructorData` + * @return the total length of the resulting data stream in bits + */ + private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : StreamBitSize) : Long = { + //knowable length + val base : Long = if(parentInfo.isDefined) { + if(parentInfo.get.slot > 127) 92L else 84L //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u)) + } + else { + 60L + } + base + data.bitsize + } implicit val codec : Codec[ObjectCreateMessage] = ( - ("stream_length" | uint32L) :: (either(bool, parent, noParent).exmap[Pattern]( { - case Left(a :: b :: Some(c) :: HNil) => Attempt.successful(a :: b :: Some(c) :: HNil) - case Right(a :: b :: None :: HNil) => Attempt.successful(a :: b :: None :: HNil) - // failure cases - case Left(a :: b :: None :: HNil) => Attempt.failure(Err("expected parent structure")) - case Right(a :: b :: Some(c) :: HNil) => Attempt.failure(Err("got unexpected parent structure")) - }, { - case a :: b :: Some(c) :: HNil => Attempt.successful(Left(a :: b :: Some(c) :: HNil)) - case a :: b :: None :: HNil => Attempt.successful(Right(a :: b :: None :: HNil)) - }) :+ ("rest" | bits) ) - ).as[ObjectCreateMessage] + ("streamLength" | uint32L) :: + (either(bool, parent, noParent).exmap[Pattern] ( + { + case Left(a :: b :: Some(c) :: HNil) => + Attempt.successful(a :: b :: Some(c) :: HNil) //true, _, _, Some(c) + case Right(a :: b :: None :: HNil) => + Attempt.successful(a :: b :: None :: HNil) //false, _, _, None + // failure cases + case Left(a :: b :: None :: HNil) => + Attempt.failure(Err("missing parent structure")) //true, _, _, None + case Right(a :: b :: Some(c) :: HNil) => + Attempt.failure(Err("unexpected parent structure")) //false, _, _, Some(c) + }, { + case a :: b :: Some(c) :: HNil => + Attempt.successful(Left(a :: b :: Some(c) :: HNil)) + case a :: b :: None :: HNil => + Attempt.successful(Right(a :: b :: None :: HNil)) + } + ) :+ + ("data" | bits)) //greed is good + ).exmap[outPattern] ( + { + case _ :: _ :: _ :: _ :: BitVector.empty :: HNil => + Attempt.failure(Err("no data to decode")) + case len :: cls :: guid :: par :: data :: HNil => + Attempt.successful(len :: cls :: guid :: par :: decodeData(cls, data) :: HNil) + }, + { + case _ :: _ :: _ :: _ :: None :: HNil => + Attempt.failure(Err("no object to encode")) + case _ :: cls :: guid :: par :: Some(obj) :: HNil => + Attempt.successful(streamLen(par, obj) :: cls :: guid :: par :: encodeData(cls, obj) :: HNil) + } + ).xmap[ObjectCreateMessage] ( + { + case len :: cls :: guid :: par :: obj :: HNil => + ObjectCreateMessage(len, cls, guid, par, obj) + }, + { + case ObjectCreateMessage(len, cls, guid, par, obj) => + len :: cls :: guid :: par :: obj :: HNil + } + ).as[ObjectCreateMessage] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala new file mode 100644 index 00000000..2d33df7a --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala @@ -0,0 +1,75 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import net.psforever.packet.game.PlanetSideGUID +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * A representation of the ammunition portion of `ObjectCreateMessage` packet data. + * This data will help construct a "box" of that type of ammunition when standalone. + * It can also be constructed directly inside a weapon as its magazine.
+ *
+ * The maximum amount of ammunition that can be stored in a single box is 65535 units. + * Regardless of the interface, however, the number will never be fully visible. + * Only the first three digits or the first four digits may be represented. + * @param magazine the number of rounds available + * @see WeaponData + */ +final case class AmmoBoxData(magazine : Int) extends ConstructorData { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = 40L +} + +object AmmoBoxData extends Marshallable[AmmoBoxData] { + /** + * An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot`. + * @param cls the code for the type of object being constructed + * @param guid the GUID this object will be assigned + * @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent + * @param ammo the `AmmoBoxData` + * @return an `InternalSlot` object that encapsulates `AmmoBoxData` + */ + def apply(cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : InternalSlot = + new InternalSlot(cls, guid, parentSlot, ammo) + + implicit val codec : Codec[AmmoBoxData] = ( + uint8L :: + uint(15) :: + ("magazine" | uint16L) :: + bool + ).exmap[AmmoBoxData] ( + { + case 0xC8 :: 0 :: mag :: false :: HNil => + Attempt.successful(AmmoBoxData(mag)) + case a :: b :: _ :: d :: HNil => + Attempt.failure(Err("invalid ammunition data format")) + }, + { + case AmmoBoxData(mag) => + Attempt.successful(0xC8 :: 0 :: mag :: false:: HNil) + } + ) + + /** + * Transform between AmmoBoxData and ConstructorData. + */ + val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( + { + case x => + Attempt.successful(Some(x.asInstanceOf[ConstructorData])) + }, + { + case Some(x) => + Attempt.successful(x.asInstanceOf[AmmoBoxData]) + case _ => + Attempt.failure(Err("can not encode ammo box data")) + } + ) +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala new file mode 100644 index 00000000..b614fc70 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala @@ -0,0 +1,392 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.{Marshallable, PacketHelpers} +import net.psforever.types.Vector3 +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.
+ *
+ * This partition of the data stream contains information used to represent how the player's avatar is presented. + * This appearance can be considered the avatar's obvious points beyond experience levels. + * It does not include passive exo-suit upgrades, battle rank 24 cosmetics, special postures, or current equipment. + * Those will occur later back in the main data stream.
+ *
+ * This base length of this stream is __430__ known bits, excluding the length of the name and the padding on that name. + * Of that, __203__ bits are perfectly unknown in significance. + *
+ * Faction:
+ * `0 - Terran Republic`
+ * `1 - New Conglomerate`
+ * `2 - Vanu Sovereignty`
+ *
+ * Exo-suit:
+ * `0 - Agile`
+ * `1 - Refinforced`
+ * `2 - Mechanized Assault`
+ * `3 - Infiltration`
+ * `4 - Standard`
+ *
+ * Sex:
+ * `0 - invalid`
+ * `1 - Male`
+ * `2 - Female`
+ * `3 - invalid`
+ *
+ * Voice:
+ * `    MALE      FEMALE`
+ * `0 - no voice  no voice`
+ * `1 - male_1    female_1`
+ * `2 - male_2    female_2`
+ * `3 - male_3    female_3`
+ * `4 - male_4    female_4`
+ * `5 - male_5    female_5`
+ * `6 - female_1  no voice`
+ * `7 - female_2  no voice` + * @param pos the position of the character in the world environment (in three coordinates) + * @param objYaw the angle with respect to the horizon towards which the object's front is facing; + * every `0x1` is 2.813 degrees counter clockwise from North; + * every `0x10` is 45-degrees; + * it wraps at `0x0` == `0x80` == North + * (note: references the avatar as a game object?) + * @param faction the empire to which the avatar belongs; + * the value scale is different from `PlanetSideEmpire` + * @param bops whether or not this avatar is enrolled in Black OPs + * @param unk1 na; + * defaults to 4 + * @param name the wide character name of the avatar, minimum of two characters + * @param exosuit the type of exosuit the avatar will be depicted in; + * for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits + * @param sex whether the avatar is male or female + * @param face1 the avatar's face, as by column number on the character creation screen + * @param face2 the avatar's face, as by row number on the character creation screen + * @param voice the avatar's voice selection + * @param unk2 na + * @param unk3 na; + * can be missing from the stream under certain conditions; + * see next + * @param unk4 na; + * can be missing from the stream under certain conditions; + * see previous + * @param unk5 na; + * defaults to `0x8080` + * @param unk6 na; + * defaults to `0xFFFF`; + * may be `0x0` + * @param unk7 na; + * defaults to 2 + * @param viewPitch the angle with respect to the sky and the ground towards which the avatar is looking; + * only supports downwards view angles; + * `0x0` is forwards-facing; + * `0x20` to `0xFF` is downwards-facing + * @param viewYaw the angle with respect to the horizon towards which the avatar is looking; + * every `0x1` is 2.813 degrees counter clockwise from North; + * every `0x10` is 45-degrees; + * it wraps at `0x0` == `0x80` == North + * @param unk8 na + * @param ribbons the four merit commendation ribbon medals + */ +final case class CharacterAppearanceData(pos : Vector3, + objYaw : Int, + faction : Int, + bops : Boolean, + unk1 : Int, + name : String, + exosuit : Int, + sex : Int, + face1 : Int, + face2 : Int, + voice : Int, + unk2 : Int, + unk3 : Int, + unk4 : Int, + unk5 : Int, + unk6 : Int, + unk7 : Int, + viewPitch : Int, + viewYaw : Int, + unk8 : Int, + ribbons : RibbonBars) extends StreamBitSize { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = { + //TODO ongoing analysis, this value will be subject to change + 430L + CharacterData.stringBitSize(name, 16) + CharacterAppearanceData.namePadding + } +} + +object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { + /** + * Get the padding of the avatar's name. + * The padding will always be a number 0-7. + * @return the pad length in bits + */ + private def namePadding : Int = { + //TODO the parameters for this function are not correct + //TODO the proper padding length should reflect all variability in the substream prior to this point + 4 + } + + implicit val codec : Codec[CharacterAppearanceData] = ( + ("pos" | Vector3.codec_pos) :: + ignore(16) :: + ("objYaw" | uint8L) :: + ignore(1) :: + ("faction" | uint2L) :: + ("bops" | bool) :: + ("unk1" | uint4L) :: + ignore(16) :: + ("name" | PacketHelpers.encodedWideStringAligned( namePadding )) :: + ("exosuit" | uintL(3)) :: + ignore(2) :: + ("sex" | uint2L) :: + ("face1" | uint4L) :: + ("face2" | uint4L) :: + ("voice" | uintL(3)) :: + ("unk2" | uint2L) :: + ignore(4) :: + ("unk3" | uint8L) :: + ("unk4" | uint8L) :: + ("unk5" | uint16L) :: + ignore(42) :: + ("unk6" | uint16L) :: + ignore(30) :: + ("unk7" | uint4L) :: + ignore(24) :: + ("viewPitch" | uint8L) :: + ("viewYaw" | uint8L) :: + ("unk8" | uint4L) :: + ignore(6) :: + ("ribbons" | RibbonBars.codec) + ).as[CharacterAppearanceData] +} + +/** + * A representation of the avatar portion of `ObjectCreateMessage` packet data.
+ *
+ * This object is huge, representing the quantity of densely-encoded data in its packet. + * Certain bits, when set or unset, introduce or remove other bits from the packet data as well. + * (As in: flipping a bit may create room or negate other bits from somewhere else in the data stream. + * Not accounting for this new pattern of bits will break decoding and encoding.) + * Due to the very real concern that bloating the constructor for this object with parameters could break the `apply` method, + * parameters will often be composed of nested case objects that contain a group of formal parameters. + * There are lists of byte-aligned `Strings` later-on in the packet data that will need access to these objects to calculate padding length.
+ *
+ * The first subdivision of parameters concerns the avatar's basic aesthetics, mostly. + * (No other parts of the data divided up yet.) + * The final sections include two lists of accredited activity performed/completed by the player. + * The remainder of the data, following after that, can be read straight, up to and through the inventory.
+ *
+ * The base length of the stream is currently __1138__ bits, excluding `List`s and `String`s and inventory. + * Of that, __831__ bits are perfectly unknown. + * @param appearance data about the avatar's basic aesthetics + * @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value; + * range is 0-65535 + * @param health for `x / y` of hitpoints, this is the avatar's `x` value; + * range is 0-65535 + * @param armor for `x / y` of armor points, this is the avatar's `x` value; + * range is 0-65535; + * the avatar's `y` armor points is tied to their exo-suit type + * @param unk1 na; + * defaults to 1 + * @param unk2 na; + * defaults to 7 + * @param unk3 na; + * defaults to 7 + * @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value; + * range is 0-65535 + * @param stamina for `x / y` of stamina points, this is the avatar's `x` value; + * range is 0-65535 + * @param unk4 na; + * defaults to 28 + * @param unk5 na; + * defaults to 4 + * @param unk6 na; + * defaults to 44 + * @param unk7 na; + * defaults to 84 + * @param unk8 na; + * defaults to 104 + * @param unk9 na; + * defaults to 1900 + * @param firstTimeEvents the list of first time events performed by this avatar; + * the size field is a 32-bit number; + * the first entry may be padded + * @param tutorials the list of tutorials completed by this avatar; + * the size field is a 32-bit number; + * the first entry may be padded + * @param inventory the avatar's inventory + */ +final case class CharacterData(appearance : CharacterAppearanceData, + healthMax : Int, + health : Int, + armor : Int, + unk1 : Int, //1 + unk2 : Int, //7 + unk3 : Int, //7 + staminaMax : Int, + stamina : Int, + unk4 : Int, //28 + unk5 : Int, //4 + unk6 : Int, //44 + unk7 : Int, //84 + unk8 : Int, //104 + unk9 : Int, //1900 + firstTimeEvents : List[String], + tutorials : List[String], + inventory : InventoryData + ) extends ConstructorData { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = { + //TODO ongoing analysis, this value will be subject to change + //fte list + val fteLen = firstTimeEvents.size + var eventListSize : Long = 32L + CharacterData.ftePadding(fteLen) + for(str <- firstTimeEvents) { + eventListSize += CharacterData.stringBitSize(str) + } + //tutorial list + val tutLen = tutorials.size + var tutorialListSize : Long = 32L + CharacterData.tutPadding(fteLen, tutLen) + for(str <- tutorials) { + tutorialListSize += CharacterData.stringBitSize(str) + } + 708L + appearance.bitsize + eventListSize + tutorialListSize + inventory.bitsize + } +} + +object CharacterData extends Marshallable[CharacterData] { + /** + * Calculate the size of a string, including the length of the "string length" field that precedes it. + * Do not pass null-terminated strings. + * @param str a length-prefixed string + * @param width the width of the character encoding; + * defaults to the standard 8-bits + * @return the size in bits + */ + def stringBitSize(str : String, width : Int = 8) : Long = { + val strlen = str.length + val lenSize = if(strlen > 127) 16L else 8L + lenSize + (strlen * width) + } + + /** + * Get the padding of the first entry in the first time events list. + * The padding will always be a number 0-7. + * @param len the length of the list + * @return the pad length in bits + */ + private def ftePadding(len : Long) : Int = { + //TODO the parameters for this function are not correct + //TODO the proper padding length should reflect all variability in the stream prior to this point + if(len > 0) { + 5 + } + else + 0 + } + + /** + * Get the padding of the first entry in the completed tutorials list. + * The padding will always be a number 0-7.
+ *
+ * The tutorials list follows the first time event list and that contains byte-aligned strings too. + * While there will be more to the padding, this other list is important. + * Any elements in that list causes the automatic byte-alignment of this list's first entry. + * @param len the length of the list + * @return the pad length in bits + */ + private def tutPadding(len : Long, len2 : Long) : Int = { + if(len > 0) //automatic alignment from previous List + 0 + else if(len2 > 0) //need to align for elements + 5 + else //both lists are empty + 0 + } + + implicit val codec : Codec[CharacterData] = ( + ("appearance" | CharacterAppearanceData.codec) :: + ignore(160) :: + ("healthMax" | uint16L) :: + ("health" | uint16L) :: + ignore(1) :: + ("armor" | uint16L) :: + ignore(9) :: + ("unk1" | uint8L) :: + ignore(8) :: + ("unk2" | uint4L) :: + ("unk3" | uintL(3)) :: + ("staminaMax" | uint16L) :: + ("stamina" | uint16L) :: + ignore(149) :: + ("unk4" | uint16L) :: + ("unk5" | uint8L) :: + ("unk6" | uint8L) :: + ("unk7" | uint8L) :: + ("unk8" | uint8L) :: + ("unk9" | uintL(12)) :: + ignore(19) :: + (("firstTimeEvent_length" | uint32L) >>:~ { len => + conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) :: + ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: + (("tutorial_length" | uint32L) >>:~ { len2 => + conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutPadding(len, len2) )) :: + ("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) :: + ignore(207) :: + ("inventory" | InventoryData.codec) + }) + }) + ).xmap[CharacterData] ( + { + case app :: _ :: b :: c :: _ :: d :: _ :: e :: _ :: f :: g :: h :: i :: _ :: j :: k :: l :: m :: n :: o :: _ :: p :: q :: r :: s :: t :: u :: _ :: v :: HNil => + //prepend the displaced first elements to their lists + val fteList : List[String] = if(q.isDefined) { q.get :: r } else r + val tutList : List[String] = if(t.isDefined) { t.get :: u } else u + CharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, v) + }, + { + case CharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, p) => + //shift the first elements off their lists + var fteListCopy = fteList + var firstEvent : Option[String] = None + if(fteList.nonEmpty) { + firstEvent = Some(fteList.head) + fteListCopy = fteList.drop(1) + } + var tutListCopy = tutList + var firstTutorial : Option[String] = None + if(tutList.nonEmpty) { + firstTutorial = Some(tutList.head) + tutListCopy = tutList.drop(1) + } + app :: () :: b :: c :: () :: d :: () :: e :: () :: f :: g :: h :: i :: () :: j :: k :: l :: m :: n :: o :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: p :: HNil + } + ).as[CharacterData] + + /** + * Transform between CharacterData and ConstructorData. + */ + val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( + { + case x => + Attempt.successful(Some(x.asInstanceOf[ConstructorData])) + }, + { + case Some(x) => + Attempt.successful(x.asInstanceOf[CharacterData]) + case _ => + Attempt.failure(Err("can not encode character data")) + } + ) +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConcurrentFeedWeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConcurrentFeedWeaponData.scala new file mode 100644 index 00000000..ae151e0f --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConcurrentFeedWeaponData.scala @@ -0,0 +1,106 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.{Marshallable, PacketHelpers} +import scodec.codecs._ +import scodec.{Attempt, Codec, Err} +import shapeless.{::, HNil} + +/** + * A representation of a class of weapons that can be created using `ObjectCreateMessage` packet data. + * A "concurrent feed weapon" refers to a weapon system that can chamber multiple types of ammunition simultaneously. + * This data will help construct a "weapon" such as a Punisher.
+ *
+ * The data for the weapons nests information for the default (current) type and number of ammunition in its magazine. + * This ammunition data essentially is the weapon's magazines as numbered slots. + * @param unk na + * @param ammo `List` data regarding the currently loaded ammunition types and quantities + * @see WeaponData + * @see AmmoBoxData + */ +final case class ConcurrentFeedWeaponData(unk : Int, + ammo : List[InternalSlot]) extends ConstructorData { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @see InternalSlot.bitsize + * @see AmmoBoxData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = { + var bitsize : Long = 0L + for(o <- ammo) { + bitsize += o.bitsize + } + 61L + bitsize + } +} + +object ConcurrentFeedWeaponData extends Marshallable[ConcurrentFeedWeaponData] { + /** + * An abbreviated constructor for creating `ConcurrentFeedWeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.
+ *
+ * Exploration:
+ * This class may need to be rewritten later to support objects spawned in the world environment. + * @param unk na + * @param cls the code for the type of object (ammunition) being constructed + * @param guid the globally unique id assigned to the ammunition + * @param parentSlot the slot where the ammunition is to be installed in the weapon + * @param ammo the constructor data for the ammunition + * @return a WeaponData object + */ + def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : ConcurrentFeedWeaponData = + new ConcurrentFeedWeaponData(unk, InternalSlot(cls, guid, parentSlot, ammo) :: Nil) + + implicit val codec : Codec[ConcurrentFeedWeaponData] = ( + ("unk" | uint4L) :: + uint4L :: + uint24 :: + uint16 :: + uint2L :: + (uint8L >>:~ { size => + uint2L :: + ("ammo" | PacketHelpers.listOfNSized(size, InternalSlot.codec)) :: + bool + }) + ).exmap[ConcurrentFeedWeaponData] ( + { + case code :: 8 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil => + if(size != ammo.size) + Attempt.failure(Err("weapon encodes wrong number of ammunition")) + else if(size == 0) + Attempt.failure(Err("weapon needs to encode at least one type of ammunition")) + else + Attempt.successful(ConcurrentFeedWeaponData(code, ammo)) + case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => + Attempt.failure(Err("invalid weapon data format")) + }, + { + case ConcurrentFeedWeaponData(code, ammo) => + val size = ammo.size + if(size == 0) + Attempt.failure(Err("weapon needs to encode at least one type of ammunition")) + else if(size >= 255) + Attempt.failure(Err("weapon has too much ammunition (255+ types!)")) + else + Attempt.successful(code :: 8 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil) + } + ).as[ConcurrentFeedWeaponData] + + /** + * Transform between WeaponData and ConstructorData. + */ + val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( + { + case x => + Attempt.successful(Some(x.asInstanceOf[ConstructorData])) + }, + { + case Some(x) => + Attempt.successful(x.asInstanceOf[ConcurrentFeedWeaponData]) + case _ => + Attempt.failure(Err("can not encode weapon data")) + } + ) +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala new file mode 100644 index 00000000..dab33755 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +/** + * The base type for the representation of any data used to produce objects from `ObjectCreateMessage` packet data. + * There is no reason to instantiate this class as-is. + * Children of this class are expected to be able to translate through `scodec` operations into packet data.
+ *
+ * The object data is uncoupled from the object class as multiple classes use the same format for their data. + * For example, both the Suppressor and the Gauss will use a "weapon data" format. + * For example, both 9mm bullets and energy cells will use an "ammunition data" format. + */ +abstract class ConstructorData extends StreamBitSize + +object ConstructorData { + /** + * This pattern is intended to provide common conversion between all of the `Codec`s of the children of this class. + * The casting will be performed through use of `exmap` in the child class. + */ + type genericPattern = Option[ConstructorData] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala new file mode 100644 index 00000000..4fef95ef --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala @@ -0,0 +1,55 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.{Marshallable, PacketHelpers} +import net.psforever.packet.game.PlanetSideGUID +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * An intermediate class for the primary fields of `ObjectCreateMessage` with an implicit parent-child relationship.
+ *
+ * Any object that is contained in a "slot" of another object will use `InternalSlot` to hold the anchoring data. + * This prior object will clarify the identity of the "parent" object that owns the given `parentSlot`.
+ *
+ * Try to avoid exposing `InternalSlot` in the process of implementing code. + * @param objectClass the code for the type of object being constructed + * @param guid the GUID this object will be assigned + * @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent + * @param obj the data used as representation of the object to be constructed + * @see ObjectClass.selectDataCodec + */ +final case class InternalSlot(objectClass : Int, + guid : PlanetSideGUID, + parentSlot : Int, + obj : ConstructorData) extends StreamBitSize { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = { + val base : Long = if(parentSlot > 127) 43L else 35L + base + obj.bitsize + } +} + +object InternalSlot extends Marshallable[InternalSlot] { + implicit val codec : Codec[InternalSlot] = ( + ("objectClass" | uintL(11)) >>:~ { obj_cls => + ("guid" | PlanetSideGUID.codec) :: + ("parentSlot" | PacketHelpers.encodedStringSize) :: + ("obj" | ObjectClass.selectDataCodec(obj_cls)) //it's fine for this call to fail + } + ).xmap[InternalSlot] ( + { + case cls :: guid :: slot :: Some(obj) :: HNil => + InternalSlot(cls, guid, slot, obj) + }, + { + case InternalSlot(cls, guid, slot, obj) => + cls :: guid :: slot :: Some(obj) :: HNil + } + ).as[InternalSlot] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala new file mode 100644 index 00000000..9a7232e8 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala @@ -0,0 +1,76 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.{Marshallable, PacketHelpers} +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * A representation of the inventory portion of `ObjectCreateMessage` packet data for avatars.
+ *
+ * The inventory is a temperamental thing. + * Items placed into the inventory must follow their proper encoding schematics to the letter. + * No values are allowed to be misplaced and no unexpected regions of data can be discovered. + * If there is even a minor failure, the whole of the inventory will fail to translate.
+ *
+ * Under the official servers, when a new character was generated, the inventory encoded as `0x1C`. + * This inventory had no size field, no contents, and an indeterminate number of values. + * This format is no longer supported. + * Going forward, an empty inventory - approximately `0x10000` - should be used as substitute.
+ *
+ * Exploration:
+ * 4u of ignored bits have been added to the end of the inventory to make up for missing stream length. + * They do not actually seem to be part of the inventory. + * Are these bits always at the end of the packet data and what is the significance? + * @param unk1 na; + * `true` to mark the start of the inventory data? + * is explicitly declaring the bit necessary when it always seems to be `true`? + * @param unk2 na + * @param unk3 na + * @param contents the actual items in the inventory; + * holster slots are 0-4; + * an inaccessible slot is 5; + * internal capacity is 6-`n`, where `n` is defined by exosuit type and is mapped into a grid + */ +final case class InventoryData(unk1 : Boolean, + unk2 : Boolean, + unk3 : Boolean, + contents : List[InventoryItem]) extends StreamBitSize { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = { + //three booleans, the 4u extra, and the 8u length field + val base : Long = 15L + //length of all items in inventory + var invSize : Long = 0L + for(item <- contents) { + invSize += item.bitsize + } + base + invSize + } +} + +object InventoryData extends Marshallable[InventoryData] { + implicit val codec : Codec[InventoryData] = ( + ("unk1" | bool) :: + (("len" | uint8L) >>:~ { len => + ("unk2" | bool) :: + ("unk3" | bool) :: + ("contents" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) :: + ignore(4) + }) + ).xmap[InventoryData] ( + { + case u1 :: _ :: a :: b :: ctnt :: _ :: HNil => + InventoryData(u1, a, b, ctnt) + }, + { + case InventoryData(u1, a, b, ctnt) => + u1 :: ctnt.size :: a :: b :: ctnt :: () :: HNil + } + ).as[InventoryData] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala new file mode 100644 index 00000000..25f17abe --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala @@ -0,0 +1,43 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import net.psforever.packet.game.PlanetSideGUID +import scodec.Codec +import scodec.codecs._ + +/** + * A representation of an item in an avatar's inventory. + * Reliance on `InternalSlot` indicates that this item is applicable to the same implicit parent-child relationship. + * (That is, its parent object will be clarified by the containing element, e.g., the inventory or its owner.) + * Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible.
+ *
+ * This intermediary object is primarily intended to mask external use of `InternalSlot`, as specified by the class. + * @param item the object in inventory + * @see InternalSlot + */ +final case class InventoryItem(item : InternalSlot) extends StreamBitSize { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = item.bitsize +} + +object InventoryItem extends Marshallable[InventoryItem] { + /** + * An abbreviated constructor for creating an `InventoryItem` without interacting with `InternalSlot` directly. + * @param objClass the code for the type of object (ammunition) being constructed + * @param guid the globally unique id assigned to the ammunition + * @param parentSlot the slot where the ammunition is to be installed in the weapon + * @param obj the constructor data + * @return an InventoryItem + */ + def apply(objClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : ConstructorData) : InventoryItem = + InventoryItem(InternalSlot(objClass, guid, parentSlot, obj)) + + implicit val codec : Codec[InventoryItem] = ( + "item" | InternalSlot.codec + ).as[InventoryItem] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala new file mode 100644 index 00000000..e351bc3d --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -0,0 +1,89 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ + +import scala.annotation.switch + +/** + * A reference between all object class codes and the name of the object they represent.
+ *
+ * Object classes compose a number between 0 and (probably) 2047, always translating into an 11-bit value. + * They are recorded as little-endian hexadecimal values here. + * In `scodec` terms, that's a `uintL(11)` or `uintL(0xB)`. + */ +object ObjectClass { + //character + final val AVATAR = 0x79 + //ammunition + final val BULLETS_9MM = 0x1C + final val BULLETS_9MM_AP = 0x1D + final val ENERGY_CELL = 0x110 + final val JAMMER_GRENADE_PACK = 0x19D + final val JAMMER_GRENADE_AMMO = 0x1A1 + final val FORCE_BLADE_AMMO = 0x21C + final val PLASMA_GRENADE_AMMO = 0x2A9 + final val BUCKSHOT = 0x2F3 //TODO apply internal name, eventually + //weapons + final val SUPPRESSOR = 0x34D + final val BEAMER = 0x8C + final val SWEEPER = 0x130 + final val FORCE_BLADE = 0x144 + final val GAUSS = 0x159 + final val JAMMER_GRENADE = 0x1A0 + final val PLASMA_GRENADE = 0x2A8 + final val PUNISHER = 0x2C2 + //tools + final val MEDKIT = 0x218 + final val REK = 0x2D8 + //unknown + final val SLOT_BLOCKER = 0x1C8 //strange item found in inventory slot #5, between holsters and grid + + //TODO refactor this function into another object later + /** + * Given an object class, retrieve the `Codec` used to parse and translate the constructor data for that type.
+ *
+ * This function serves as a giant `switch` statement that loosely connects object data to object class. + * All entries, save the default, merely point to the `Codec` of pattern `ConstructorData.genericPattern`. + * This pattern connects all `Codec`s back to the superclass `ConstructorData`. + * The default case is a failure case for trying to either decode or encode an unknown class of object. + * @param objClass the code for the type of object being constructed + * @return the `Codec` that handles the format of data for that particular item class, or a failing `Codec` + */ + def selectDataCodec(objClass : Int) : Codec[ConstructorData.genericPattern] = { + (objClass : @switch) match { + case ObjectClass.AVATAR => CharacterData.genericCodec + case ObjectClass.BEAMER => WeaponData.genericCodec + case ObjectClass.BUCKSHOT => AmmoBoxData.genericCodec + case ObjectClass.BULLETS_9MM => AmmoBoxData.genericCodec + case ObjectClass.BULLETS_9MM_AP => AmmoBoxData.genericCodec + case ObjectClass.ENERGY_CELL => AmmoBoxData.genericCodec + case ObjectClass.FORCE_BLADE_AMMO => AmmoBoxData.genericCodec + case ObjectClass.FORCE_BLADE => WeaponData.genericCodec + case ObjectClass.GAUSS => WeaponData.genericCodec + case ObjectClass.JAMMER_GRENADE => WeaponData.genericCodec + case ObjectClass.JAMMER_GRENADE_AMMO => AmmoBoxData.genericCodec + case ObjectClass.JAMMER_GRENADE_PACK => AmmoBoxData.genericCodec + case ObjectClass.MEDKIT => AmmoBoxData.genericCodec + case ObjectClass.PLASMA_GRENADE => WeaponData.genericCodec + case ObjectClass.PLASMA_GRENADE_AMMO => AmmoBoxData.genericCodec + case ObjectClass.PUNISHER => ConcurrentFeedWeaponData.genericCodec + case ObjectClass.REK => REKData.genericCodec + case ObjectClass.SLOT_BLOCKER => AmmoBoxData.genericCodec + case ObjectClass.SUPPRESSOR => WeaponData.genericCodec + case ObjectClass.SWEEPER => WeaponData.genericCodec + //failure case + case _ => conditional(false, bool).exmap[ConstructorData.genericPattern] ( + { + case None | _ => + Attempt.failure(Err("decoding unknown object class")) + }, + { + case None | _ => + Attempt.failure(Err("encoding unknown object class")) + } + ) + } + } +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala new file mode 100644 index 00000000..89078a59 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala @@ -0,0 +1,62 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * A representation of the REK portion of `ObjectCreateMessage` packet data. + * This data will help construct the "tool" called a Remote Electronics Kit.
+ *
+ * Of note is the first portion of the data which resembles the `WeaponData` format. + * @param unk na + */ +final case class REKData(unk : Int) extends ConstructorData { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = 67L +} + +object REKData extends Marshallable[REKData] { + implicit val codec : Codec[REKData] = ( + ("unk" | uint4L) :: + uint4L :: + uintL(20) :: + uint4L :: + uint16L :: + uint4L :: + uintL(15) + ).exmap[REKData] ( + { + case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil => + Attempt.successful(REKData(code)) + case code :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => + Attempt.failure(Err("invalid rek data format")) + }, + { + case REKData(code) => + Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil) + } + ).as[REKData] + + /** + * Transform between REKData and ConstructorData. + */ + val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( + { + case x => + Attempt.successful(Some(x.asInstanceOf[ConstructorData])) + }, + { + case Some(x) => + Attempt.successful(x.asInstanceOf[REKData]) + case _ => + Attempt.failure(Err("can not encode rek data")) + } + ) +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala new file mode 100644 index 00000000..28358a97 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala @@ -0,0 +1,39 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import scodec.Codec +import scodec.codecs._ + +/** + * Enumerate the player-displayed merit commendation awards granted for excellence (or tenacity) in combat. + * These are the medals players wish to brandish on their left pauldron.
+ *
+ * All merit commendation ribbons are represented by a 32-bit signature. + * The default "no-ribbon" value is `0xFFFFFFFF`, although some illegal values will also work. + * The term of service ribbon can not be modified by the user and will apply itself to its slot automatically when valid. + * @param upper the "top" configurable merit ribbon + * @param middle the central configurable merit ribbon + * @param lower the lower configurable merit ribbon + * @param tos the top-most term of service merit ribbon + */ +final case class RibbonBars(upper : Long = 0xFFFFFFFFL, + middle : Long = 0xFFFFFFFFL, + lower : Long = 0xFFFFFFFFL, + tos : Long = 0xFFFFFFFFL) extends StreamBitSize { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = 128L +} + +object RibbonBars extends Marshallable[RibbonBars] { + implicit val codec : Codec[RibbonBars] = ( + ("upper" | uint32L) :: + ("middle" | uint32L) :: + ("lower" | uint32L) :: + ("tos" | uint32L) + ).as[RibbonBars] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala new file mode 100644 index 00000000..bbdd12ce --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala @@ -0,0 +1,18 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +/** + * Apply this trait to a class that needs to have its size in bits calculated. + */ +trait StreamBitSize { + /** + * Performs a "sizeof()" analysis of the given object. + * The calculation reflects the `scodec Codec` definition rather than the explicit parameter fields. + * For example, an `Int` is normally a 32u number; + * when parsed with a `uintL(7)`, it's length will be considered 7u. + * (Note: being permanently signed, an `scodec` 32u value must fit into a `Long` type.) + * @return the number of bits necessary to represent this object; + * defaults to `0L` + */ + def bitsize : Long = 0L +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala new file mode 100644 index 00000000..41dded1d --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala @@ -0,0 +1,89 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import net.psforever.packet.game.PlanetSideGUID +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * A representation of a class of weapons that can be created using `ObjectCreateMessage` packet data. + * This data will help construct a "loaded weapon" such as a Suppressor or a Gauss.
+ *
+ * The data for the weapons nests information for the default (current) type and number of ammunition in its magazine. + * This ammunition data essentially is the weapon's magazines as numbered slots. + * Having said that, this format only handles one type of ammunition at a time. + * Any weapon that has two types of ammunition simultaneously loaded, e.g., a Punisher, must be handled with another `Codec`. + * This functionality is unrelated to a weapon that switches ammunition type; + * a weapon with that behavior is handled perfectly fine using this `case class`. + * @param unk na + * @param ammo data regarding the currently loaded ammunition type and quantity + * @see AmmoBoxData + */ +final case class WeaponData(unk : Int, + ammo : InternalSlot) extends ConstructorData { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @see AmmoBoxData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = 61L + ammo.bitsize +} + +object WeaponData extends Marshallable[WeaponData] { + /** + * An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.
+ *
+ * Exploration:
+ * This class may need to be rewritten later to support objects spawned in the world environment. + * @param unk na + * @param cls the code for the type of object (ammunition) being constructed + * @param guid the globally unique id assigned to the ammunition + * @param parentSlot the slot where the ammunition is to be installed in the weapon + * @param ammo the constructor data for the ammunition + * @return a WeaponData object + */ + def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData = + new WeaponData(unk, InternalSlot(cls, guid, parentSlot, ammo)) + + implicit val codec : Codec[WeaponData] = ( + ("unk" | uint4L) :: + uint4L :: + uint24 :: + uint16L :: + uint2 :: + uint8 :: //size = 1 type of ammunition loaded + uint2 :: + ("ammo" | InternalSlot.codec) :: + bool + ).exmap[WeaponData] ( + { + case code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil => + Attempt.successful(WeaponData(code, ammo)) + case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => + Attempt.failure(Err("invalid weapon data format")) + }, + { + case WeaponData(code, ammo) => + Attempt.successful(code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil) + } + ).as[WeaponData] + + /** + * Transform between WeaponData and ConstructorData. + */ + val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( + { + case x => + Attempt.successful(Some(x.asInstanceOf[ConstructorData])) + }, + { + case Some(x) => + Attempt.successful(x.asInstanceOf[WeaponData]) + case _ => + Attempt.failure(Err("can not encode weapon data")) + } + ) +} diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index e9cda565..f1dba3ab 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -4,8 +4,9 @@ import java.net.{InetAddress, InetSocketAddress} import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ +import net.psforever.packet.game.objectcreate.{InventoryItem, _} import net.psforever.types._ -import scodec.Attempt +import scodec.{Attempt, Err} import scodec.Attempt.Successful import scodec.bits._ @@ -254,25 +255,320 @@ class GamePacketTest extends Specification { } "ObjectCreateMessage" should { - val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 " - val packet2 = hex"18 17 74 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" + val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 " //fake data? + val packet2 = hex"18 F8 00 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" //fake data + //val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16) + var string_inventoryItem = hex"46 04 C0 08 08 80 00 00 20 00 0C 04 10 29 A0 10 19 00 00 04 00 00" + val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" + val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" + val string_punisher = hex"18 27010000 2580 612 a706 82 080000020000c08 1c13a0d01900000780 13a4701a072000000800" + val string_rek = hex"18 97000000 2580 6C2 9F05 81 48000002000080000" + val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" - "decode" in { + "decode (2)" in { + //an invalid bit representation will fail to turn into an object PacketCoding.DecodePacket(packet2).require match { - case obj @ ObjectCreateMessage(len, cls, guid, parent, rest) => - val manualRest = packet2.bits.drop(32 + 1 + 0xb + 16) - len === 29719 - cls === 121 - guid === 2497 - rest === manualRest - parent === None + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 248 + cls mustEqual 121 + guid mustEqual PlanetSideGUID(2497) + parent mustEqual None + data.isDefined mustEqual false case default => ko } } - "encode" in { - ok + "decode (9mm)" in { + PacketCoding.DecodePacket(string_9mm).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 124 + cls mustEqual 28 + guid mustEqual PlanetSideGUID(1280) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(75) + parent.get.slot mustEqual 33 + data.isDefined mustEqual true + data.get.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + case default => + ko + } + } + + "decode (gauss)" in { + PacketCoding.DecodePacket(string_gauss).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 220 + cls mustEqual 345 + guid mustEqual PlanetSideGUID(1465) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(75) + parent.get.slot mustEqual 2 + data.isDefined mustEqual true + val obj_wep = data.get.asInstanceOf[WeaponData] + obj_wep.unk mustEqual 4 + val obj_ammo = obj_wep.ammo + obj_ammo.objectClass mustEqual 28 + obj_ammo.guid mustEqual PlanetSideGUID(1286) + obj_ammo.parentSlot mustEqual 0 + obj_ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30 + case default => + ko + } + } + + "decode (punisher)" in { + PacketCoding.DecodePacket(string_punisher).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 295 + cls mustEqual 706 + guid mustEqual PlanetSideGUID(1703) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(75) + parent.get.slot mustEqual 2 + data.isDefined mustEqual true + val obj_wep = data.get.asInstanceOf[ConcurrentFeedWeaponData] + obj_wep.unk mustEqual 0 + val obj_ammo = obj_wep.ammo + obj_ammo.size mustEqual 2 + obj_ammo.head.objectClass mustEqual 28 + obj_ammo.head.guid mustEqual PlanetSideGUID(1693) + obj_ammo.head.parentSlot mustEqual 0 + obj_ammo.head.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30 + obj_ammo(1).objectClass mustEqual 413 + obj_ammo(1).guid mustEqual PlanetSideGUID(1564) + obj_ammo(1).parentSlot mustEqual 1 + obj_ammo(1).obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1 + case _ => + ko + } + } + + "decode (rek)" in { + PacketCoding.DecodePacket(string_rek).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 151 + cls mustEqual 0x2D8 + guid mustEqual PlanetSideGUID(1439) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(75) + parent.get.slot mustEqual 1 + data.isDefined mustEqual true + data.get.asInstanceOf[REKData].unk mustEqual 4 + case _ => + ko + } + } + + "decode (character)" in { + PacketCoding.DecodePacket(string_testchar).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 3159 + cls mustEqual 0x79 + guid mustEqual PlanetSideGUID(75) + parent.isDefined mustEqual false + data.isDefined mustEqual true + + val char = data.get.asInstanceOf[CharacterData] + char.appearance.pos.x mustEqual 3674.8438f + char.appearance.pos.y mustEqual 2726.789f + char.appearance.pos.z mustEqual 91.15625f + char.appearance.objYaw mustEqual 19 + char.appearance.faction mustEqual 2 //vs + char.appearance.bops mustEqual false + char.appearance.unk1 mustEqual 4 + char.appearance.name mustEqual "IlllIIIlllIlIllIlllIllI" + char.appearance.exosuit mustEqual 4 //standard + char.appearance.sex mustEqual 2 //female + char.appearance.face1 mustEqual 2 + char.appearance.face2 mustEqual 9 + char.appearance.voice mustEqual 1 //female 1 + char.appearance.unk2 mustEqual 3 + char.appearance.unk3 mustEqual 118 + char.appearance.unk4 mustEqual 30 + char.appearance.unk5 mustEqual 0x8080 + char.appearance.unk6 mustEqual 0xFFFF + char.appearance.unk7 mustEqual 2 + char.appearance.viewPitch mustEqual 0xFF + char.appearance.viewYaw mustEqual 0x6A + char.appearance.unk8 mustEqual 7 + char.appearance.ribbons.upper mustEqual 0xFFFFFFFFL //none + char.appearance.ribbons.middle mustEqual 0xFFFFFFFFL //none + char.appearance.ribbons.lower mustEqual 0xFFFFFFFFL //none + char.appearance.ribbons.tos mustEqual 0xFFFFFFFFL //none + char.healthMax mustEqual 100 + char.health mustEqual 100 + char.armor mustEqual 50 //standard exosuit value + char.unk1 mustEqual 1 + char.unk2 mustEqual 7 + char.unk3 mustEqual 7 + char.staminaMax mustEqual 100 + char.stamina mustEqual 100 + char.unk4 mustEqual 28 + char.unk5 mustEqual 4 + char.unk6 mustEqual 44 + char.unk7 mustEqual 84 + char.unk8 mustEqual 104 + char.unk9 mustEqual 1900 + char.firstTimeEvents.size mustEqual 4 + char.firstTimeEvents.head mustEqual "xpe_sanctuary_help" + char.firstTimeEvents(1) mustEqual "xpe_th_firemodes" + char.firstTimeEvents(2) mustEqual "used_beamer" + char.firstTimeEvents(3) mustEqual "map13" + char.tutorials.size mustEqual 0 + char.inventory.unk1 mustEqual true + char.inventory.unk2 mustEqual false + char.inventory.contents.size mustEqual 10 + val inventory = char.inventory.contents + //0 + inventory.head.item.objectClass mustEqual 0x8C //beamer + inventory.head.item.guid mustEqual PlanetSideGUID(76) + inventory.head.item.parentSlot mustEqual 0 + var wep = inventory.head.item.obj.asInstanceOf[WeaponData] + wep.ammo.objectClass mustEqual 0x110 //plasma + wep.ammo.guid mustEqual PlanetSideGUID(77) + wep.ammo.parentSlot mustEqual 0 + wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 16 + //1 + inventory(1).item.objectClass mustEqual 0x34D //suppressor + inventory(1).item.guid mustEqual PlanetSideGUID(78) + inventory(1).item.parentSlot mustEqual 2 + wep = inventory(1).item.obj.asInstanceOf[WeaponData] + wep.ammo.objectClass mustEqual 0x1C //9mm + wep.ammo.guid mustEqual PlanetSideGUID(79) + wep.ammo.parentSlot mustEqual 0 + wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 25 + //2 + inventory(2).item.objectClass mustEqual 0x144 //force blade + inventory(2).item.guid mustEqual PlanetSideGUID(80) + inventory(2).item.parentSlot mustEqual 4 + wep = inventory(2).item.obj.asInstanceOf[WeaponData] + wep.ammo.objectClass mustEqual 0x21C //force blade ammo + wep.ammo.guid mustEqual PlanetSideGUID(81) + wep.ammo.parentSlot mustEqual 0 + wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1 + //3 + inventory(3).item.objectClass mustEqual 0x1C8 //thing + inventory(3).item.guid mustEqual PlanetSideGUID(82) + inventory(3).item.parentSlot mustEqual 5 + inventory(3).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1 + //4 + inventory(4).item.objectClass mustEqual 0x1C //9mm + inventory(4).item.guid mustEqual PlanetSideGUID(83) + inventory(4).item.parentSlot mustEqual 6 + inventory(4).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + //5 + inventory(5).item.objectClass mustEqual 0x1C //9mm + inventory(5).item.guid mustEqual PlanetSideGUID(84) + inventory(5).item.parentSlot mustEqual 9 + inventory(5).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + //6 + inventory(6).item.objectClass mustEqual 0x1C //9mm + inventory(6).item.guid mustEqual PlanetSideGUID(85) + inventory(6).item.parentSlot mustEqual 12 + inventory(6).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + //7 + inventory(7).item.objectClass mustEqual 0x1D //9mm ap + inventory(7).item.guid mustEqual PlanetSideGUID(86) + inventory(7).item.parentSlot mustEqual 33 + inventory(7).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + //8 + inventory(8).item.objectClass mustEqual 0x110 //plasma + inventory(8).item.guid mustEqual PlanetSideGUID(87) + inventory(8).item.parentSlot mustEqual 36 + inventory(8).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + //9 + inventory(9).item.objectClass mustEqual 0x2D8 //rek + inventory(9).item.guid mustEqual PlanetSideGUID(88) + inventory(9).item.parentSlot mustEqual 39 + //the rek has data but none worth testing here + case default => + ko + } + } + + "encode (2)" in { + //the lack of an object will fail to turn into a bad bitstream + val msg = ObjectCreateMessage(0, 121, PlanetSideGUID(2497), None, None) + PacketCoding.EncodePacket(msg).isFailure mustEqual true + } + + "encode (9mm)" in { + val obj = AmmoBoxData(50) + val msg = ObjectCreateMessage(0, 28, PlanetSideGUID(1280), ObjectCreateMessageParent(PlanetSideGUID(75), 33), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_9mm + } + + "encode (gauss)" in { + val obj = WeaponData(4, 28, PlanetSideGUID(1286), 0, AmmoBoxData(30)) + val msg = ObjectCreateMessage(0, 345, PlanetSideGUID(1465), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_gauss + } + + "encode (punisher)" in { + val obj = ConcurrentFeedWeaponData(0, AmmoBoxData(28, PlanetSideGUID(1693), 0, AmmoBoxData(30)) :: AmmoBoxData(413, PlanetSideGUID(1564), 1, AmmoBoxData(1)) :: Nil) + val msg = ObjectCreateMessage(0, 706, PlanetSideGUID(1703), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj) + var pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_punisher + } + + "encode (rek)" in { + val obj = REKData(4) + val msg = ObjectCreateMessage(0, 0x2D8, PlanetSideGUID(1439), ObjectCreateMessageParent(PlanetSideGUID(75), 1), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_rek + } + + "encode (character)" in { + val app = CharacterAppearanceData( + Vector3(3674.8438f, 2726.789f, 91.15625f), + 19, + 2, + false, + 4, + "IlllIIIlllIlIllIlllIllI", + 4, + 2, + 2,9, + 1, + 3, 118,30, 0x8080, 0xFFFF, 2, + 255, 106, 7, + RibbonBars() + ) + val inv = InventoryItem(0x8C, PlanetSideGUID(76), 0, WeaponData(8, 0x110, PlanetSideGUID(77), 0, AmmoBoxData(16))) :: + InventoryItem(0x34D, PlanetSideGUID(78), 2, WeaponData(8, 0x1C, PlanetSideGUID(79), 0, AmmoBoxData(25))) :: + InventoryItem(0x144, PlanetSideGUID(80), 4, WeaponData(8, 0x21C, PlanetSideGUID(81), 0, AmmoBoxData(1))) :: + InventoryItem(0x1C8, PlanetSideGUID(82), 5, AmmoBoxData(1)) :: + InventoryItem(0x1C, PlanetSideGUID(83), 6, AmmoBoxData(50)) :: + InventoryItem(0x1C, PlanetSideGUID(84), 9, AmmoBoxData(50)) :: + InventoryItem(0x1C, PlanetSideGUID(85), 12, AmmoBoxData(50)) :: + InventoryItem(0x1D, PlanetSideGUID(86), 33, AmmoBoxData(50)) :: + InventoryItem(0x110, PlanetSideGUID(87), 36, AmmoBoxData(50)) :: + InventoryItem(0x2D8, PlanetSideGUID(88), 39, REKData(8)) :: + Nil + val obj = CharacterData( + app, + 100, 100, + 50, + 1, 7, 7, + 100, 100, + 28, 4, 44, 84, 104, 1900, + "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, + List.empty, + InventoryData( + true, false, false, inv + ) + ) + val msg = ObjectCreateMessage(0, 0x79, PlanetSideGUID(75), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_testchar } } diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index ff525441..8232882f 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -9,7 +9,8 @@ import scodec.Attempt.{Failure, Successful} import scodec.bits._ import org.log4s.MDC import MDCContextAware.Implicits._ -import net.psforever.types.ChatMessageType +import net.psforever.packet.game.objectcreate._ +import net.psforever.types.{ChatMessageType, Vector3} class WorldSessionActor extends Actor with MDCContextAware { private[this] val log = org.log4s.getLogger @@ -107,8 +108,49 @@ class WorldSessionActor extends Actor with MDCContextAware { } } - // XXX: hard coded ObjectCreateMessage - val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00 " + //val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" + //currently, the character's starting BEP is discarded due to unknown bit format + val app = CharacterAppearanceData( + Vector3(3674.8438f, 2726.789f, 91.15625f), + 19, + 2, + false, + 4, + "IlllIIIlllIlIllIlllIllI", + 4, + 2, + 2, 9, + 1, + 3, 118, 30, 0x8080, 0xFFFF, 2, + 255, 106, 7, + RibbonBars() + ) + val inv = + InventoryItem(ObjectClass.BEAMER, PlanetSideGUID(76), 0, WeaponData(8, ObjectClass.ENERGY_CELL, PlanetSideGUID(77), 0, AmmoBoxData(16))) :: + InventoryItem(ObjectClass.SUPPRESSOR, PlanetSideGUID(78), 2, WeaponData(8, ObjectClass.BULLETS_9MM, PlanetSideGUID(79), 0, AmmoBoxData(25))) :: + InventoryItem(ObjectClass.FORCE_BLADE, PlanetSideGUID(80), 4, WeaponData(8, ObjectClass.FORCE_BLADE_AMMO, PlanetSideGUID(81), 0, AmmoBoxData(1))) :: + InventoryItem(ObjectClass.SLOT_BLOCKER, PlanetSideGUID(82), 5, AmmoBoxData(1)) :: + InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(83), 6, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(84), 9, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(85), 12, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.BULLETS_9MM_AP, PlanetSideGUID(86), 33, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.ENERGY_CELL, PlanetSideGUID(87), 36, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.REK, PlanetSideGUID(88), 39, REKData(8)) :: + Nil + val obj = CharacterData( + app, + 100, 100, + 50, + 1, 7, 7, + 100, 100, + 28, 4, 44, 84, 104, 1900, + "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, + List.empty, + InventoryData( + true, false, false, inv + ) + ) + val objectHex = ObjectCreateMessage(0, ObjectClass.AVATAR, PlanetSideGUID(75), obj) def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match { case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) => @@ -118,7 +160,7 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"New world login to ${server} with Token:${token}. ${clientVersion}") // ObjectCreateMessage - sendRawResponse(objectHex) + sendResponse(PacketCoding.CreateGamePacket(0, objectHex)) // XXX: hard coded message sendRawResponse(hex"14 0F 00 00 00 10 27 00 00 C1 D8 7A 02 4B 00 26 5C B0 80 00 ") @@ -132,14 +174,14 @@ class WorldSessionActor extends Actor with MDCContextAware { case CharacterRequestAction.Delete => sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1)))) case CharacterRequestAction.Select => - PacketCoding.DecodeGamePacket(objectHex).require match { + objectHex match { case obj @ ObjectCreateMessage(len, cls, guid, _, _) => log.debug("Object: " + obj) // LoadMapMessage 13714 in mossy .gcap // XXX: hardcoded shit sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0))) - sendRawResponse(objectHex) + sendResponse(PacketCoding.CreateGamePacket(0, objectHex)) // These object_guids are specfic to VS Sanc sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C @@ -170,8 +212,8 @@ class WorldSessionActor extends Actor with MDCContextAware { true, //Boosted spawn room pain field true))) //Boosted generator room pain field - sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(PlanetSideGUID(guid),0,0))) - sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(PlanetSideGUID(guid), 1, 0, true, Shortcut.MEDKIT))) + sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0))) + sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT))) import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global