Merge branch 'object-create'

This commit is contained in:
FateJH 2017-01-15 00:25:01 -05:00
commit 8b28eda470
16 changed files with 1696 additions and 153 deletions

View file

@ -3,15 +3,12 @@ package net.psforever.packet
import java.nio.charset.Charset import java.nio.charset.Charset
import scodec.Attempt.Successful import scodec.{DecodeResult, Err, Codec, Attempt}
import scodec.{Attempt, Codec, DecodeResult, Err}
import scodec.bits._ import scodec.bits._
import scodec.codecs._ import scodec.codecs._
import scodec._ import scodec._
import shapeless._ import shapeless._
import scala.util.Success
/** The base of all packets */ /** The base of all packets */
sealed trait PlanetSidePacket extends Serializable { sealed trait PlanetSidePacket extends Serializable {
def encode : Attempt[BitVector] def encode : Attempt[BitVector]
@ -64,7 +61,7 @@ final case class PlanetSidePacketFlags(packetType : PacketType.Value, secured :
/** Codec for [[PlanetSidePacketFlags]] */ /** Codec for [[PlanetSidePacketFlags]] */
object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] { object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
implicit val codec : Codec[PlanetSidePacketFlags] = ( implicit val codec : Codec[PlanetSidePacketFlags] = (
("packet_type" | PacketType.codec) :: // first 4-bits ("packet_type" | PacketType.codec) :: // first 4-bits
("unused" | constant(bin"0")) :: ("unused" | constant(bin"0")) ::
("secured" | bool) :: ("secured" | bool) ::
("advanced" | constant(bin"1")) :: // we only support "advanced packets" ("advanced" | constant(bin"1")) :: // we only support "advanced packets"
@ -77,38 +74,35 @@ object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
object PacketHelpers { object PacketHelpers {
/** Used in certain instances where Codec defintions are stubbed out */ /** Used in certain instances where Codec defintions are stubbed out */
def emptyCodec[T](instance : T) = { def emptyCodec[T](instance : T) = {
def to(pkt: T) = HNil def to(pkt : T) = HNil
def from(a: HNil) = instance def from(a : HNil) = instance
Codec[HNil].xmap[T](from, to) Codec[HNil].xmap[T](from, to)
} }
/** Create a Codec for an enumeration type that can correctly represent its value /** 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 * @param storageCodec the Codec used for actually representing the value
* @tparam E The inferred type * @tparam E The inferred type
* @return Generated codec * @return Generated codec
*/ */
def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = { def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = {
type Struct = Int :: HNil 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) val primitiveLimit = Math.pow(2, storageCodec.sizeBound.exact.get)
// Assure that the enum will always be able to fit in a N-bit int // Assure that the enum will always be able to fit in a N-bit int
assert(enum.maxId <= primitiveLimit, assert(enum.maxId <= primitiveLimit,
enum.getClass.getCanonicalName + s": maxId exceeds primitive type (limit of $primitiveLimit, maxId ${enum.maxId})") 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 pkt.id :: HNil
} }
def from(struct: Struct): Attempt[E#Value] = struct match { def from(struct : Struct) : Attempt[E#Value] = struct match {
case enumVal :: HNil => case enumVal :: HNil =>
// verify that this int can match the enum // verify that this int can match the enum
val first = enum.values.firstKey.id val first = enum.values.firstKey.id
val last = enum.maxId-1 val last = enum.maxId - 1
if(enumVal >= first && enumVal <= last) if(enumVal >= first && enumVal <= last)
Attempt.successful(enum(enumVal)) Attempt.successful(enum(enumVal))
@ -147,13 +141,11 @@ object PacketHelpers {
/** Codec for how PlanetSide represents strings on the wire */ /** Codec for how PlanetSide represents strings on the wire */
def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii) def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii)
/** Same as [[encodedString]] but with a bit adjustment /** 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 * 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 * 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. * variability and there doesn't appear to be a way to fix this issue.
*
* @param adjustment The adjustment amount in bits * @param adjustment The adjustment amount in bits
* @return Generated string decoding codec with adjustment * @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 * 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( def encodedWideString : Codec[String] = variableSizeBytes(encodedStringSize.xmap(
insize => insize*2, // number of symbols -> number of bytes (decode) insize => insize * 2, // number of symbols -> number of bytes (decode)
outSize => outSize/2 // number of bytes -> number of symbols (encode) outSize => outSize / 2 // number of bytes -> number of symbols (encode)
), utf16) ), utf16)
/** Same as [[encodedWideString]] but with a bit alignment after the decoded size /** Same as [[encodedWideString]] but with a bit alignment after the decoded size
*/ */
def encodedWideStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment).xmap( def encodedWideStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment).xmap(
insize => insize*2, insize => insize * 2,
outSize => outSize/2 outSize => outSize / 2
), utf16) ), utf16)
// TODO: make the function below work as there are places it should be used // TODO: make the function below work as there are places it should be used
@ -185,7 +177,6 @@ object PacketHelpers {
exmap[Int]( exmap[Int](
(a : Either[Int, Int]) => { (a : Either[Int, Int]) => {
val result = a.fold[Int](a => a, a => a) val result = a.fold[Int](a => a, a => a)
if(result > limit) if(result > limit)
Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit")) Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit"))
else else
@ -195,7 +186,6 @@ object PacketHelpers {
if(a > limit) if(a > limit)
return Attempt.failure(Err("adsf")) return Attempt.failure(Err("adsf"))
//return Left(Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit"))) //return Left(Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit")))
if(a > 0x7f) if(a > 0x7f)
return Attempt.successful(Left(a)) return Attempt.successful(Left(a))
else else
@ -203,48 +193,13 @@ object PacketHelpers {
} }
) )
} }
def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii) 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.<br>
* <br>
* 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.<br>
* <br>
* 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`.<br> * Encode and decode a byte-aligned `List`.<br>
* <br> * <br>
* This function is copied almost verbatim from its source, but swapping the normal `ListCodec` for a new `AlignedListCodec`. * This function is copied almost verbatim from its source, with exception of 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)`.
* @param countCodec the codec that represents the prefixed size of the `List` * @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 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` * @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]] = { def listOfNAligned[A](countCodec : Codec[Long], alignment : Int, valueCodec : Codec[A]) : Codec[List[A]] = {
countCodec. countCodec.
flatZip { flatZip { count => new AlignedListCodec(countCodec, valueCodec, alignment, Some(count)) }.
count => narrow[List[A]]({ case (cnt, xs) =>
new AlignedListCodec(countCodec, valueCodec, alignment, Some(count)) if(xs.size == cnt) Attempt.successful(xs)
}. else Attempt.failure(Err(s"Insufficient number of elements: decoded ${xs.size} but should have decoded $cnt"))
narrow[List[A]] ( }, xs => (xs.size, xs)).
{
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)") withToString(s"listOfN($countCodec, $valueCodec)")
} }
/**
* Codec that encodes/decodes a list of `n` elements, where `n` is known at compile time.<br>
* <br>
* 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`.<br> * The codec that encodes and decodes a byte-aligned `List`.<br>
* <br> * <br>
* This class is copied almost verbatim from its source, with two major modifications. * This class is copied almost verbatim from its source, with only heavy modifications to its `encode` process.
* 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.
* @param countCodec the codec that represents the prefixed size of the `List` * @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 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) * @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]
* <br> * <br>
* 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. * 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. * 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 * @param list the `List` to be encoded
* @return the `BitVector` encoding, if successful * @return the `BitVector` encoding, if successful
*/ */
override def encode(list : List[A]) : Attempt[BitVector] = { 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) { if(alignment > 0) {
solve match { solve match {
case Attempt.Successful(vector) => case Attempt.Successful(vector) =>
val countCodecSize : Long = countCodec.sizeBound.lowerBound 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 _ => case _ =>
solve = Attempt.failure(Err("failed to create a list")) return Attempt.failure(Err("failed to create a list"))
} }
} }
solve 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 * @param buffer the encoded bits in the `List`, preceded by the alignment bits
* @return the decoded `List` * @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 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)) Decoder.decodeCollect[List, A](valueCodec, lim)(buffer.drop(alignment))
} }
/** /**
* The size of the encoded `List`. * The size of the encoded `List`.<br>
* <br>
* Unchanged from original.
* @return the size as calculated by the size of each element for each element * @return the size as calculated by the size of each element for each element
*/ */
override def sizeBound = limit match { def sizeBound = limit match {
case None => case None => SizeBound.unknown
SizeBound.unknown case Some(lim) => valueCodec.sizeBound * lim
case Some(lim : Long) =>
valueCodec.sizeBound * lim
} }
/** /**
* Get a `String` representation of this `List`. * Get a `String` representation of this `List`.<br>
* <br>
* Unchanged from original. * Unchanged from original.
* @return the `String` representation * @return the `String` representation
*/ */
override def toString = s"list($valueCodec)" override def toString = s"list($valueCodec)"
} }

