mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-19 18:14:44 +00:00
Merge branch 'object-create'
This commit is contained in:
commit
8b28eda470
|
|
@ -3,15 +3,12 @@ package net.psforever.packet
|
|||
|
||||
import java.nio.charset.Charset
|
||||
|
||||
import scodec.Attempt.Successful
|
||||
import scodec.{Attempt, Codec, DecodeResult, Err}
|
||||
import scodec.{DecodeResult, Err, Codec, Attempt}
|
||||
import scodec.bits._
|
||||
import scodec.codecs._
|
||||
import scodec._
|
||||
import shapeless._
|
||||
|
||||
import scala.util.Success
|
||||
|
||||
/** The base of all packets */
|
||||
sealed trait PlanetSidePacket extends Serializable {
|
||||
def encode : Attempt[BitVector]
|
||||
|
|
@ -64,7 +61,7 @@ final case class PlanetSidePacketFlags(packetType : PacketType.Value, secured :
|
|||
/** Codec for [[PlanetSidePacketFlags]] */
|
||||
object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
|
||||
implicit val codec : Codec[PlanetSidePacketFlags] = (
|
||||
("packet_type" | PacketType.codec) :: // first 4-bits
|
||||
("packet_type" | PacketType.codec) :: // first 4-bits
|
||||
("unused" | constant(bin"0")) ::
|
||||
("secured" | bool) ::
|
||||
("advanced" | constant(bin"1")) :: // we only support "advanced packets"
|
||||
|
|
@ -77,38 +74,35 @@ object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
|
|||
object PacketHelpers {
|
||||
/** Used in certain instances where Codec defintions are stubbed out */
|
||||
def emptyCodec[T](instance : T) = {
|
||||
def to(pkt: T) = HNil
|
||||
def from(a: HNil) = instance
|
||||
def to(pkt : T) = HNil
|
||||
def from(a : HNil) = instance
|
||||
Codec[HNil].xmap[T](from, to)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Create a Codec for an enumeration type that can correctly represent its value
|
||||
*
|
||||
* @param enum the enumeration type to create a codec for
|
||||
* @param enum the enumeration type to create a codec for
|
||||
* @param storageCodec the Codec used for actually representing the value
|
||||
* @tparam E The inferred type
|
||||
* @return Generated codec
|
||||
*/
|
||||
def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = {
|
||||
type Struct = Int :: HNil
|
||||
val struct: Codec[Struct] = storageCodec.hlist
|
||||
val struct : Codec[Struct] = storageCodec.hlist
|
||||
val primitiveLimit = Math.pow(2, storageCodec.sizeBound.exact.get)
|
||||
|
||||
// Assure that the enum will always be able to fit in a N-bit int
|
||||
assert(enum.maxId <= primitiveLimit,
|
||||
enum.getClass.getCanonicalName + s": maxId exceeds primitive type (limit of $primitiveLimit, maxId ${enum.maxId})")
|
||||
|
||||
def to(pkt: E#Value): Struct = {
|
||||
def to(pkt : E#Value) : Struct = {
|
||||
pkt.id :: HNil
|
||||
}
|
||||
|
||||
def from(struct: Struct): Attempt[E#Value] = struct match {
|
||||
def from(struct : Struct) : Attempt[E#Value] = struct match {
|
||||
case enumVal :: HNil =>
|
||||
// verify that this int can match the enum
|
||||
val first = enum.values.firstKey.id
|
||||
val last = enum.maxId-1
|
||||
val last = enum.maxId - 1
|
||||
|
||||
if(enumVal >= first && enumVal <= last)
|
||||
Attempt.successful(enum(enumVal))
|
||||
|
|
@ -147,13 +141,11 @@ object PacketHelpers {
|
|||
/** Codec for how PlanetSide represents strings on the wire */
|
||||
def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii)
|
||||
|
||||
|
||||
/** Same as [[encodedString]] but with a bit adjustment
|
||||
*
|
||||
* This comes in handy when a PlanetSide string is decoded on a non-byte boundary. The PlanetSide client
|
||||
* will byte align after decoding the string lenght, but BEFORE the string itself. Scodec doesn't like this
|
||||
* variability and there doesn't appear to be a way to fix this issue.
|
||||
*
|
||||
* @param adjustment The adjustment amount in bits
|
||||
* @return Generated string decoding codec with adjustment
|
||||
*/
|
||||
|
|
@ -168,15 +160,15 @@ object PacketHelpers {
|
|||
* input string. We use xmap to transform the [[encodedString]] codec as this change is just a division and multiply
|
||||
*/
|
||||
def encodedWideString : Codec[String] = variableSizeBytes(encodedStringSize.xmap(
|
||||
insize => insize*2, // number of symbols -> number of bytes (decode)
|
||||
outSize => outSize/2 // number of bytes -> number of symbols (encode)
|
||||
insize => insize * 2, // number of symbols -> number of bytes (decode)
|
||||
outSize => outSize / 2 // number of bytes -> number of symbols (encode)
|
||||
), utf16)
|
||||
|
||||
/** Same as [[encodedWideString]] but with a bit alignment after the decoded size
|
||||
*/
|
||||
def encodedWideStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment).xmap(
|
||||
insize => insize*2,
|
||||
outSize => outSize/2
|
||||
insize => insize * 2,
|
||||
outSize => outSize / 2
|
||||
), utf16)
|
||||
|
||||
// TODO: make the function below work as there are places it should be used
|
||||
|
|
@ -185,7 +177,6 @@ object PacketHelpers {
|
|||
exmap[Int](
|
||||
(a : Either[Int, Int]) => {
|
||||
val result = a.fold[Int](a => a, a => a)
|
||||
|
||||
if(result > limit)
|
||||
Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit"))
|
||||
else
|
||||
|
|
@ -195,7 +186,6 @@ object PacketHelpers {
|
|||
if(a > limit)
|
||||
return Attempt.failure(Err("adsf"))
|
||||
//return Left(Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit")))
|
||||
|
||||
if(a > 0x7f)
|
||||
return Attempt.successful(Left(a))
|
||||
else
|
||||
|
|
@ -203,48 +193,13 @@ object PacketHelpers {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Codec that encodes/decodes a list of `n` elements, where `n` is known at compile time.<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>
|
||||
* <br>
|
||||
* This function is copied almost verbatim from its source, but swapping the normal `ListCodec` for a new `AlignedListCodec`.
|
||||
* It also changes the type of the list length `Codec` from `Int` to `Long`.
|
||||
* Due to type erasure, this method can not be overloaded for both `Codec[Int]` and `Codec[Long]`.
|
||||
* The compiler would resolve both internally into type `Codec[T]` and their function definitions would be identical.
|
||||
* For the purposes of use, `longL(n)` will cast to an `Int` for the same acceptable values of `n` as in `uintL(n)`.
|
||||
* This function is copied almost verbatim from its source, with exception of swapping the normal `ListCodec` for a new `AlignedListCodec`.
|
||||
* @param countCodec the codec that represents the prefixed size of the `List`
|
||||
* @param alignment the number of bits padded between the `List` size and the `List` contents
|
||||
* @param valueCodec a codec that describes each of the contents of the `List`
|
||||
|
|
@ -254,33 +209,34 @@ object PacketHelpers {
|
|||
*/
|
||||
def listOfNAligned[A](countCodec : Codec[Long], alignment : Int, valueCodec : Codec[A]) : Codec[List[A]] = {
|
||||
countCodec.
|
||||
flatZip {
|
||||
count =>
|
||||
new AlignedListCodec(countCodec, valueCodec, alignment, Some(count))
|
||||
}.
|
||||
narrow[List[A]] (
|
||||
{
|
||||
case (cnt, xs) =>
|
||||
if(xs.size == cnt)
|
||||
Attempt.successful(xs)
|
||||
else
|
||||
Attempt.failure(Err(s"Insufficient number of elements: decoded ${xs.size} but should have decoded $cnt"))
|
||||
},
|
||||
{
|
||||
xs =>
|
||||
(xs.size, xs)
|
||||
}
|
||||
).
|
||||
flatZip { count => new AlignedListCodec(countCodec, valueCodec, alignment, Some(count)) }.
|
||||
narrow[List[A]]({ case (cnt, xs) =>
|
||||
if(xs.size == cnt) Attempt.successful(xs)
|
||||
else Attempt.failure(Err(s"Insufficient number of elements: decoded ${xs.size} but should have decoded $cnt"))
|
||||
}, xs => (xs.size, xs)).
|
||||
withToString(s"listOfN($countCodec, $valueCodec)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Codec that encodes/decodes a list of `n` elements, where `n` is known at compile time.<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>
|
||||
* This class is copied almost verbatim from its source, with two major modifications.
|
||||
* First, heavy modifications to its `encode` process account for the alignment value.
|
||||
* Second, the length field is parsed as a `Codec[Long]` value and type conversion is accounted for at several points.
|
||||
* This class is copied almost verbatim from its source, with only heavy modifications to its `encode` process.
|
||||
* @param countCodec the codec that represents the prefixed size of the `List`
|
||||
* @param valueCodec a codec that describes each of the contents of the `List`
|
||||
* @param alignment the number of bits padded between the `List` size and the `List` contents (on successful)
|
||||
|
|
@ -294,18 +250,19 @@ private class AlignedListCodec[A](countCodec : Codec[Long], valueCodec: Codec[A]
|
|||
* <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.
|
||||
* The padding is rather heavy-handed and a completely different `BitVector` is returned if successful.
|
||||
* Performance hits for this complexity are not expected to be significant.
|
||||
* @param list the `List` to be encoded
|
||||
* @return the `BitVector` encoding, if successful
|
||||
*/
|
||||
override def encode(list : List[A]) : Attempt[BitVector] = {
|
||||
var solve : Attempt[BitVector] = Encoder.encodeSeq(valueCodec)(list)
|
||||
val solve : Attempt[BitVector] = Encoder.encodeSeq(valueCodec)(list)
|
||||
if(alignment > 0) {
|
||||
solve match {
|
||||
case Attempt.Successful(vector) =>
|
||||
val countCodecSize : Long = countCodec.sizeBound.lowerBound
|
||||
solve = Attempt.successful(vector.take(countCodecSize) ++ BitVector.fill(alignment)(false) ++ vector.drop(countCodecSize))
|
||||
return Attempt.successful(vector.take(countCodecSize) ++ BitVector.fill(alignment)(false) ++ vector.drop(countCodecSize))
|
||||
case _ =>
|
||||
solve = Attempt.failure(Err("failed to create a list"))
|
||||
return Attempt.failure(Err("failed to create a list"))
|
||||
}
|
||||
}
|
||||
solve
|
||||
|
|
@ -316,26 +273,27 @@ private class AlignedListCodec[A](countCodec : Codec[Long], valueCodec: Codec[A]
|
|||
* @param buffer the encoded bits in the `List`, preceded by the alignment bits
|
||||
* @return the decoded `List`
|
||||
*/
|
||||
override def decode(buffer: BitVector) = {
|
||||
def decode(buffer: BitVector) = {
|
||||
val lim = Option( if(limit.isDefined) limit.get.asInstanceOf[Int] else 0 ) //TODO potentially unsafe size conversion
|
||||
Decoder.decodeCollect[List, A](valueCodec, lim)(buffer.drop(alignment))
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of the encoded `List`.
|
||||
* The size of the encoded `List`.<br>
|
||||
* <br>
|
||||
* Unchanged from original.
|
||||
* @return the size as calculated by the size of each element for each element
|
||||
*/
|
||||
override def sizeBound = limit match {
|
||||
case None =>
|
||||
SizeBound.unknown
|
||||
case Some(lim : Long) =>
|
||||
valueCodec.sizeBound * lim
|
||||
def sizeBound = limit match {
|
||||
case None => SizeBound.unknown
|
||||
case Some(lim) => valueCodec.sizeBound * lim
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a `String` representation of this `List`.
|
||||
* Get a `String` representation of this `List`.<br>
|
||||
* <br>
|
||||
* Unchanged from original.
|
||||
* @return the `String` representation
|
||||
*/
|
||||
override def toString = s"list($valueCodec)"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +1,239 @@
|
|||
// Copyright (c) 2016 PSForever.net to present
|
||||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass, StreamBitSize}
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||
import scodec.bits._
|
||||
import scodec.{Attempt, Codec, Err}
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, Codec, DecodeResult, Err}
|
||||
import scodec.codecs._
|
||||
import shapeless._
|
||||
import shapeless.{::, HNil}
|
||||
|
||||
case class ObjectCreateMessageParent(guid : Int, slot : Int)
|
||||
/**
|
||||
* The parent information of a created object.<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,
|
||||
guid : Int,
|
||||
parentInfo : Option[ObjectCreateMessageParent],
|
||||
stream : BitVector
|
||||
)
|
||||
/**
|
||||
* Communicate with the client that a certain object with certain properties is to be created.
|
||||
* The object may also have primitive assignment (attachment) properties.<br>
|
||||
* <br>
|
||||
* 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 {
|
||||
|
||||
def opcode = GamePacketOpcode.ObjectCreateMessage
|
||||
def encode = ObjectCreateMessage.encode(this)
|
||||
}
|
||||
|
||||
object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
|
||||
/**
|
||||
* An abbreviated constructor for creating `ObjectCreateMessages`, ignoring the optional aspect of some fields.
|
||||
* @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding
|
||||
* @param objectClass the code for the type of object being constructed
|
||||
* @param guid the GUID this object will be assigned
|
||||
* @param parentInfo the relationship between this object and another object (its parent)
|
||||
* @param data the data used to construct this type of object
|
||||
* @return an ObjectCreateMessage
|
||||
*/
|
||||
def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage =
|
||||
ObjectCreateMessage(streamLength, objectClass, guid, Some(parentInfo), Some(data))
|
||||
|
||||
type Pattern = Int :: Int :: Option[ObjectCreateMessageParent] :: HNil
|
||||
type ChoicePattern = Either[Pattern, Pattern]
|
||||
/**
|
||||
* An abbreviated constructor for creating `ObjectCreateMessages`, ignoring `parentInfo`.
|
||||
* @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding
|
||||
* @param objectClass the code for the type of object being constructed
|
||||
* @param guid the GUID this object will be assigned
|
||||
* @param data the data used to construct this type of object
|
||||
* @return an ObjectCreateMessage
|
||||
*/
|
||||
def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateMessage =
|
||||
ObjectCreateMessage(streamLength, objectClass, guid, None, Some(data))
|
||||
|
||||
val noParent : Codec[Pattern] = (("object_class" | uintL(0xb)) ::
|
||||
("guid" | uint16L)).xmap[Pattern]( {
|
||||
case cls :: guid :: HNil => cls :: guid :: None :: HNil
|
||||
}, {
|
||||
case cls :: guid :: None :: HNil => cls :: guid :: HNil
|
||||
})
|
||||
val parent : Codec[Pattern] = (("parent_guid" | uint16L) ::
|
||||
("object_class" | uintL(0xb)) ::
|
||||
("guid" | uint16L) ::
|
||||
("parent_slot_index" | PacketHelpers.encodedStringSize)).xmap[Pattern]( {
|
||||
case pguid :: cls :: guid :: slot :: HNil =>
|
||||
cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil
|
||||
}, {
|
||||
case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil =>
|
||||
pguid :: cls :: guid :: slot :: HNil
|
||||
})
|
||||
type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil
|
||||
type outPattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil
|
||||
/**
|
||||
* Codec for formatting around the lack of parent data in the stream.
|
||||
*/
|
||||
private val noParent : Codec[Pattern] = (
|
||||
("objectClass" | uintL(0xb)) :: //11u
|
||||
("guid" | PlanetSideGUID.codec) //16u
|
||||
).xmap[Pattern](
|
||||
{
|
||||
case cls :: guid :: HNil =>
|
||||
cls :: guid :: None :: HNil
|
||||
}, {
|
||||
case cls :: guid :: None :: HNil =>
|
||||
cls :: guid :: HNil
|
||||
}
|
||||
)
|
||||
/**
|
||||
* Codec for reading and formatting parent data from the stream.
|
||||
*/
|
||||
private val parent : Codec[Pattern] = (
|
||||
("parentGuid" | PlanetSideGUID.codec) :: //16u
|
||||
("objectClass" | uintL(0xb)) :: //11u
|
||||
("guid" | PlanetSideGUID.codec) :: //16u
|
||||
("parentSlotIndex" | PacketHelpers.encodedStringSize) //8u or 16u
|
||||
).xmap[Pattern](
|
||||
{
|
||||
case pguid :: cls :: guid :: slot :: HNil =>
|
||||
cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil
|
||||
}, {
|
||||
case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil =>
|
||||
pguid :: cls :: guid :: slot :: HNil
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Take bit data and transform it into an object that expresses the important information of a game piece.
|
||||
* This function is fail-safe because it catches errors involving bad parsing of the bitstream data.
|
||||
* Generally, the `Exception` messages themselves are not useful here.
|
||||
* The important parts are what the packet thought the object class should be and what it actually processed.
|
||||
* @param objectClass the code for the type of object being constructed
|
||||
* @param data the bitstream data
|
||||
* @return the optional constructed object
|
||||
*/
|
||||
private def decodeData(objectClass : Int, data : BitVector) : Option[ConstructorData] = {
|
||||
var out : Option[ConstructorData] = None
|
||||
try {
|
||||
val outOpt : Option[DecodeResult[_]] = ObjectClass.selectDataCodec(objectClass).decode(data).toOption
|
||||
if(outOpt.isDefined)
|
||||
out = outOpt.get.value.asInstanceOf[ConstructorData.genericPattern]
|
||||
}
|
||||
catch {
|
||||
case ex : Exception =>
|
||||
//catch and release, any sort of parse error
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/**
|
||||
* Take the important information of a game piece and transform it into bit data.
|
||||
* This function is fail-safe because it catches errors involving bad parsing of the object data.
|
||||
* Generally, the `Exception` messages themselves are not useful here.
|
||||
* @param objClass the code for the type of object being deconstructed
|
||||
* @param obj the object data
|
||||
* @return the bitstream data
|
||||
*/
|
||||
private def encodeData(objClass : Int, obj : ConstructorData) : BitVector = {
|
||||
var out = BitVector.empty
|
||||
try {
|
||||
val outOpt : Option[BitVector] = ObjectClass.selectDataCodec(objClass).encode(Some(obj.asInstanceOf[ConstructorData])).toOption
|
||||
if(outOpt.isDefined)
|
||||
out = outOpt.get
|
||||
}
|
||||
catch {
|
||||
case ex : Exception =>
|
||||
//catch and release, any sort of parse error
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the stream length in number of bits by factoring in the whole message in two portions.
|
||||
* This process automates for: object encoding.<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] = (
|
||||
("stream_length" | uint32L) :: (either(bool, parent, noParent).exmap[Pattern]( {
|
||||
case Left(a :: b :: Some(c) :: HNil) => Attempt.successful(a :: b :: Some(c) :: HNil)
|
||||
case Right(a :: b :: None :: HNil) => Attempt.successful(a :: b :: None :: HNil)
|
||||
// failure cases
|
||||
case Left(a :: b :: None :: HNil) => Attempt.failure(Err("expected parent structure"))
|
||||
case Right(a :: b :: Some(c) :: HNil) => Attempt.failure(Err("got unexpected parent structure"))
|
||||
}, {
|
||||
case a :: b :: Some(c) :: HNil => Attempt.successful(Left(a :: b :: Some(c) :: HNil))
|
||||
case a :: b :: None :: HNil => Attempt.successful(Right(a :: b :: None :: HNil))
|
||||
}) :+ ("rest" | bits) )
|
||||
).as[ObjectCreateMessage]
|
||||
("streamLength" | uint32L) ::
|
||||
(either(bool, parent, noParent).exmap[Pattern] (
|
||||
{
|
||||
case Left(a :: b :: Some(c) :: HNil) =>
|
||||
Attempt.successful(a :: b :: Some(c) :: HNil) //true, _, _, Some(c)
|
||||
case Right(a :: b :: None :: HNil) =>
|
||||
Attempt.successful(a :: b :: None :: HNil) //false, _, _, None
|
||||
// failure cases
|
||||
case Left(a :: b :: None :: HNil) =>
|
||||
Attempt.failure(Err("missing parent structure")) //true, _, _, None
|
||||
case Right(a :: b :: Some(c) :: HNil) =>
|
||||
Attempt.failure(Err("unexpected parent structure")) //false, _, _, Some(c)
|
||||
}, {
|
||||
case a :: b :: Some(c) :: HNil =>
|
||||
Attempt.successful(Left(a :: b :: Some(c) :: HNil))
|
||||
case a :: b :: None :: HNil =>
|
||||
Attempt.successful(Right(a :: b :: None :: HNil))
|
||||
}
|
||||
) :+
|
||||
("data" | bits)) //greed is good
|
||||
).exmap[outPattern] (
|
||||
{
|
||||
case _ :: _ :: _ :: _ :: BitVector.empty :: HNil =>
|
||||
Attempt.failure(Err("no data to decode"))
|
||||
case len :: cls :: guid :: par :: data :: HNil =>
|
||||
Attempt.successful(len :: cls :: guid :: par :: decodeData(cls, data) :: HNil)
|
||||
},
|
||||
{
|
||||
case _ :: _ :: _ :: _ :: None :: HNil =>
|
||||
Attempt.failure(Err("no object to encode"))
|
||||
case _ :: cls :: guid :: par :: Some(obj) :: HNil =>
|
||||
Attempt.successful(streamLen(par, obj) :: cls :: guid :: par :: encodeData(cls, obj) :: HNil)
|
||||
}
|
||||
).xmap[ObjectCreateMessage] (
|
||||
{
|
||||
case len :: cls :: guid :: par :: obj :: HNil =>
|
||||
ObjectCreateMessage(len, cls, guid, par, obj)
|
||||
},
|
||||
{
|
||||
case ObjectCreateMessage(len, cls, guid, par, obj) =>
|
||||
len :: cls :: guid :: par :: obj :: HNil
|
||||
}
|
||||
).as[ObjectCreateMessage]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
* ` MALE FEMALE`<br>
|
||||
* `0 - no voice no voice`<br>
|
||||
* `1 - male_1 female_1`<br>
|
||||
* `2 - male_2 female_2`<br>
|
||||
* `3 - male_3 female_3`<br>
|
||||
* `4 - male_4 female_4`<br>
|
||||
* `5 - male_5 female_5`<br>
|
||||
* `6 - female_1 no voice`<br>
|
||||
* `7 - female_2 no voice`
|
||||
* @param pos the position of the character in the world environment (in three coordinates)
|
||||
* @param objYaw the angle with respect to the horizon towards which the object's front is facing;
|
||||
* every `0x1` is 2.813 degrees counter clockwise from North;
|
||||
* every `0x10` is 45-degrees;
|
||||
* it wraps at `0x0` == `0x80` == North
|
||||
* (note: references the avatar as a game object?)
|
||||
* @param faction the empire to which the avatar belongs;
|
||||
* the value scale is different from `PlanetSideEmpire`
|
||||
* @param bops whether or not this avatar is enrolled in Black OPs
|
||||
* @param unk1 na;
|
||||
* defaults to 4
|
||||
* @param name the wide character name of the avatar, minimum of two characters
|
||||
* @param exosuit the type of exosuit the avatar will be depicted in;
|
||||
* for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits
|
||||
* @param sex whether the avatar is male or female
|
||||
* @param face1 the avatar's face, as by column number on the character creation screen
|
||||
* @param face2 the avatar's face, as by row number on the character creation screen
|
||||
* @param voice the avatar's voice selection
|
||||
* @param unk2 na
|
||||
* @param unk3 na;
|
||||
* can be missing from the stream under certain conditions;
|
||||
* see next
|
||||
* @param unk4 na;
|
||||
* can be missing from the stream under certain conditions;
|
||||
* see previous
|
||||
* @param unk5 na;
|
||||
* defaults to `0x8080`
|
||||
* @param unk6 na;
|
||||
* defaults to `0xFFFF`;
|
||||
* may be `0x0`
|
||||
* @param unk7 na;
|
||||
* defaults to 2
|
||||
* @param viewPitch the angle with respect to the sky and the ground towards which the avatar is looking;
|
||||
* only supports downwards view angles;
|
||||
* `0x0` is forwards-facing;
|
||||
* `0x20` to `0xFF` is downwards-facing
|
||||
* @param viewYaw the angle with respect to the horizon towards which the avatar is looking;
|
||||
* every `0x1` is 2.813 degrees counter clockwise from North;
|
||||
* every `0x10` is 45-degrees;
|
||||
* it wraps at `0x0` == `0x80` == North
|
||||
* @param unk8 na
|
||||
* @param ribbons the four merit commendation ribbon medals
|
||||
*/
|
||||
final case class CharacterAppearanceData(pos : Vector3,
|
||||
objYaw : Int,
|
||||
faction : Int,
|
||||
bops : Boolean,
|
||||
unk1 : Int,
|
||||
name : String,
|
||||
exosuit : Int,
|
||||
sex : Int,
|
||||
face1 : Int,
|
||||
face2 : Int,
|
||||
voice : Int,
|
||||
unk2 : Int,
|
||||
unk3 : Int,
|
||||
unk4 : Int,
|
||||
unk5 : Int,
|
||||
unk6 : Int,
|
||||
unk7 : Int,
|
||||
viewPitch : Int,
|
||||
viewYaw : Int,
|
||||
unk8 : Int,
|
||||
ribbons : RibbonBars) extends StreamBitSize {
|
||||
/**
|
||||
* Performs a "sizeof()" analysis of the given object.
|
||||
* @see ConstructorData.bitsize
|
||||
* @return the number of bits necessary to represent this object
|
||||
*/
|
||||
override def bitsize : Long = {
|
||||
//TODO ongoing analysis, this value will be subject to change
|
||||
430L + CharacterData.stringBitSize(name, 16) + CharacterAppearanceData.namePadding
|
||||
}
|
||||
}
|
||||
|
||||
object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
|
||||
/**
|
||||
* Get the padding of the avatar's name.
|
||||
* The padding will always be a number 0-7.
|
||||
* @return the pad length in bits
|
||||
*/
|
||||
private def namePadding : Int = {
|
||||
//TODO the parameters for this function are not correct
|
||||
//TODO the proper padding length should reflect all variability in the substream prior to this point
|
||||
4
|
||||
}
|
||||
|
||||
implicit val codec : Codec[CharacterAppearanceData] = (
|
||||
("pos" | Vector3.codec_pos) ::
|
||||
ignore(16) ::
|
||||
("objYaw" | uint8L) ::
|
||||
ignore(1) ::
|
||||
("faction" | uint2L) ::
|
||||
("bops" | bool) ::
|
||||
("unk1" | uint4L) ::
|
||||
ignore(16) ::
|
||||
("name" | PacketHelpers.encodedWideStringAligned( namePadding )) ::
|
||||
("exosuit" | uintL(3)) ::
|
||||
ignore(2) ::
|
||||
("sex" | uint2L) ::
|
||||
("face1" | uint4L) ::
|
||||
("face2" | uint4L) ::
|
||||
("voice" | uintL(3)) ::
|
||||
("unk2" | uint2L) ::
|
||||
ignore(4) ::
|
||||
("unk3" | uint8L) ::
|
||||
("unk4" | uint8L) ::
|
||||
("unk5" | uint16L) ::
|
||||
ignore(42) ::
|
||||
("unk6" | uint16L) ::
|
||||
ignore(30) ::
|
||||
("unk7" | uint4L) ::
|
||||
ignore(24) ::
|
||||
("viewPitch" | uint8L) ::
|
||||
("viewYaw" | uint8L) ::
|
||||
("unk8" | uint4L) ::
|
||||
ignore(6) ::
|
||||
("ribbons" | RibbonBars.codec)
|
||||
).as[CharacterAppearanceData]
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the avatar portion of `ObjectCreateMessage` packet data.<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"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -4,8 +4,9 @@ import java.net.{InetAddress, InetSocketAddress}
|
|||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.packet.game.objectcreate.{InventoryItem, _}
|
||||
import net.psforever.types._
|
||||
import scodec.Attempt
|
||||
import scodec.{Attempt, Err}
|
||||
import scodec.Attempt.Successful
|
||||
import scodec.bits._
|
||||
|
||||
|
|
@ -254,25 +255,320 @@ class GamePacketTest extends Specification {
|
|||
}
|
||||
|
||||
"ObjectCreateMessage" should {
|
||||
val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 "
|
||||
val packet2 = hex"18 17 74 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45"
|
||||
val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 " //fake data?
|
||||
val packet2 = hex"18 F8 00 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" //fake data
|
||||
//val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16)
|
||||
var string_inventoryItem = hex"46 04 C0 08 08 80 00 00 20 00 0C 04 10 29 A0 10 19 00 00 04 00 00"
|
||||
val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000"
|
||||
val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000"
|
||||
val string_punisher = hex"18 27010000 2580 612 a706 82 080000020000c08 1c13a0d01900000780 13a4701a072000000800"
|
||||
val string_rek = hex"18 97000000 2580 6C2 9F05 81 48000002000080000"
|
||||
val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00"
|
||||
|
||||
"decode" in {
|
||||
"decode (2)" in {
|
||||
//an invalid bit representation will fail to turn into an object
|
||||
PacketCoding.DecodePacket(packet2).require match {
|
||||
case obj @ ObjectCreateMessage(len, cls, guid, parent, rest) =>
|
||||
val manualRest = packet2.bits.drop(32 + 1 + 0xb + 16)
|
||||
len === 29719
|
||||
cls === 121
|
||||
guid === 2497
|
||||
rest === manualRest
|
||||
parent === None
|
||||
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
|
||||
len mustEqual 248
|
||||
cls mustEqual 121
|
||||
guid mustEqual PlanetSideGUID(2497)
|
||||
parent mustEqual None
|
||||
data.isDefined mustEqual false
|
||||
case default =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode" in {
|
||||
ok
|
||||
"decode (9mm)" in {
|
||||
PacketCoding.DecodePacket(string_9mm).require match {
|
||||
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
|
||||
len mustEqual 124
|
||||
cls mustEqual 28
|
||||
guid mustEqual PlanetSideGUID(1280)
|
||||
parent.isDefined mustEqual true
|
||||
parent.get.guid mustEqual PlanetSideGUID(75)
|
||||
parent.get.slot mustEqual 33
|
||||
data.isDefined mustEqual true
|
||||
data.get.asInstanceOf[AmmoBoxData].magazine mustEqual 50
|
||||
case default =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode (gauss)" in {
|
||||
PacketCoding.DecodePacket(string_gauss).require match {
|
||||
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
|
||||
len mustEqual 220
|
||||
cls mustEqual 345
|
||||
guid mustEqual PlanetSideGUID(1465)
|
||||
parent.isDefined mustEqual true
|
||||
parent.get.guid mustEqual PlanetSideGUID(75)
|
||||
parent.get.slot mustEqual 2
|
||||
data.isDefined mustEqual true
|
||||
val obj_wep = data.get.asInstanceOf[WeaponData]
|
||||
obj_wep.unk mustEqual 4
|
||||
val obj_ammo = obj_wep.ammo
|
||||
obj_ammo.objectClass mustEqual 28
|
||||
obj_ammo.guid mustEqual PlanetSideGUID(1286)
|
||||
obj_ammo.parentSlot mustEqual 0
|
||||
obj_ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30
|
||||
case default =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode (punisher)" in {
|
||||
PacketCoding.DecodePacket(string_punisher).require match {
|
||||
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
|
||||
len mustEqual 295
|
||||
cls mustEqual 706
|
||||
guid mustEqual PlanetSideGUID(1703)
|
||||
parent.isDefined mustEqual true
|
||||
parent.get.guid mustEqual PlanetSideGUID(75)
|
||||
parent.get.slot mustEqual 2
|
||||
data.isDefined mustEqual true
|
||||
val obj_wep = data.get.asInstanceOf[ConcurrentFeedWeaponData]
|
||||
obj_wep.unk mustEqual 0
|
||||
val obj_ammo = obj_wep.ammo
|
||||
obj_ammo.size mustEqual 2
|
||||
obj_ammo.head.objectClass mustEqual 28
|
||||
obj_ammo.head.guid mustEqual PlanetSideGUID(1693)
|
||||
obj_ammo.head.parentSlot mustEqual 0
|
||||
obj_ammo.head.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30
|
||||
obj_ammo(1).objectClass mustEqual 413
|
||||
obj_ammo(1).guid mustEqual PlanetSideGUID(1564)
|
||||
obj_ammo(1).parentSlot mustEqual 1
|
||||
obj_ammo(1).obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode (rek)" in {
|
||||
PacketCoding.DecodePacket(string_rek).require match {
|
||||
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
|
||||
len mustEqual 151
|
||||
cls mustEqual 0x2D8
|
||||
guid mustEqual PlanetSideGUID(1439)
|
||||
parent.isDefined mustEqual true
|
||||
parent.get.guid mustEqual PlanetSideGUID(75)
|
||||
parent.get.slot mustEqual 1
|
||||
data.isDefined mustEqual true
|
||||
data.get.asInstanceOf[REKData].unk mustEqual 4
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode (character)" in {
|
||||
PacketCoding.DecodePacket(string_testchar).require match {
|
||||
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
|
||||
len mustEqual 3159
|
||||
cls mustEqual 0x79
|
||||
guid mustEqual PlanetSideGUID(75)
|
||||
parent.isDefined mustEqual false
|
||||
data.isDefined mustEqual true
|
||||
|
||||
val char = data.get.asInstanceOf[CharacterData]
|
||||
char.appearance.pos.x mustEqual 3674.8438f
|
||||
char.appearance.pos.y mustEqual 2726.789f
|
||||
char.appearance.pos.z mustEqual 91.15625f
|
||||
char.appearance.objYaw mustEqual 19
|
||||
char.appearance.faction mustEqual 2 //vs
|
||||
char.appearance.bops mustEqual false
|
||||
char.appearance.unk1 mustEqual 4
|
||||
char.appearance.name mustEqual "IlllIIIlllIlIllIlllIllI"
|
||||
char.appearance.exosuit mustEqual 4 //standard
|
||||
char.appearance.sex mustEqual 2 //female
|
||||
char.appearance.face1 mustEqual 2
|
||||
char.appearance.face2 mustEqual 9
|
||||
char.appearance.voice mustEqual 1 //female 1
|
||||
char.appearance.unk2 mustEqual 3
|
||||
char.appearance.unk3 mustEqual 118
|
||||
char.appearance.unk4 mustEqual 30
|
||||
char.appearance.unk5 mustEqual 0x8080
|
||||
char.appearance.unk6 mustEqual 0xFFFF
|
||||
char.appearance.unk7 mustEqual 2
|
||||
char.appearance.viewPitch mustEqual 0xFF
|
||||
char.appearance.viewYaw mustEqual 0x6A
|
||||
char.appearance.unk8 mustEqual 7
|
||||
char.appearance.ribbons.upper mustEqual 0xFFFFFFFFL //none
|
||||
char.appearance.ribbons.middle mustEqual 0xFFFFFFFFL //none
|
||||
char.appearance.ribbons.lower mustEqual 0xFFFFFFFFL //none
|
||||
char.appearance.ribbons.tos mustEqual 0xFFFFFFFFL //none
|
||||
char.healthMax mustEqual 100
|
||||
char.health mustEqual 100
|
||||
char.armor mustEqual 50 //standard exosuit value
|
||||
char.unk1 mustEqual 1
|
||||
char.unk2 mustEqual 7
|
||||
char.unk3 mustEqual 7
|
||||
char.staminaMax mustEqual 100
|
||||
char.stamina mustEqual 100
|
||||
char.unk4 mustEqual 28
|
||||
char.unk5 mustEqual 4
|
||||
char.unk6 mustEqual 44
|
||||
char.unk7 mustEqual 84
|
||||
char.unk8 mustEqual 104
|
||||
char.unk9 mustEqual 1900
|
||||
char.firstTimeEvents.size mustEqual 4
|
||||
char.firstTimeEvents.head mustEqual "xpe_sanctuary_help"
|
||||
char.firstTimeEvents(1) mustEqual "xpe_th_firemodes"
|
||||
char.firstTimeEvents(2) mustEqual "used_beamer"
|
||||
char.firstTimeEvents(3) mustEqual "map13"
|
||||
char.tutorials.size mustEqual 0
|
||||
char.inventory.unk1 mustEqual true
|
||||
char.inventory.unk2 mustEqual false
|
||||
char.inventory.contents.size mustEqual 10
|
||||
val inventory = char.inventory.contents
|
||||
//0
|
||||
inventory.head.item.objectClass mustEqual 0x8C //beamer
|
||||
inventory.head.item.guid mustEqual PlanetSideGUID(76)
|
||||
inventory.head.item.parentSlot mustEqual 0
|
||||
var wep = inventory.head.item.obj.asInstanceOf[WeaponData]
|
||||
wep.ammo.objectClass mustEqual 0x110 //plasma
|
||||
wep.ammo.guid mustEqual PlanetSideGUID(77)
|
||||
wep.ammo.parentSlot mustEqual 0
|
||||
wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 16
|
||||
//1
|
||||
inventory(1).item.objectClass mustEqual 0x34D //suppressor
|
||||
inventory(1).item.guid mustEqual PlanetSideGUID(78)
|
||||
inventory(1).item.parentSlot mustEqual 2
|
||||
wep = inventory(1).item.obj.asInstanceOf[WeaponData]
|
||||
wep.ammo.objectClass mustEqual 0x1C //9mm
|
||||
wep.ammo.guid mustEqual PlanetSideGUID(79)
|
||||
wep.ammo.parentSlot mustEqual 0
|
||||
wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 25
|
||||
//2
|
||||
inventory(2).item.objectClass mustEqual 0x144 //force blade
|
||||
inventory(2).item.guid mustEqual PlanetSideGUID(80)
|
||||
inventory(2).item.parentSlot mustEqual 4
|
||||
wep = inventory(2).item.obj.asInstanceOf[WeaponData]
|
||||
wep.ammo.objectClass mustEqual 0x21C //force blade ammo
|
||||
wep.ammo.guid mustEqual PlanetSideGUID(81)
|
||||
wep.ammo.parentSlot mustEqual 0
|
||||
wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1
|
||||
//3
|
||||
inventory(3).item.objectClass mustEqual 0x1C8 //thing
|
||||
inventory(3).item.guid mustEqual PlanetSideGUID(82)
|
||||
inventory(3).item.parentSlot mustEqual 5
|
||||
inventory(3).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1
|
||||
//4
|
||||
inventory(4).item.objectClass mustEqual 0x1C //9mm
|
||||
inventory(4).item.guid mustEqual PlanetSideGUID(83)
|
||||
inventory(4).item.parentSlot mustEqual 6
|
||||
inventory(4).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50
|
||||
//5
|
||||
inventory(5).item.objectClass mustEqual 0x1C //9mm
|
||||
inventory(5).item.guid mustEqual PlanetSideGUID(84)
|
||||
inventory(5).item.parentSlot mustEqual 9
|
||||
inventory(5).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50
|
||||
//6
|
||||
inventory(6).item.objectClass mustEqual 0x1C //9mm
|
||||
inventory(6).item.guid mustEqual PlanetSideGUID(85)
|
||||
inventory(6).item.parentSlot mustEqual 12
|
||||
inventory(6).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50
|
||||
//7
|
||||
inventory(7).item.objectClass mustEqual 0x1D //9mm ap
|
||||
inventory(7).item.guid mustEqual PlanetSideGUID(86)
|
||||
inventory(7).item.parentSlot mustEqual 33
|
||||
inventory(7).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50
|
||||
//8
|
||||
inventory(8).item.objectClass mustEqual 0x110 //plasma
|
||||
inventory(8).item.guid mustEqual PlanetSideGUID(87)
|
||||
inventory(8).item.parentSlot mustEqual 36
|
||||
inventory(8).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50
|
||||
//9
|
||||
inventory(9).item.objectClass mustEqual 0x2D8 //rek
|
||||
inventory(9).item.guid mustEqual PlanetSideGUID(88)
|
||||
inventory(9).item.parentSlot mustEqual 39
|
||||
//the rek has data but none worth testing here
|
||||
case default =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode (2)" in {
|
||||
//the lack of an object will fail to turn into a bad bitstream
|
||||
val msg = ObjectCreateMessage(0, 121, PlanetSideGUID(2497), None, None)
|
||||
PacketCoding.EncodePacket(msg).isFailure mustEqual true
|
||||
}
|
||||
|
||||
"encode (9mm)" in {
|
||||
val obj = AmmoBoxData(50)
|
||||
val msg = ObjectCreateMessage(0, 28, PlanetSideGUID(1280), ObjectCreateMessageParent(PlanetSideGUID(75), 33), obj)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_9mm
|
||||
}
|
||||
|
||||
"encode (gauss)" in {
|
||||
val obj = WeaponData(4, 28, PlanetSideGUID(1286), 0, AmmoBoxData(30))
|
||||
val msg = ObjectCreateMessage(0, 345, PlanetSideGUID(1465), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_gauss
|
||||
}
|
||||
|
||||
"encode (punisher)" in {
|
||||
val obj = ConcurrentFeedWeaponData(0, AmmoBoxData(28, PlanetSideGUID(1693), 0, AmmoBoxData(30)) :: AmmoBoxData(413, PlanetSideGUID(1564), 1, AmmoBoxData(1)) :: Nil)
|
||||
val msg = ObjectCreateMessage(0, 706, PlanetSideGUID(1703), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj)
|
||||
var pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_punisher
|
||||
}
|
||||
|
||||
"encode (rek)" in {
|
||||
val obj = REKData(4)
|
||||
val msg = ObjectCreateMessage(0, 0x2D8, PlanetSideGUID(1439), ObjectCreateMessageParent(PlanetSideGUID(75), 1), obj)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_rek
|
||||
}
|
||||
|
||||
"encode (character)" in {
|
||||
val app = CharacterAppearanceData(
|
||||
Vector3(3674.8438f, 2726.789f, 91.15625f),
|
||||
19,
|
||||
2,
|
||||
false,
|
||||
4,
|
||||
"IlllIIIlllIlIllIlllIllI",
|
||||
4,
|
||||
2,
|
||||
2,9,
|
||||
1,
|
||||
3, 118,30, 0x8080, 0xFFFF, 2,
|
||||
255, 106, 7,
|
||||
RibbonBars()
|
||||
)
|
||||
val inv = InventoryItem(0x8C, PlanetSideGUID(76), 0, WeaponData(8, 0x110, PlanetSideGUID(77), 0, AmmoBoxData(16))) ::
|
||||
InventoryItem(0x34D, PlanetSideGUID(78), 2, WeaponData(8, 0x1C, PlanetSideGUID(79), 0, AmmoBoxData(25))) ::
|
||||
InventoryItem(0x144, PlanetSideGUID(80), 4, WeaponData(8, 0x21C, PlanetSideGUID(81), 0, AmmoBoxData(1))) ::
|
||||
InventoryItem(0x1C8, PlanetSideGUID(82), 5, AmmoBoxData(1)) ::
|
||||
InventoryItem(0x1C, PlanetSideGUID(83), 6, AmmoBoxData(50)) ::
|
||||
InventoryItem(0x1C, PlanetSideGUID(84), 9, AmmoBoxData(50)) ::
|
||||
InventoryItem(0x1C, PlanetSideGUID(85), 12, AmmoBoxData(50)) ::
|
||||
InventoryItem(0x1D, PlanetSideGUID(86), 33, AmmoBoxData(50)) ::
|
||||
InventoryItem(0x110, PlanetSideGUID(87), 36, AmmoBoxData(50)) ::
|
||||
InventoryItem(0x2D8, PlanetSideGUID(88), 39, REKData(8)) ::
|
||||
Nil
|
||||
val obj = CharacterData(
|
||||
app,
|
||||
100, 100,
|
||||
50,
|
||||
1, 7, 7,
|
||||
100, 100,
|
||||
28, 4, 44, 84, 104, 1900,
|
||||
"xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil,
|
||||
List.empty,
|
||||
InventoryData(
|
||||
true, false, false, inv
|
||||
)
|
||||
)
|
||||
val msg = ObjectCreateMessage(0, 0x79, PlanetSideGUID(75), obj)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_testchar
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import scodec.Attempt.{Failure, Successful}
|
|||
import scodec.bits._
|
||||
import org.log4s.MDC
|
||||
import MDCContextAware.Implicits._
|
||||
import net.psforever.types.ChatMessageType
|
||||
import net.psforever.packet.game.objectcreate._
|
||||
import net.psforever.types.{ChatMessageType, Vector3}
|
||||
|
||||
class WorldSessionActor extends Actor with MDCContextAware {
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
|
@ -107,8 +108,49 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
}
|
||||
}
|
||||
|
||||
// XXX: hard coded ObjectCreateMessage
|
||||
val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00 "
|
||||
//val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00"
|
||||
//currently, the character's starting BEP is discarded due to unknown bit format
|
||||
val app = CharacterAppearanceData(
|
||||
Vector3(3674.8438f, 2726.789f, 91.15625f),
|
||||
19,
|
||||
2,
|
||||
false,
|
||||
4,
|
||||
"IlllIIIlllIlIllIlllIllI",
|
||||
4,
|
||||
2,
|
||||
2, 9,
|
||||
1,
|
||||
3, 118, 30, 0x8080, 0xFFFF, 2,
|
||||
255, 106, 7,
|
||||
RibbonBars()
|
||||
)
|
||||
val inv =
|
||||
InventoryItem(ObjectClass.BEAMER, PlanetSideGUID(76), 0, WeaponData(8, ObjectClass.ENERGY_CELL, PlanetSideGUID(77), 0, AmmoBoxData(16))) ::
|
||||
InventoryItem(ObjectClass.SUPPRESSOR, PlanetSideGUID(78), 2, WeaponData(8, ObjectClass.BULLETS_9MM, PlanetSideGUID(79), 0, AmmoBoxData(25))) ::
|
||||
InventoryItem(ObjectClass.FORCE_BLADE, PlanetSideGUID(80), 4, WeaponData(8, ObjectClass.FORCE_BLADE_AMMO, PlanetSideGUID(81), 0, AmmoBoxData(1))) ::
|
||||
InventoryItem(ObjectClass.SLOT_BLOCKER, PlanetSideGUID(82), 5, AmmoBoxData(1)) ::
|
||||
InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(83), 6, AmmoBoxData(50)) ::
|
||||
InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(84), 9, AmmoBoxData(50)) ::
|
||||
InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(85), 12, AmmoBoxData(50)) ::
|
||||
InventoryItem(ObjectClass.BULLETS_9MM_AP, PlanetSideGUID(86), 33, AmmoBoxData(50)) ::
|
||||
InventoryItem(ObjectClass.ENERGY_CELL, PlanetSideGUID(87), 36, AmmoBoxData(50)) ::
|
||||
InventoryItem(ObjectClass.REK, PlanetSideGUID(88), 39, REKData(8)) ::
|
||||
Nil
|
||||
val obj = CharacterData(
|
||||
app,
|
||||
100, 100,
|
||||
50,
|
||||
1, 7, 7,
|
||||
100, 100,
|
||||
28, 4, 44, 84, 104, 1900,
|
||||
"xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil,
|
||||
List.empty,
|
||||
InventoryData(
|
||||
true, false, false, inv
|
||||
)
|
||||
)
|
||||
val objectHex = ObjectCreateMessage(0, ObjectClass.AVATAR, PlanetSideGUID(75), obj)
|
||||
|
||||
def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match {
|
||||
case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) =>
|
||||
|
|
@ -118,7 +160,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
log.info(s"New world login to ${server} with Token:${token}. ${clientVersion}")
|
||||
|
||||
// ObjectCreateMessage
|
||||
sendRawResponse(objectHex)
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, objectHex))
|
||||
// XXX: hard coded message
|
||||
sendRawResponse(hex"14 0F 00 00 00 10 27 00 00 C1 D8 7A 02 4B 00 26 5C B0 80 00 ")
|
||||
|
||||
|
|
@ -132,14 +174,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
case CharacterRequestAction.Delete =>
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1))))
|
||||
case CharacterRequestAction.Select =>
|
||||
PacketCoding.DecodeGamePacket(objectHex).require match {
|
||||
objectHex match {
|
||||
case obj @ ObjectCreateMessage(len, cls, guid, _, _) =>
|
||||
log.debug("Object: " + obj)
|
||||
// LoadMapMessage 13714 in mossy .gcap
|
||||
// XXX: hardcoded shit
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0)))
|
||||
sendRawResponse(objectHex)
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, objectHex))
|
||||
|
||||
// These object_guids are specfic to VS Sanc
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C
|
||||
|
|
@ -170,8 +212,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
true, //Boosted spawn room pain field
|
||||
true))) //Boosted generator room pain field
|
||||
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(PlanetSideGUID(guid),0,0)))
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(PlanetSideGUID(guid), 1, 0, true, Shortcut.MEDKIT)))
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0)))
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)))
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
|
|
|||
Loading…
Reference in a new issue