View file

@ -1,57 +1,239 @@
// Copyright (c) 2016 PSForever.net to present
package net.psforever.packet.game package net.psforever.packet.game
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass, StreamBitSize}
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.bits._ import scodec.bits.BitVector
import scodec.{Attempt, Codec, Err} import scodec.{Attempt, Codec, DecodeResult, Err}
import scodec.codecs._ import scodec.codecs._
import shapeless._ import shapeless.{::, HNil}
case class ObjectCreateMessageParent(guid : Int, slot : Int) /**
* The parent information of a created object.<br>
* <br>
* 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, * Communicate with the client that a certain object with certain properties is to be created.
guid : Int, * The object may also have primitive assignment (attachment) properties.<br>
parentInfo : Option[ObjectCreateMessageParent], * <br>
stream : BitVector * 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.<br>
* <br>
* 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.)<br>
* <br>
* 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 { extends PlanetSideGamePacket {
def opcode = GamePacketOpcode.ObjectCreateMessage def opcode = GamePacketOpcode.ObjectCreateMessage
def encode = ObjectCreateMessage.encode(this) def encode = ObjectCreateMessage.encode(this)
} }
object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { 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)) :: type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil
("guid" | uint16L)).xmap[Pattern]( { type outPattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil
case cls :: guid :: HNil => cls :: guid :: None :: HNil /**
}, { * Codec for formatting around the lack of parent data in the stream.
case cls :: guid :: None :: HNil => cls :: guid :: HNil */
}) private val noParent : Codec[Pattern] = (
val parent : Codec[Pattern] = (("parent_guid" | uint16L) :: ("objectClass" | uintL(0xb)) :: //11u
("object_class" | uintL(0xb)) :: ("guid" | PlanetSideGUID.codec) //16u
("guid" | uint16L) :: ).xmap[Pattern](
("parent_slot_index" | PacketHelpers.encodedStringSize)).xmap[Pattern]( { {
case pguid :: cls :: guid :: slot :: HNil => case cls :: guid :: HNil =>
cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil cls :: guid :: None :: HNil
}, { }, {
case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil => case cls :: guid :: None :: HNil =>
pguid :: cls :: guid :: slot :: 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.<br>
* <br>
* 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] = ( implicit val codec : Codec[ObjectCreateMessage] = (
("stream_length" | uint32L) :: (either(bool, parent, noParent).exmap[Pattern]( { ("streamLength" | uint32L) ::
case Left(a :: b :: Some(c) :: HNil) => Attempt.successful(a :: b :: Some(c) :: HNil) (either(bool, parent, noParent).exmap[Pattern] (
case Right(a :: b :: None :: HNil) => Attempt.successful(a :: b :: None :: HNil) {
// failure cases case Left(a :: b :: Some(c) :: HNil) =>
case Left(a :: b :: None :: HNil) => Attempt.failure(Err("expected parent structure")) Attempt.successful(a :: b :: Some(c) :: HNil) //true, _, _, Some(c)
case Right(a :: b :: Some(c) :: HNil) => Attempt.failure(Err("got unexpected parent structure")) case Right(a :: b :: None :: HNil) =>
}, { Attempt.successful(a :: b :: None :: HNil) //false, _, _, None
case a :: b :: Some(c) :: HNil => Attempt.successful(Left(a :: b :: Some(c) :: HNil)) // failure cases
case a :: b :: None :: HNil => Attempt.successful(Right(a :: b :: None :: HNil)) case Left(a :: b :: None :: HNil) =>
}) :+ ("rest" | bits) ) Attempt.failure(Err("missing parent structure")) //true, _, _, None
).as[ObjectCreateMessage] 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]
} }

View file

@ -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.<br>
* <br>
* 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"))
}
)
}

View file

@ -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.<br>
* <br>
* 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.<br>
* <br>
* 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.
* <br>
* Faction:<br>
* `0 - Terran Republic`<br>
* `1 - New Conglomerate`<br>
* `2 - Vanu Sovereignty`<br>
* <br>
* Exo-suit:<br>
* `0 - Agile`<br>
* `1 - Refinforced`<br>
* `2 - Mechanized Assault`<br>
* `3 - Infiltration`<br>
* `4 - Standard`<br>
* <br>
* Sex:<br>
* `0 - invalid`<br>
* `1 - Male`<br>
* `2 - Female`<br>
* `3 - invalid`<br>
* <br>
* Voice:<br>
* `&nbsp;&nbsp;&nbsp;&nbsp;MALE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FEMALE`<br>
* `0 - no voice &nbsp;no voice`<br>
* `1 - male_1 &nbsp;&nbsp; female_1`<br>
* `2 - male_2 &nbsp;&nbsp; female_2`<br>
* `3 - male_3 &nbsp;&nbsp; female_3`<br>
* `4 - male_4 &nbsp;&nbsp; female_4`<br>
* `5 - male_5 &nbsp;&nbsp; female_5`<br>
* `6 - female_1 &nbsp;no voice`<br>
* `7 - female_2 &nbsp;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.<br>
* <br>
* 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.<br>
* <br>
* 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.<br>
* <br>
* 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.<br>
* <br>
* 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"))
}
)
}

View file

@ -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.<br>
* <br>
* 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`.<br>
* <br>
* Exploration:<br>
* 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"))
}
)
}

View file

@ -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.<br>
* <br>
* 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]
}

View file

@ -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.<br>
* <br>
* 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`.<br>
* <br>
* 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]
}

View file

@ -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.<br>
* <br>
* 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.<br>
* <br>
* 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.<br>
* <br>
* Exploration:<br>
* 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]
}

View file

@ -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.<br>
* <br>
* 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]
}

View file

@ -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.<br>
* <br>
* 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.<br>
* <br>
* 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"))
}
)
}
}
}

View file

@ -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.<br>
* <br>
* 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"))
}
)
}

View file

@ -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.<br>
* <br>
* 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]
}

View file

@ -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
}

View file

@ -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.<br>
* <br>
* 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`.<br>
* <br>
* Exploration:<br>
* 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"))
}
)
}

View file

@ -4,8 +4,9 @@ import java.net.{InetAddress, InetSocketAddress}
import org.specs2.mutable._ import org.specs2.mutable._
import net.psforever.packet._ import net.psforever.packet._
import net.psforever.packet.game._ import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate.{InventoryItem, _}
import net.psforever.types._ import net.psforever.types._
import scodec.Attempt import scodec.{Attempt, Err}
import scodec.Attempt.Successful import scodec.Attempt.Successful
import scodec.bits._ import scodec.bits._
@ -254,25 +255,320 @@ class GamePacketTest extends Specification {
} }
"ObjectCreateMessage" should { "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 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 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 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 { PacketCoding.DecodePacket(packet2).require match {
case obj @ ObjectCreateMessage(len, cls, guid, parent, rest) => case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
val manualRest = packet2.bits.drop(32 + 1 + 0xb + 16) len mustEqual 248
len === 29719 cls mustEqual 121
cls === 121 guid mustEqual PlanetSideGUID(2497)
guid === 2497 parent mustEqual None
rest === manualRest data.isDefined mustEqual false
parent === None
case default => case default =>
ko ko
} }
} }
"encode" in { "decode (9mm)" in {
ok 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
} }
} }

View file

@ -9,7 +9,8 @@ import scodec.Attempt.{Failure, Successful}
import scodec.bits._ import scodec.bits._
import org.log4s.MDC import org.log4s.MDC
import MDCContextAware.Implicits._ 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 { class WorldSessionActor extends Actor with MDCContextAware {
private[this] val log = org.log4s.getLogger 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 { def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match {
case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) => 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}") log.info(s"New world login to ${server} with Token:${token}. ${clientVersion}")
// ObjectCreateMessage // ObjectCreateMessage
sendRawResponse(objectHex) sendResponse(PacketCoding.CreateGamePacket(0, objectHex))
// XXX: hard coded message // 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 ") 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 => case CharacterRequestAction.Delete =>
sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1)))) sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1))))
case CharacterRequestAction.Select => case CharacterRequestAction.Select =>
PacketCoding.DecodeGamePacket(objectHex).require match { objectHex match {
case obj @ ObjectCreateMessage(len, cls, guid, _, _) => case obj @ ObjectCreateMessage(len, cls, guid, _, _) =>
log.debug("Object: " + obj) log.debug("Object: " + obj)
// LoadMapMessage 13714 in mossy .gcap // LoadMapMessage 13714 in mossy .gcap
// XXX: hardcoded shit // XXX: hardcoded shit
sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary 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))) 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 // These object_guids are specfic to VS Sanc
sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C 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 spawn room pain field
true))) //Boosted generator room pain field true))) //Boosted generator room pain field
sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(PlanetSideGUID(guid),0,0))) sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0)))
sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(PlanetSideGUID(guid), 1, 0, true, Shortcut.MEDKIT))) sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)))
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global