Merge pull request #128 from Fate-JH/object-create-updates

Object Create Message Update #2
This commit is contained in:
Fate-JH 2017-04-30 00:19:36 -04:00 committed by GitHub
commit f1dfc20cb3
54 changed files with 5199 additions and 1476 deletions

View file

@ -345,9 +345,9 @@ object GamePacketOpcode extends Enumeration {
case 0x14 => game.CharacterInfoMessage.decode
case 0x15 => noDecoder(UnknownMessage21)
case 0x16 => game.BindPlayerMessage.decode
case 0x17 => noDecoder(ObjectCreateMessage_Duplicate)
case 0x17 => game.ObjectCreateMessage.decode
// 0x18
case 0x18 => game.ObjectCreateMessage.decode
case 0x18 => game.ObjectCreateDetailedMessage.decode
case 0x19 => game.ObjectDeleteMessage.decode
case 0x1a => game.PingMsg.decode
case 0x1b => noDecoder(VehicleStateMessage)

View file

@ -2,6 +2,7 @@
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.ExoSuitType
import scodec.Codec
import scodec.codecs._
@ -13,23 +14,21 @@ import scodec.codecs._
* Due to the way armor is handled internally, a player of one faction may not spawn in the exo-suit of another faction.
* That style of exo-suit is never available through this packet.
* As MAX units do not get their weapon by default, all the MAX values produce the same faction-appropriate mechanized exo-suit body visually.
* (The MAX weapons are supplied in subsequent packets.)
* (The MAX weapons are supplied in subsequent packets.)<br>
* <br>
* Mechanized Assault Subtypes:<br>
* `
* 0, 0 - Agile<br>
* 1, 0 - Reinforced<br>
* 2, 0 - MAX<br>
* 2, 1 - AI MAX<br>
* 2, 2 - AV MAX<br>
* 2, 3 - AA MAX<br>
* 3, 0 - Infiltration<br>
* 4, 0 - Standard
* 0 - na<br>
* 1 - AI MAX<br>
* 2 - AV MAX<br>
* 3 - AA MAX
* `
* @param player_guid the player
* @param armor the type of exo-suit
* @param subtype the exo-suit subtype, if any
*/
final case class ArmorChangedMessage(player_guid : PlanetSideGUID,
armor : Int,
armor : ExoSuitType.Value,
subtype : Int)
extends PlanetSideGamePacket {
type Packet = ArmorChangedMessage
@ -40,7 +39,7 @@ final case class ArmorChangedMessage(player_guid : PlanetSideGUID,
object ArmorChangedMessage extends Marshallable[ArmorChangedMessage] {
implicit val codec : Codec[ArmorChangedMessage] = (
("player_guid" | PlanetSideGUID.codec) ::
("armor" | uintL(3)) ::
("armor" | ExoSuitType.codec) ::
("subtype" | uintL(3))
).as[ArmorChangedMessage]
}

View file

@ -1,23 +1,11 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.GrenadeState
import scodec.Codec
import scodec.codecs._
/**
* An `Enumeration` of the kinds of states applicable to the grenade animation.
*/
object GrenadeState extends Enumeration {
type Type = Value
val UNK0,
PRIMED, //avatars and other depicted player characters
THROWN //avatars only
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
}
/**
* Report the state of the grenade throw animation for this player.
* The default state is "held at side," though the client's avatar never has to announce this.<br>
@ -25,12 +13,12 @@ object GrenadeState extends Enumeration {
* The throwing animation has a minor timing glitch.
* Causing another player to raise his arm will always result in that arm being lowered a few seconds later.
* This is as opposed to the client's avatar, who can seem to hold a grenade in the "prepare to throw" state indefinitely.
* If the avatar looks away from a player whose grenade arm is up ("prepare to throw"), however, when they look back at the player
* If the avatar looks away from a player whose grenade arm is up ("prepare to throw"), however, when they look back at the player,
* his grenade arm will occasionally have been lowered ("held at side") again before it would normally be lowered.<br>
* <br>
* A client will dispatch state '1' and state '2' for the avatar's actions.
* A client will only react temporarily for another character other than the avatar when the given a state '1'.
* If that internal state is not changed, however, that other character will not respond to any subsequent '1' state.
* A client will dispatch state 'Primed' and state 'Thrown' for the avatar's actions.
* A client will only react temporarily for another character other than the avatar when the given a state 'Primed'.
* If that internal state is not changed, however, that other character will not respond to any subsequent 'Primed' state.
* (This may also be a glitch.)<br>
* <br>
* States:<br>

View file

@ -2,19 +2,11 @@
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.PlanetSideEmpire
import net.psforever.types.{CharacterGender, PlanetSideEmpire}
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
object CharacterGender extends Enumeration(1) {
type Type = Value
val Male, Female = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L)
}
/**
* Is sent by the PlanetSide client on character selection completion.
*/

View file

@ -0,0 +1,113 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass, ObjectCreateBase, ObjectCreateMessageParent}
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.bits.BitVector
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* Communicate with the client that a certain object with certain properties is to be created.
* In general, `ObjectCreateMessage` and its counterpart `ObjectCreateDetailedMessage` should look similar.<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 type is essential for parsing the specific information passed by the `data` parameter.
* If the object does not have encoding information or is unknown, it will not translate between byte data and a game object.
* @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
*/
final case class ObjectCreateDetailedMessage(streamLength : Long,
objectClass : Int,
guid : PlanetSideGUID,
parentInfo : Option[ObjectCreateMessageParent],
data : Option[ConstructorData])
extends PlanetSideGamePacket {
type Packet = ObjectCreateDetailedMessage
def opcode = GamePacketOpcode.ObjectCreateMessage
def encode = ObjectCreateDetailedMessage.encode(this)
}
object ObjectCreateDetailedMessage extends Marshallable[ObjectCreateDetailedMessage] {
/**
* An abbreviated constructor for creating `ObjectCreateMessages`, ignoring the optional aspect of some fields.
* @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(objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateDetailedMessage =
ObjectCreateDetailedMessage(0L, objectClass, guid, Some(parentInfo), Some(data))
/**
* An abbreviated constructor for creating `ObjectCreateMessages`, ignoring `parentInfo`.
* @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(objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateDetailedMessage =
ObjectCreateDetailedMessage(0L, objectClass, guid, None, Some(data))
/**
* 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
* @see ObjectClass.selectDataCodec
*/
def encodeData(objClass : Int, obj : ConstructorData, getCodecFunc : (Int) => Codec[ConstructorData.genericPattern]) : BitVector = {
var out = BitVector.empty
try {
val outOpt : Option[BitVector] = getCodecFunc(objClass).encode(Some(obj.asInstanceOf[ConstructorData])).toOption
if(outOpt.isDefined)
out = outOpt.get
}
catch {
case _ : Exception =>
//catch and release, any sort of parse error
}
out
}
implicit val codec : Codec[ObjectCreateDetailedMessage] = ObjectCreateBase.baseCodec.exmap[ObjectCreateDetailedMessage] (
{
case _ :: _ :: _ :: _ :: BitVector.empty :: HNil =>
Attempt.failure(Err("no data to decode"))
case len :: cls :: guid :: par :: data :: HNil =>
val obj = ObjectCreateBase.decodeData(cls, data, ObjectClass.selectDataDetailedCodec)
Attempt.successful(ObjectCreateDetailedMessage(len, cls, guid, par, obj))
},
{
case ObjectCreateDetailedMessage(_ , _ , _, _, None) =>
Attempt.failure(Err("no object to encode"))
case ObjectCreateDetailedMessage(_, cls, guid, par, Some(obj)) =>
val len = ObjectCreateBase.streamLen(par, obj) //even if a stream length has been assigned, it can not be trusted during encoding
val bitvec = ObjectCreateBase.encodeData(cls, obj, ObjectClass.selectDataDetailedCodec)
Attempt.successful(len :: cls :: guid :: par :: bitvec :: HNil)
}
)
}

View file

@ -1,53 +1,50 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass, StreamBitSize}
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.packet.game.objectcreate._
import scodec.{Attempt, Codec, Err}
import scodec.bits.BitVector
import scodec.{Attempt, Codec, DecodeResult, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* 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)
/**
* 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>
* In general, `ObjectCreateMessage` and its counterpart `ObjectCreateDetailedMessage` should look similar.<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>
* `ObjectCreateMessage` is capable of creating every non-environmental object in the game through the use of encoding patterns.
* The objects produced by this packet generally do not always fully express all the complexities of the object class.
* With respect to a client's avatar, all of the items in his inventory are given thorough detail so that the client can account for their interaction.
* The "shallow" objects produced by this packet are not like that.
* They express only the essential information necessary for client interaction when the client interacts with them.
* For example, a weapon defined by this packet may not care internally what fire mode it is in or how much ammunition it has.
* Such a weapon is not in the client's player's holster or inventory.
* It is imperceptive information to which he would not currently have access.
* An `0x17` game object is, therefore, a game object with only the essential data exposed.<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>
* When interacting with an `0x17` game object, the server will swap back and forth between it and an `0x18` object.
* (Or it will be removed when it is placed somewhere a given client will no longer be able to see it.)
* The purpose of this conversion is to control network traffic and object agency.
* It is not necessary to keep track of all objects on every player on every client individually.
* This relates to the goal of this packet exposing only "essential data."
* One player does not need to know how much ammunition remains in a weapon belonging to another player normally.
* One player also does not need to know how much ammunition is used up when another player reloads their weapon.
* The only way the first player will know is when the weapon is transferred into his own inventory.
* All other clients are spared micromanagement of the hypothetical other player's weapon.
* Updated information is only made available when and where it is needed.<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
* Knowing the object's type is necessary for proper parsing.
* If the object does not have encoding information or is unknown, it will not translate between byte data and a game object.
* @param streamLength the total length of the data that composes this packet in bits;
* exclude the opcode (1 byte) and end padding (0-7 bits);
* when encoding, it will be calculated automatically
* @param objectClass the code for the type of object being constructed;
* always an 11-bit LE value
* @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
* @see ObjectCreateDetailedMessage
* @see ObjectCreateMessageParent
*/
final case class ObjectCreateMessage(streamLength : Long,
objectClass : Int,
@ -55,185 +52,67 @@ final case class ObjectCreateMessage(streamLength : Long,
parentInfo : Option[ObjectCreateMessageParent],
data : Option[ConstructorData])
extends PlanetSideGamePacket {
def opcode = GamePacketOpcode.ObjectCreateMessage
type Packet = ObjectCreateMessage
def opcode = GamePacketOpcode.ObjectCreateMessage_Duplicate
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
* An abbreviated constructor for creating `ObjectCreateMessage`s, ignoring the optional aspect of some fields.
* @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
* @return an `ObjectCreateMessage`
*/
def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage =
ObjectCreateMessage(streamLength, objectClass, guid, Some(parentInfo), Some(data))
def apply(objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage = {
val parentInfoOpt : Option[ObjectCreateMessageParent] = Some(parentInfo)
ObjectCreateMessage(ObjectCreateBase.streamLen(parentInfoOpt, data), objectClass, guid, parentInfoOpt, Some(data))
}
/**
* 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
* An abbreviated constructor for creating `ObjectCreateMessage`s, calculating `streamLen` and ignoring `parentInfo`.
* @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
* @return an `ObjectCreateMessage`
*/
def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateMessage =
ObjectCreateMessage(streamLength, objectClass, guid, None, Some(data))
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
def apply(objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateMessage = {
ObjectCreateMessage(ObjectCreateBase.streamLen(None, data), objectClass, guid, None, Some(data))
}
/**
* 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] = (
("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] (
implicit val codec : Codec[ObjectCreateMessage] = ObjectCreateBase.baseCodec.exmap[ObjectCreateMessage] (
{
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)
val obj = ObjectCreateBase.decodeData(cls, data,
if(par.isDefined) {
ObjectClass.selectDataCodec
}
else {
ObjectClass.selectDataDroppedCodec
}
)
Attempt.successful(ObjectCreateMessage(len, cls, guid, par, obj))
},
{
case _ :: _ :: _ :: _ :: None :: HNil =>
case ObjectCreateMessage(_ , _ , _, _, None) =>
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)
case ObjectCreateMessage(_, cls, guid, par, Some(obj)) =>
val len = ObjectCreateBase.streamLen(par, obj) //even if a stream length has been assigned, it can not be trusted during encoding
val bitvec = ObjectCreateBase.encodeData(cls, obj,
if(par.isDefined) {
ObjectClass.selectDataCodec
}
else {
ObjectClass.selectDataDroppedCodec
}
)
Attempt.successful(len :: cls :: guid :: par :: bitvec :: HNil)
}
).xmap[ObjectCreateMessage] (
{
case len :: cls :: guid :: par :: obj :: HNil =>
ObjectCreateMessage(len, cls, guid, par, obj)
},
{
case ObjectCreateMessage(len, cls, guid, par, obj) =>
len :: cls :: guid :: par :: obj :: HNil
}
).as[ObjectCreateMessage]
)
}

View file

@ -104,7 +104,7 @@ final case class SquadListing(index : Int = 255,
* `behavior behavior2`<br>
* `1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;X&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Update where initial entry removes a squad from the list<br>
* `5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Clear squad list and initialize new squad list<br>
* `5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Clear squad list (ransitions directly into 255-entry)<br>
* `5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Clear squad list (transitions directly into 255-entry)<br>
* `6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;X&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Update a squad in the list
* @param behavior a code that suggests the primary purpose of the data in this packet
* @param behavior2 during initialization, this code is read;

View file

@ -0,0 +1,44 @@
// Copyright (c) 2017 PSForever
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 an adaptive construction engine (ACE).
* This one-time-use item deploys a variety of utilities into the game environment.
* Has an advanced version internally called an `advanced_ace` and commonly called a Field Deployment Unit (FDU).
* @param unk1 na
* @param unk2 na
* @param unk3 na
*/
final case class ACEData(unk1 : Int,
unk2 : Int,
unk3 : Int = 0
) extends ConstructorData {
override def bitsize : Long = 34L
}
object ACEData extends Marshallable[ACEData] {
implicit val codec : Codec[ACEData] = (
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(20) ::
("unk3" | uint4L) ::
uint2L
).exmap[ACEData] (
{
case unk1 :: unk2 :: 0 :: unk3 :: 0 :: HNil =>
Attempt.successful(ACEData(unk1, unk2, unk3))
case _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid ace data format"))
},
{
case ACEData(unk1, unk2, unk3) =>
Attempt.successful(unk1 :: unk2 :: 0 :: unk3 :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,77 @@
// Copyright (c) 2017 PSForever
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}
/**
* Data that is common to a number of items that are spawned by the adaptive construction engine, or its advanced version.
* @param pos where and how the object is oriented
* @param unk na
* @param player_guid the player who placed this object
*/
final case class ACEDeployableData(pos : PlacementData,
unk : Int,
player_guid : PlanetSideGUID
) extends StreamBitSize {
override def bitsize : Long = 23L + pos.bitsize
}
object ACEDeployableData extends Marshallable[ACEDeployableData] {
final val internalWeapon_bitsize : Long = 10
/**
* `Codec` for transforming reliable `WeaponData` from the internal structure of the turret when it is defined.
* Works for both `SmallTurretData` and `OneMannedFieldTurretData`.
*/
val internalWeaponCodec : Codec[InternalSlot] = (
uint8L :: //number of internal weapons (should be 1)?
uint2L ::
InternalSlot.codec
).exmap[InternalSlot] (
{
case 1 :: 0 :: InternalSlot(a1, b1, c1, WeaponData(a2, b2, c2, d)) :: HNil =>
Attempt.successful(InternalSlot(a1, b1, c1, WeaponData(a2, b2, c2, d)))
case 1 :: 0 :: InternalSlot(_, _, _, _) :: HNil =>
Attempt.failure(Err(s"turret internals must contain weapon data"))
case n :: 0 :: _ :: HNil =>
Attempt.failure(Err(s"turret internals can not have $n weapons"))
case _ =>
Attempt.failure(Err("invalid turret internals data format"))
},
{
case InternalSlot(a1, b1, c1, WeaponData(a2, b2, c2, d)) =>
Attempt.successful(1 :: 0 :: InternalSlot(a1, b1, c1, WeaponData(a2, b2, c2, d)) :: HNil)
case InternalSlot(_, _, _, _) =>
Attempt.failure(Err(s"turret internals must contain weapon data"))
case _ =>
Attempt.failure(Err("invalid turret internals data format"))
}
)
implicit val codec : Codec[ACEDeployableData] = (
("pos" | PlacementData.codec) ::
("unk1" | uint(7)) ::
("player_guid" | PlanetSideGUID.codec)
).exmap[ACEDeployableData] (
{
case pos :: unk :: player :: HNil =>
Attempt.successful(ACEDeployableData(pos, unk, player))
case _ =>
Attempt.failure(Err("invalid deployable data format"))
},
{
case ACEDeployableData(pos, unk, player) =>
Attempt.successful(pos :: unk :: player :: HNil)
}
)
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of the aegis shield generator deployed using an advanced adaptive construction engine.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param health the amount of health the object has, as a percentage of a filled bar
*/
final case class AegisShieldGeneratorData(deploy : ACEDeployableData,
health : Int
) extends ConstructorData {
override def bitsize : Long = {
108 + deploy.bitsize //8u + 100u
}
}
object AegisShieldGeneratorData extends Marshallable[AegisShieldGeneratorData] {
implicit val codec : Codec[AegisShieldGeneratorData] = (
("deploy" | ACEDeployableData.codec) ::
("health" | uint8L) ::
uint32 :: uint32 :: uint32 :: uint4L //100 bits
).exmap[AegisShieldGeneratorData] (
{
case deploy :: health :: 0 :: 0 :: 0 :: 0 :: HNil =>
Attempt.successful(AegisShieldGeneratorData(deploy, health))
case _ =>
Attempt.failure(Err("invalid aegis data format"))
},
{
case AegisShieldGeneratorData(deploy, health) =>
Attempt.successful(deploy :: health :: 0L :: 0L :: 0L :: 0 :: HNil)
}
)
}

View file

@ -8,68 +8,46 @@ import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the ammunition portion of `ObjectCreateMessage` packet data.
* A representation of ammunition that can be created using `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
* This ammunition object ompletely ignores thr capacity field, normal to detailed ammunition objects.
* Creating an object of this type directly and picking it up or observing it (in a weapon) reveals a single round.
* @param unk na;
* defaults to 0
* @see `DetailedAmmoBoxData`
*/
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
final case class AmmoBoxData(unk : Int = 0) extends ConstructorData {
override def bitsize : Long = 24L
}
object AmmoBoxData extends Marshallable[AmmoBoxData] {
/**
* An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot`.
* An abbreviated constructor for creating `AmmoBoxData` 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`
* @param ammo the ammunition object
* @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] (
uint4L ::
("unk" | uint4L) ::
uint(16)
).exmap[AmmoBoxData] (
{
case 0xC8 :: 0 :: mag :: false :: HNil =>
Attempt.successful(AmmoBoxData(mag))
case a :: b :: _ :: d :: HNil =>
case 0xC :: unk :: 0 :: HNil =>
Attempt.successful(AmmoBoxData(unk))
case _ :: _ :: _ :: 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"))
case AmmoBoxData(unk) =>
Attempt.successful(0xC :: unk :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,34 @@
// Copyright (c) 2017 PSForever
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 detonator utility that is created when putting down a Boomer with an ACE.
* @param unk na
*/
final case class BoomerTriggerData(unk : Int = 0x8) extends ConstructorData {
override def bitsize : Long = 34L
}
object BoomerTriggerData extends Marshallable[BoomerTriggerData] {
implicit val codec : Codec[BoomerTriggerData] = (
uint4L ::
uint4L ::
uint(26)
).exmap[BoomerTriggerData] (
{
case 0xC :: unk :: 0 :: HNil =>
Attempt.successful(BoomerTriggerData(unk))
case _ =>
Attempt.failure(Err("invalid command detonater format"))
},
{
case BoomerTriggerData(unk) =>
Attempt.successful(0xC :: unk :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,61 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.types.PlanetSideEmpire
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the capture flag portion of `ObjectCreateDetailedMessage` packet data.
* This creates what is known as a lattice logic unit, or LLU.
* It is originally spawned in the base object called the lattice link socket during certain base captures.<br>
* <br>
* Players can not directly interact with the capture flag.
* Whenever an applicable player is nearby, that client will rapidly fire off `ItemUseMessage` packets to the server.
* The capture flag will be picked-up by the player and stored in a special slot that is not part of their inventory.
* A special dropping keybind has been prepared to relinquish the capture flag back to the game world.
* @param faction the empire whose players may interact with this capture flag
* @param unk1 na
* @param unk2 na
* @param unk3 na
* @param unk4 na
*/
final case class CaptureFlagData(pos : PlacementData,
faction : PlanetSideEmpire.Value,
unk1 : Int,
unk2 : Int,
unk3 : Int,
unk4 : Int
) extends ConstructorData {
override def bitsize : Long = 88L + pos.bitsize
}
object CaptureFlagData extends Marshallable[CaptureFlagData] {
implicit val codec : Codec[CaptureFlagData] = (
("pos" | PlacementData.codec) ::
("faction" | PlanetSideEmpire.codec) ::
bool ::
uint4L ::
uint16L ::
("unk1" | uint8L) ::
uint8L ::
("unk2" | uint8L) ::
uint8L ::
("unk3" | uint16L) :: //probably a PlanetSideGUID
("unk4" | uint8L) ::
uint(9)
).exmap[CaptureFlagData] (
{
case pos :: fac :: false :: 4 :: 0 :: unk1 :: 0 :: unk2 :: 0 :: unk3 :: unk4 :: 0 :: HNil =>
Attempt.Successful(CaptureFlagData(pos, fac, unk1, unk2, unk3, unk4))
case _ =>
Attempt.failure(Err("invalid capture flag data"))
},
{
case CaptureFlagData(pos, fac, unk1, unk2, unk3, unk4) =>
Attempt.successful(pos :: fac :: false :: 4 :: 0 :: unk1 :: 0 :: unk2 :: 0 :: unk3 :: unk4 :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,220 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.{Marshallable, PacketHelpers}
import net.psforever.types.{CharacterGender, ExoSuitType, GrenadeState, PlanetSideEmpire}
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 coincides with the data available from the `CharacterCreateRequestMessage` packet.<br>
* <br>
* Voice:<br>
* `&nbsp;&nbsp;&nbsp;&nbsp;MALE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FEMALE`<br>
* `0 - no voice &nbsp;no voice`<br>
* `1 - male_1 &nbsp;&nbsp; female_1`<br>
* `2 - male_2 &nbsp;&nbsp; female_2`<br>
* `3 - male_3 &nbsp;&nbsp; female_3`<br>
* `4 - male_4 &nbsp;&nbsp; female_4`<br>
* `5 - male_5 &nbsp;&nbsp; female_5`<br>
* `6 - female_1 &nbsp;no voice`<br>
* `7 - female_2 &nbsp;no voice`
* @param name the unique name of the avatar;
* minimum of two characters
* @param faction the empire to which the avatar belongs
* @param sex whether the avatar is `Male` or `Female`
* @param head the avatar's face and hair;
* by row and column on the character creation screen, the high nibble is the row and the low nibble is the column
* @param voice the avatar's voice selection
* @see `PlanetSideEmpire`
* @see `CharacaterGender`
*/
final case class BasicCharacterData(name : String,
faction : PlanetSideEmpire.Value,
sex : CharacterGender.Value,
head : Int,
voice : Int)
/**
* A part of a representation of the avatar portion of `ObjectCreateDetailedMessage` packet data.<br>
* <br>
* This is a shared partition of the data used to represent how the player's avatar is presented.
* It is utilized by both `0x17 ObjectCreateMessage CharacterData` and `0x18 ObjectCreateDetailedMessage DetailedCharacterData`.
* This can be considered the data that goes into creating the player's model.<br>
* <br>
* Only a few changes would occur depending on which packet would deal with the data.
* One example is `facingYawUpper` which, when depicting avatars, can be set to represent non-trivial turning angles.
* When depicting other players, it is limited to a small range of angles in the direction of that model's forward-facing.
* Another example is the outfit information: not usually represented for avatars; but, always represented for other players.<br>
* <br>
* One way the player's model can be changed dramatically involves being depicted as "released."
* In this form, their body appears as a backpack (or pumpkin or pastry) that can be looted for the equipment carried while alive.
* Companion data will describe how the player is represented while he is "dead," usually a requirement for being "released."
* Without that requirement here, it is possible to depicte the player as a "living backpack."
* The said equipment is also defined elsewhere.
* Another dramatic change replaces the player's model with a ball of plasma that masks the player while riding zip lines.<br>
* <br>
* Exploration:<br>
* How do I crouch?
* @param pos the position of the character in the world environment (in three coordinates)
* @param basic_appearance the player's cardinal appearance settings
* @param voice2 na;
* affects the frequency by which the character's voice is heard (somehow);
* commonly 3 for best results
* @param black_ops whether or not this avatar is enrolled in Black OPs
* @param jammered the player has been caught in an EMP blast recently;
* creates a jammered sound effect that follows the player around and can be heard by others
* @param exosuit the type of exo-suit 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 outfit_name the name of the outfit to which this player belongs;
* if the option is selected, allies with see either "[`outfit_name`]" or "{No Outfit}" under the player's name
* @param outfit_logo the decal seen on the player's exo-suit (and beret and cap) associated with the player's outfit;
* if there is a variable color for that decal, the faction-appropriate one is selected
* @param facingPitch the angle with respect to the sky and the ground towards which the avatar is looking
* @param facingYawUpper the angle of the avatar's upper body with respect to its forward-facing direction
* @param lfs this player is looking for a squad;
* all allies will see the phrase "[Looking for Squad]" under the player's name
* @param is_cloaking avatar is cloaked by virtue of an Infiltration Suit
* @param grenade_state if the player has a grenade `Primed`;
* should be `GrenadeStateState.None` if nothing special
* @param charging_pose animation pose for both charging modules and BFR imprinting
* @param on_zipline player's model is changed into a faction-color ball of energy, as if on a zip line
* @param ribbons the four merit commendation ribbon medals
* @see `CharacterData`
* @see `DetailedCharacterData`
* @see `PlacementData`
* @see `ExoSuitType`
* @see `GrenadeState`
* @see `RibbonBars`
* @see `http://wiki.planetsidesyndicate.com/index.php?title=Outfit_Logo` for a list of outfit decals
*/
final case class CharacterAppearanceData(pos : PlacementData,
basic_appearance : BasicCharacterData,
voice2 : Int,
black_ops : Boolean,
jammered : Boolean,
exosuit : ExoSuitType.Value,
outfit_name : String,
outfit_logo : Int,
backpack : Boolean,
facingPitch : Int,
facingYawUpper : Int,
lfs : Boolean,
grenade_state : GrenadeState.Value,
is_cloaking : Boolean,
charging_pose : Boolean,
on_zipline : Boolean,
ribbons : RibbonBars) extends StreamBitSize {
override def bitsize : Long = {
//factor guard bool values into the base size, not its corresponding optional field
val placementSize : Long = pos.bitsize
val nameStringSize : Long = StreamBitSize.stringBitSize(basic_appearance.name, 16) + CharacterAppearanceData.namePadding(pos.init_move)
val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) + CharacterAppearanceData.outfitNamePadding
val altModelSize = if(on_zipline || backpack) { 1L } else { 0L }
335L + placementSize + nameStringSize + outfitStringSize + altModelSize
}
}
object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
/**
* Get the padding of the player's name.
* The padding will always be a number 0-7.
* @return the pad length in bits
*/
def namePadding(move : Option[_]) : Int = {
if(move.isDefined) {
2
}
else {
4
}
}
/**
* Get the padding of the outfit's name.
* The padding will always be a number 0-7.
* @return the pad length in bits
*/
def outfitNamePadding : Int = {
6
}
implicit val codec : Codec[CharacterAppearanceData] = (
("pos" | PlacementData.codec) >>:~ { pos =>
("faction" | PlanetSideEmpire.codec) ::
("black_ops" | bool) ::
(("alt_model" | bool) >>:~ { alt_model => //modifies stream format (to display alternate player models)
ignore(1) :: //unknown
("jammered" | bool) ::
bool :: //crashes client
uint(16) :: //unknown, but usually 0
("name" | PacketHelpers.encodedWideStringAligned( namePadding(pos.init_move) )) ::
("exosuit" | ExoSuitType.codec) ::
ignore(2) :: //unknown
("sex" | CharacterGender.codec) ::
("head" | uint8L) ::
("voice" | uint(3)) ::
("voice2" | uint2L) ::
ignore(78) :: //unknown
uint16L :: //usually either 0 or 65535
uint32L :: //for outfit_name (below) to be visible in-game, this value should be non-zero
("outfit_name" | PacketHelpers.encodedWideStringAligned( outfitNamePadding )) ::
("outfit_logo" | uint8L) ::
ignore(1) :: //unknown
("backpack" | bool) :: //requires alt_model flag (does NOT require health == 0)
bool :: //stream misalignment when set
("facingPitch" | uint8L) ::
("facingYawUpper" | uint8L) ::
ignore(1) :: //unknown
conditional(alt_model, bool) :: //alt_model flag adds a bit before lfs
ignore(1) :: //an alternate lfs?
("lfs" | bool) ::
("grenade_state" | GrenadeState.codec_2u) :: //note: bin10 and bin11 are neutral (bin00 is not defined)
("is_cloaking" | bool) ::
ignore(1) :: //unknown
bool :: //stream misalignment when set
("charging_pose" | bool) ::
ignore(1) :: //alternate charging pose?
("on_zipline" | bool) :: //requires alt_model flag
("ribbons" | RibbonBars.codec)
})
}).exmap[CharacterAppearanceData] (
{
case _ :: _ :: _ :: false :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: true :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil |
_ :: _ :: _ :: false :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: true :: _ :: HNil =>
Attempt.Failure(Err("invalid character appearance data; can not encode alternate model without required bit set"))
case pos :: faction :: bops :: _ :: _ :: jamd :: false :: 0 :: name :: suit :: _ :: sex :: head :: v1 :: v2 :: _ :: _ :: _/*has_outfit_name*/ :: outfit :: logo :: _ :: bpack :: false :: facingPitch :: facingYawUpper :: _ :: _ :: _ :: lfs :: gstate :: cloaking :: _ :: false :: charging :: _ :: zipline :: ribbons :: HNil =>
Attempt.successful(
CharacterAppearanceData(pos, BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons)
)
case _ =>
Attempt.Failure(Err("invalid character appearance data; can not encode"))
},
{
case CharacterAppearanceData(_, BasicCharacterData(name, PlanetSideEmpire.NEUTRAL, _, _, _), _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) =>
Attempt.failure(Err(s"character $name's faction can not declare as neutral"))
case CharacterAppearanceData(pos, BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons) =>
val has_outfit_name : Long = outfit.length.toLong //todo this is a kludge
var alt_model : Boolean = false
var alt_model_extrabit : Option[Boolean] = None
if(zipline || bpack) {
alt_model = true
alt_model_extrabit = Some(false)
}
Attempt.successful(
pos :: faction :: bops :: alt_model :: () :: jamd :: false :: 0 :: name :: suit :: () :: sex :: head :: v1 :: v2 :: () :: 0 :: has_outfit_name :: outfit :: logo :: () :: bpack :: false :: facingPitch :: facingYawUpper :: () :: alt_model_extrabit :: () :: lfs :: gstate :: cloaking :: () :: false :: charging :: () :: zipline :: ribbons :: HNil
)
case _ =>
Attempt.Failure(Err("invalid character appearance data; can not decode"))
}
)
}

View file

@ -2,402 +2,194 @@
package net.psforever.packet.game.objectcreate
import net.psforever.packet.{Marshallable, PacketHelpers}
import net.psforever.types.{PlanetSideEmpire, Vector3}
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.<br>
* Values for the implant effects on a character model.
* The effects can not be activated simultaneously.
* In at least one case, attempting to activate multiple effects will cause the PlanetSide client to crash.<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>
* Exo-suit:<br>
* `0 - Agile`<br>
* `1 - Refinforced`<br>
* `2 - Mechanized Assault`<br>
* `3 - Infiltration`<br>
* `4 - Standard`<br>
* <br>
* Sex:<br>
* `0 - invalid`<br>
* `1 - Male`<br>
* `2 - Female`<br>
* `3 - invalid`<br>
* <br>
* Voice:<br>
* `&nbsp;&nbsp;&nbsp;&nbsp;MALE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FEMALE`<br>
* `0 - no voice &nbsp;no voice`<br>
* `1 - male_1 &nbsp;&nbsp; female_1`<br>
* `2 - male_2 &nbsp;&nbsp; female_2`<br>
* `3 - male_3 &nbsp;&nbsp; female_3`<br>
* `4 - male_4 &nbsp;&nbsp; female_4`<br>
* `5 - male_5 &nbsp;&nbsp; female_5`<br>
* `6 - female_1 &nbsp;no voice`<br>
* `7 - female_2 &nbsp;no voice`
* @param pos the position of the character in the world environment (in three coordinates)
* @param objYaw the angle with respect to the horizon towards which the object's front is facing;
* every `0x1` is 2.813 degrees counter clockwise from North;
* every `0x10` is 45-degrees;
* it wraps at `0x0` == `0x80` == North
* (note: references the avatar as a game object?)
* @param faction the empire to which the avatar belongs;
* the value scale is different from `PlanetSideEmpire`
* @param bops whether or not this avatar is enrolled in Black OPs
* @param unk1 na;
* defaults to 4
* @param name the wide character name of the avatar, minimum of two characters
* @param exosuit the type of exosuit the avatar will be depicted in;
* for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits
* @param sex whether the avatar is male or female
* @param face1 the avatar's face, as by column number on the character creation screen
* @param face2 the avatar's face, as by row number on the character creation screen
* @param voice the avatar's voice selection
* @param unk2 na
* @param unk3 na;
* can be missing from the stream under certain conditions;
* see next
* @param unk4 na;
* can be missing from the stream under certain conditions;
* see previous
* @param unk5 na;
* defaults to `0x8080`
* @param unk6 na;
* defaults to `0xFFFF`;
* may be `0x0`
* @param unk7 na;
* defaults to 2
* @param viewPitch the angle with respect to the sky and the ground towards which the avatar is looking;
* only supports downwards view angles;
* `0x0` is forwards-facing;
* `0x20` to `0xFF` is downwards-facing
* @param viewYaw the angle with respect to the horizon towards which the avatar is looking;
* every `0x1` is 2.813 degrees counter clockwise from North;
* every `0x10` is 45-degrees;
* it wraps at `0x0` == `0x80` == North
* @param unk8 na
* @param ribbons the four merit commendation ribbon medals
* `RegenEffects` is a reverse-flagged item - inactive when the corresponding bit is set.
* For that reason, every other effect is `n`+1, while `NoEffects` is 1 and `RegenEffects` is 0.
*/
final case class CharacterAppearanceData(pos : Vector3,
objYaw : Int,
faction : PlanetSideEmpire.Value,
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 ImplantEffects extends Enumeration {
type Type = Value
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
}
val SurgeEffects = Value(9)
val PersonalShieldEffects = Value(5)
val DarklightEffects = Value(3)
val RegenEffects = Value(0)
val NoEffects = Value(1)
implicit val codec : Codec[CharacterAppearanceData] = (
("pos" | Vector3.codec_pos) ::
ignore(16) ::
("objYaw" | uint8L) ::
ignore(1) ::
("faction" | PlanetSideEmpire.codec) ::
("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)
).exmap[CharacterAppearanceData] (
{
case a :: _ :: b :: _ :: c :: d :: e :: _ :: f :: g :: _ :: h :: i :: j :: k :: l :: _ :: m :: n :: o :: _ :: p :: _ :: q :: _ :: r :: s :: t :: _ :: u :: HNil =>
Attempt.successful(
CharacterAppearanceData(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u)
)
},
{
case CharacterAppearanceData(_, _, PlanetSideEmpire.NEUTRAL, _, _, name, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) =>
Attempt.failure(Err(s"character $name's faction can not declare as neutral"))
case CharacterAppearanceData(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) =>
Attempt.successful(
a :: () :: b :: () :: c :: d :: e :: () :: f :: g :: () :: h :: i :: j :: k :: l :: () :: m :: n :: o :: () :: p :: () :: q :: () :: r :: s :: t :: () :: u :: HNil
)
}
)
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
}
/**
* A representation of the avatar portion of `ObjectCreateMessage` packet data.<br>
* Values for the four different color designs that impact a player's uniform.
* Exo-suits get minor graphical updates at the following battle rank levels: seven, fourteen, and twenty-five.
*/
object UniformStyle extends Enumeration {
type Type = Value
val Normal = Value(0)
val FirstUpgrade = Value(1)
val SecondUpgrade = Value(2)
val ThirdUpgrade = Value(4)
implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(3))
}
/**
* The different cosmetics that a player can apply to their model's head.<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>
* The player gets the ability to apply these minor modifications at battle rank twenty-four, just one rank before the third uniform upgrade.
* @param no_helmet removes the current helmet on the reinforced exo-suit and the agile exo-suit;
* all other cosmetics require `no_helmet` to be `true` before they can be seen
* @param beret player dons a beret
* @param sunglasses player dons sunglasses
* @param earpiece player dons an earpiece on the left
* @param brimmed_cap player dons a cap;
* the cap overrides the beret, if both are selected
*/
final case class Cosmetics(no_helmet : Boolean,
beret : Boolean,
sunglasses : Boolean,
earpiece : Boolean,
brimmed_cap : Boolean)
/**
* A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.
* This densely-packed information outlines most of the specifics of depicting some other character.<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>
* The character created by this data is treated like an NPC from the perspective of the server.
* Someone else decides how that character is behaving and the server tells each client how to depict that behavior.
* For that reason, the character is mostly for presentation purposes, rather than really being fleshed-out.
* (As far as the client is concerned, nothing stops this character from being declared an "avatar."
* A player would find such a client-controlled character lacking many important details and have poor equipment.
* They would also be competing with some other player for input control, if they could control the character at all.)<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
* Divisions exist to make the data more manageable.
* The first division of data only manages the general appearance of the player's in-game model.
* The second division (currently, the fields actually in this class) manages the status of the character.
* In general, it passes more simplified data about the character, the minimum that is necessary to explain status to some other player.
* For example, health and armor are percentages, and are depicted as bars over the player's head near the nameplate.
* The third is the inventory (composed of normal-type objects).
* Rather than equipment other players would never interact with, it only comprises the contents of the five holster slots.<br>
* <br>
* If this player is spawned as dead - with their `health` at 0% - he will start standing and then immediately fall into a lying pose.
* The death pose selected is randomized, can not be influenced, and is not be shared across clients.
* @param appearance the player's cardinal appearance settings
* @param health the amount of health the player has, as a percentage of a filled bar;
* the bar has 85 states, with 3 points for each state;
* when 0% (less than 3 of 255), the player will collapse into a death pose on the ground
* @param armor the amount of armor the player has, as a percentage of a filled bar;
* the bar has 85 states, with 3 points for each state
* @param uniform_upgrade the level of upgrade to apply to the player's base uniform
* @param command_rank the player's command rank as a number from 0 to 5;
* cosmetic armor associated with the command rank will be applied automatically
* @param implant_effects the effects of implants that can be seen on a player's character;
* though many implants can be used simultaneously, only one implant effect can be applied here
* @param cosmetics optional decorative features that are added to the player's head model by console/chat commands;
* they become available at battle rank 24, but here they require the third uniform upgrade (rank 25);
* these flags do not exist if they are not applicable
* @param inventory the avatar's inventory;
* typically, only the tools and weapons in the equipment holster slots
* @param drawn_slot the holster that is initially drawn;
* defaults to `DrawnSlot.None`
* @see `CharacterAppearanceData`
* @see `DetailedCharacterData`
* @see `InventoryData`
* @see `DrawnSlot`
*/
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
*/
uniform_upgrade : UniformStyle.Value,
command_rank : Int,
implant_effects : Option[ImplantEffects.Value],
cosmetics : Option[Cosmetics],
inventory : Option[InventoryData],
drawn_slot : DrawnSlot.Value = DrawnSlot.None
) extends ConstructorData {
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
//factor guard bool values into the base size, not its corresponding optional field
val appearanceSize : Long = appearance.bitsize
val effectsSize : Long = if(implant_effects.isDefined) { 4L } else { 0L }
val cosmeticsSize : Long = if(cosmetics.isDefined) { 5L } else { 0L }
val inventorySize : Long = if(inventory.isDefined) { inventory.get.bitsize } else { 0L }
32L + appearanceSize + effectsSize + cosmeticsSize + inventorySize
}
}
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
* An overloaded constructor for `CharacterData` that allows for a not-optional inventory.
* @param appearance the player's cardinal appearance settings
* @param health the amount of health the player has, as a percentage of a filled bar
* @param armor the amount of armor the player has, as a percentage of a filled bar
* @param uniform the level of upgrade to apply to the player's base uniform
* @param cr the player's command rank as a number from 0 to 5
* @param implant_effects the effects of implants that can be seen on a player's character
* @param cosmetics optional decorative features that are added to the player's head model by console/chat commands
* @param inv the avatar's inventory
* @param drawn_slot the holster that is initially drawn
* @return a `CharacterData` object
*/
def stringBitSize(str : String, width : Int = 8) : Long = {
val strlen = str.length
val lenSize = if(strlen > 127) 16L else 8L
lenSize + (strlen * width)
}
def apply(appearance : CharacterAppearanceData, health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : Option[ImplantEffects.Value], cosmetics : Option[Cosmetics], inv : InventoryData, drawn_slot : DrawnSlot.Value) : CharacterData =
new CharacterData(appearance, health, armor, uniform, cr, implant_effects, cosmetics, Some(inv), drawn_slot)
/**
* 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
* Check for the bit flags for the cosmetic items.
* These flags are only valid if the player has acquired their third uniform upgrade.
* @see `UniformStyle.ThirdUpgrade`
*/
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
}
private val cosmeticsCodec : Codec[Cosmetics] = (
("no_helmet" | bool) ::
("beret" | bool) ::
("sunglasses" | bool) ::
("earpiece" | bool) ::
("brimmed_cap" | bool)
).as[Cosmetics]
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)
})
("app" | CharacterAppearanceData.codec) ::
("health" | uint8L) :: //dead state when health == 0
("armor" | uint8L) ::
(("uniform_upgrade" | UniformStyle.codec) >>:~ { style =>
ignore(3) :: //unknown
("command_rank" | uintL(3)) ::
bool :: //stream misalignment when != 1
optional(bool, "implant_effects" | ImplantEffects.codec) ::
conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | cosmeticsCodec) ::
optional(bool, "inventory" | InventoryData.codec) ::
("drawn_slot" | DrawnSlot.codec) ::
bool //usually false
})
).xmap[CharacterData] (
).exmap[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)
case app :: health :: armor :: uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: inv :: drawn_slot :: false :: HNil =>
var newHealth = health
if(app.backpack) {
newHealth = 0
}
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]
Attempt.Successful(CharacterData(app, newHealth, armor, uniform, cr, implant_effects, cosmetics, inv, drawn_slot))
/**
* Transform between CharacterData and ConstructorData.
*/
val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] (
{
case x =>
Attempt.successful(Some(x.asInstanceOf[ConstructorData]))
case _ =>
Attempt.Failure(Err("invalid character data; can not encode"))
},
{
case Some(x) =>
Attempt.successful(x.asInstanceOf[CharacterData])
case CharacterData(app, health, armor, uniform, cr, implant_effects, cosmetics, inv, drawn_slot) =>
var newHealth = health
if(app.backpack) {
newHealth = 0
}
Attempt.Successful(app :: newHealth :: armor :: uniform :: () :: cr :: false :: implant_effects :: cosmetics :: inv :: drawn_slot :: false :: HNil)
case _ =>
Attempt.failure(Err("can not encode character data"))
Attempt.Failure(Err("invalid character data; can not decode"))
}
)
}

View file

@ -0,0 +1,35 @@
// Copyright (c) 2017 PSForever
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 command uplink device.<br>
* I don't know much about the command uplink device so someone else has to provide this commentary.
*/
final case class CommandDetonaterData(unk1 : Int = 0,
unk2 : Int = 0) extends ConstructorData {
override def bitsize : Long = 34L
}
object CommandDetonaterData extends Marshallable[CommandDetonaterData] {
implicit val codec : Codec[CommandDetonaterData] = (
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(26)
).exmap[CommandDetonaterData] (
{
case unk1 :: unk2 :: 0 :: HNil =>
Attempt.successful(CommandDetonaterData(unk1, unk2))
case _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid command detonator data format"))
},
{
case CommandDetonaterData(unk1, unk2) =>
Attempt.successful(unk1 :: unk2 :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2017 PSForever
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 an object that can be interacted with when using a variety of terminals.
* This object is generally invisible.
* @param pos where and how the object is oriented
*/
final case class CommonTerminalData(pos : PlacementData) extends ConstructorData {
override def bitsize : Long = 24L + pos.bitsize
}
object CommonTerminalData extends Marshallable[CommonTerminalData] {
implicit val codec : Codec[CommonTerminalData] = (
("pos" | PlacementData.codec) ::
bool ::
bool ::
uint(22)
).exmap[CommonTerminalData] (
{
case pos :: false :: true :: 0 :: HNil =>
Attempt.successful(CommonTerminalData(pos))
case _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid terminal data format"))
},
{
case CommonTerminalData(pos) =>
Attempt.successful(pos :: false :: true :: 0 :: HNil)
}
)
}

View file

@ -3,62 +3,63 @@ package net.psforever.packet.game.objectcreate
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.packet.{Marshallable, PacketHelpers}
import scodec.codecs._
import scodec.codecs.{uint, _}
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of a class of weapons that can be created using `ObjectCreateMessage` packet data.
* A representation of a class of weapons that can be created using `ObjectCreateDetailedMessage` 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 unk1 na
* @param unk2 na
* @param fire_mode the current mode of weapon's fire;
* zero-indexed
* @param ammo `List` data regarding the currently loaded ammunition types and quantities
* @see WeaponData
* @see AmmoBoxData
* @see `WeaponData`
* @see `AmmoBoxData`
*/
final case class ConcurrentFeedWeaponData(unk : Int,
final case class ConcurrentFeedWeaponData(unk1 : Int,
unk2 : Int,
fire_mode : 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
44L + bitsize
}
}
object ConcurrentFeedWeaponData extends Marshallable[ConcurrentFeedWeaponData] {
/**
* An abbreviated constructor for creating `ConcurrentFeedWeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.<br>
* An abbreviated constructor for creating `ConcurrentFeedWeaponData` while masking use of `InternalSlot` for its `DetailedAmmoBoxData`.<br>
* <br>
* Exploration:<br>
* This class may need to be rewritten later to support objects spawned in the world environment.
* @param unk na
* @param unk1 na
* @param unk2 na
* @param fire_mode data regarding the currently loaded ammunition type
* @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
* @return a DetailedWeaponData object
*/
def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : ConcurrentFeedWeaponData =
new ConcurrentFeedWeaponData(unk, InternalSlot(cls, guid, parentSlot, ammo) :: Nil)
def apply(unk1 : Int, unk2 : Int, fire_mode : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : DetailedAmmoBoxData) : ConcurrentFeedWeaponData =
new ConcurrentFeedWeaponData(unk1, unk2, fire_mode, InternalSlot(cls, guid, parentSlot, ammo) :: Nil)
implicit val codec : Codec[ConcurrentFeedWeaponData] = (
("unk" | uint4L) ::
uint4L ::
uint24 ::
uint16 ::
uint2L ::
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(20) ::
("fire_mode" | int(3)) ::
bool ::
bool ::
(uint8L >>:~ { size =>
uint2L ::
("ammo" | PacketHelpers.listOfNSized(size, InternalSlot.codec)) ::
@ -66,41 +67,25 @@ object ConcurrentFeedWeaponData extends Marshallable[ConcurrentFeedWeaponData] {
})
).exmap[ConcurrentFeedWeaponData] (
{
case code :: 8 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil =>
case unk1 :: unk2 :: 0 :: fmode :: false :: true :: 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.successful(ConcurrentFeedWeaponData(unk1, unk2, fmode, ammo))
case _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid weapon data format"))
},
{
case ConcurrentFeedWeaponData(code, ammo) =>
case ConcurrentFeedWeaponData(unk1, unk2, fmode, 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"))
Attempt.successful(unk1 :: unk2 :: 0 :: fmode :: false :: true :: size :: 0 :: ammo :: false :: HNil)
}
)
}

View file

@ -1,6 +1,8 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import scodec.{Attempt, Codec, Err}
/**
* 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.
@ -18,4 +20,26 @@ object ConstructorData {
* The casting will be performed through use of `exmap` in the child class.
*/
type genericPattern = Option[ConstructorData]
/**
* Transform a `Codec[T]` for object type `T` into `ConstructorData.genericPattern`.
* @param objCodec a `Codec` that satisfies the transformation `Codec[T] -> T`
* @param objType a `String` that explains what the object should be identified as in the `Err` message;
* defaults to "object"
* @tparam T a subclass of `ConstructorData` that indicates what type the object is
* @return `ConstructorData.genericPattern`
*/
def genericCodec[T <: ConstructorData](objCodec : Codec[T], objType : String = "object") : Codec[ConstructorData.genericPattern] =
objCodec.exmap[ConstructorData.genericPattern] (
{
case x =>
Attempt.successful(Some(x.asInstanceOf[ConstructorData]))
},
{
case Some(x) =>
Attempt.successful(x.asInstanceOf[T]) //why does this work? shouldn't type erasure be a problem?
case _ =>
Attempt.failure(Err(s"can not encode as $objType data"))
}
)
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2017 PSForever
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 an adaptive construction engine (ACE).
* This one-time-use item deploys a variety of utilities into the game environment.
* Has an advanced version internally called an `advanced_ace` and commonly called a Field Deployment Unit (FDU).
* @param unk na
*/
final case class DetailedACEData(unk : Int) extends ConstructorData {
override def bitsize : Long = 51L
}
object DetailedACEData extends Marshallable[DetailedACEData] {
implicit val codec : Codec[DetailedACEData] = (
("unk" | uint4L) ::
uint4L ::
uintL(20) ::
uint4L ::
uint16L ::
uint(3)
).exmap[DetailedACEData] (
{
case code :: 8 :: 0 :: 2 :: 0 :: 4 :: HNil =>
Attempt.successful(DetailedACEData(code))
case _ =>
Attempt.failure(Err("invalid ace data format"))
},
{
case DetailedACEData(code) =>
Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 4 :: HNil)
}
)
}

View file

@ -0,0 +1,58 @@
// Copyright (c) 2017 PSForever
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 `ObjectCreateDetailedMessage` 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 unk na
* @param magazine the number of rounds available
* @see DetailedWeaponData
*/
final case class DetailedAmmoBoxData(unk : Int,
magazine : Int
) extends ConstructorData {
override def bitsize : Long = 40L
}
object DetailedAmmoBoxData extends Marshallable[DetailedAmmoBoxData] {
/**
* An abbreviated constructor for creating `DetailedWeaponData` 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 `DetailedAmmoBoxData`
* @return an `InternalSlot` object that encapsulates `DetailedAmmoBoxData`
*/
def apply(cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : DetailedAmmoBoxData) : InternalSlot =
new InternalSlot(cls, guid, parentSlot, ammo)
implicit val codec : Codec[DetailedAmmoBoxData] = (
uint4L ::
("unk" | uint4L) ::
uint(15) ::
("magazine" | uint16L) ::
bool
).exmap[DetailedAmmoBoxData] (
{
case 0xC :: unk :: 0 :: mag :: false :: HNil =>
Attempt.successful(DetailedAmmoBoxData(unk, mag))
case _ =>
Attempt.failure(Err("invalid ammunition data format"))
},
{
case DetailedAmmoBoxData(unk, mag) =>
Attempt.successful(0xC :: unk :: 0 :: mag :: false:: HNil)
}
)
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2017 PSForever
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 detonater utility that is created when putting down a Boomer with an ACE.
*/
final case class DetailedBoomerTriggerData() extends ConstructorData {
override def bitsize : Long = 51L
}
object DetailedBoomerTriggerData extends Marshallable[DetailedBoomerTriggerData] {
implicit val codec : Codec[DetailedBoomerTriggerData] = (
uint8L ::
uint(22) ::
bool :: //true
uint(17) ::
bool :: //true
uint2L
).exmap[DetailedBoomerTriggerData] (
{
case 0xC8 :: 0 :: true :: 0 :: true :: 0 :: HNil =>
Attempt.successful(DetailedBoomerTriggerData())
case _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid command detonater format"))
},
{
case DetailedBoomerTriggerData() =>
Attempt.successful(0xC8 :: 0 :: true :: 0 :: true :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,252 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.{Marshallable, PacketHelpers}
import scodec.{Attempt, Codec}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the avatar portion of `ObjectCreateDetailedMessage` packet data.
* This densely-packed information outlines most of the specifics required to depict a character as an avatar.<br>
* <br>
* As an avatar, the character created by this data is expected to be controllable by the client that gets sent this data.
* It goes into depth about information related to the given character in-game career that is not revealed to other players.<br>
* <br>
* Divisions exist to make the data more manageable.
* The first division of data only manages the general appearance of the player's in-game model.
* The second division (currently, the fields actually in this class) manages the status of the character as an avatar.
* In general, it passes more thorough data about the character that the client can display to the owner of the client.
* For example, health is a full number, rather than a percentage.
* Just as prominent is the list of first time events and the list of completed tutorials.
* The third subdivision is also exclusive to avatar-prepared characters and contains (omitted).
* The fourth is the inventory (composed of `Direct`-type objects).<br>
* <br>
* Exploration:<br>
* Lots of analysis needed for the remainder of the byte data.
* @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
* @param drawn_slot the holster that is initially drawn
* @see `CharacterAppearanceData`
* @see `CharacterData`
* @see `InventoryData`
* @see `DrawnSlot`
*/
final case class DetailedCharacterData(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 : Option[InventoryData],
drawn_slot : DrawnSlot.Value = DrawnSlot.None
) extends ConstructorData {
override def bitsize : Long = {
//factor guard bool values into the base size, not its corresponding optional field
val appearanceSize = appearance.bitsize
val fteLen = firstTimeEvents.size //fte list
var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen)
for(str <- firstTimeEvents) {
eventListSize += StreamBitSize.stringBitSize(str)
}
val tutLen = tutorials.size //tutorial list
var tutorialListSize : Long = 32L + DetailedCharacterData.tutPadding(fteLen, tutLen)
for(str <- tutorials) {
tutorialListSize += StreamBitSize.stringBitSize(str)
}
var inventorySize : Long = 0L //inventory
if(inventory.isDefined) {
inventorySize = inventory.get.bitsize
}
713L + appearanceSize + eventListSize + tutorialListSize + inventorySize
}
}
object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
/**
* Overloaded constructor for `DetailedCharacterData` that skips all the unknowns by assigning defaulted values.
* It also allows for a not-optional inventory.
* @param appearance data about the avatar's basic aesthetics
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value
* @param health for `x / y` of hitpoints, this is the avatar's `x` value
* @param armor for `x / y` of armor points, this is the avatar's `x` value
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value
* @param firstTimeEvents the list of first time events performed by this avatar
* @param tutorials the list of tutorials completed by this avatar
* @param inventory the avatar's inventory
* @param drawn_slot the holster that is initially drawn
* @return a `DetailedCharacterData` object
*/
def apply(appearance : CharacterAppearanceData, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
new DetailedCharacterData(appearance, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, 28, 4, 44, 84, 104, 1900, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
/**
* Overloaded constructor for `DetailedCharacterData` that allows for a not-optional inventory.
* @param appearance data about the avatar's basic aesthetics
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value
* @param health for `x / y` of hitpoints, this is the avatar's `x` value
* @param armor for `x / y` of armor points, this is the avatar's `x` value
* @param unk1 na
* @param unk2 na
* @param unk3 na
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value
* @param unk4 na
* @param unk5 na
* @param unk6 na
* @param unk7 na
* @param unk8 na
* @param unk9 na
* @param firstTimeEvents the list of first time events performed by this avatar
* @param tutorials the list of tutorials completed by this avatar
* @param inventory the avatar's inventory
* @param drawn_slot the holster that is initially drawn
* @return a `DetailedCharacterData` object
*/
def apply(appearance : CharacterAppearanceData, healthMax : Int, health : Int, armor : Int, unk1 : Int, unk2 : Int, unk3 : Int, staminaMax : Int, stamina : Int, unk4 : Int, unk5 : Int, unk6 : Int, unk7 : Int, unk8 : Int, unk9 : Int, firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
new DetailedCharacterData(appearance, healthMax, health, armor, unk1, unk2, unk3, staminaMax, stamina, unk4, unk5, unk6, unk7, unk8, unk9, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
/**
* 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[DetailedCharacterData] = (
("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) ::
optional(bool, "inventory" | InventoryData.codec_detailed) ::
("drawn_slot" | DrawnSlot.codec) ::
bool //usually false
})
})
).exmap[DetailedCharacterData] (
{
case app :: _ :: b :: c :: _ :: d :: _ :: e :: _ :: f :: g :: h :: i :: _ :: j :: k :: l :: m :: n :: o :: _ :: _ :: q :: r :: _ :: t :: u :: _ :: v :: w :: false :: 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
Attempt.successful(DetailedCharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, v, w))
},
{
case DetailedCharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, p, q) =>
//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)
}
Attempt.successful(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 :: q :: false :: HNil)
}
)
}

View file

@ -0,0 +1,38 @@
// Copyright (c) 2017 PSForever
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 command uplink device.<br>
* I don't know much about the command uplink device so someone else has to provide this commentary.
*/
final case class DetailedCommandDetonaterData(unk1 : Int = 8,
unk2 : Int = 0) extends ConstructorData {
override def bitsize : Long = 51L
}
object DetailedCommandDetonaterData extends Marshallable[DetailedCommandDetonaterData] {
implicit val codec : Codec[DetailedCommandDetonaterData] = (
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(20) ::
uint4L ::
uint16 ::
uint(3)
).exmap[DetailedCommandDetonaterData] (
{
case unk1 :: unk2 :: 0 :: 2 :: 0 :: 4 :: HNil =>
Attempt.successful(DetailedCommandDetonaterData(unk1, unk2))
case _ =>
Attempt.failure(Err("invalid command detonator data format"))
},
{
case DetailedCommandDetonaterData(unk1, unk2) =>
Attempt.successful(unk1 :: unk2 :: 0 :: 2 :: 0 :: 4 :: HNil)
}
)
}

View file

@ -0,0 +1,86 @@
// Copyright (c) 2017 PSForever
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 `ObjectCreateDetailedMessage` 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 of ammunition in its magazine.
* This ammunition data essentially is the weapon's magazines as numbered slots.
* @param unk1 na
* @param unk2 na
* @param ammo `List` data regarding the currently loaded ammunition types and quantities
* @see DetailedWeaponData
* @see DetailedAmmoBoxData
*/
final case class DetailedConcurrentFeedWeaponData(unk1 : Int,
unk2 : Int,
ammo : List[InternalSlot]) extends ConstructorData {
override def bitsize : Long = {
var bitsize : Long = 0L
for(o <- ammo) {
bitsize += o.bitsize
}
61L + bitsize
}
}
object DetailedConcurrentFeedWeaponData extends Marshallable[DetailedConcurrentFeedWeaponData] {
/**
* An abbreviated constructor for creating `DetailedConcurrentFeedWeaponData` while masking use of `InternalSlot` for its `DetailedAmmoBoxData`.<br>
* <br>
* Exploration:<br>
* This class may need to be rewritten later to support objects spawned in the world environment.
* @param unk1 na
* @param unk2 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 DetailedWeaponData object
*/
def apply(unk1 : Int, unk2 : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : DetailedAmmoBoxData) : DetailedConcurrentFeedWeaponData =
new DetailedConcurrentFeedWeaponData(unk1, unk2, InternalSlot(cls, guid, parentSlot, ammo) :: Nil)
implicit val codec : Codec[DetailedConcurrentFeedWeaponData] = (
("unk" | uint4L) ::
uint4L ::
uint24 ::
uint16 ::
uint2L ::
(uint8L >>:~ { size =>
uint2L ::
("ammo" | PacketHelpers.listOfNSized(size, InternalSlot.codec_detailed)) ::
bool
})
).exmap[DetailedConcurrentFeedWeaponData] (
{
case unk1 :: unk2 :: 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(DetailedConcurrentFeedWeaponData(unk1, unk2, ammo))
case _ =>
Attempt.failure(Err("invalid weapon data format"))
},
{
case DetailedConcurrentFeedWeaponData(unk1, unk2, 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(unk1 :: unk2 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil)
}
)
}

View file

@ -0,0 +1,41 @@
// Copyright (c) 2017 PSForever
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 `ObjectCreateDetailedMessage` 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 `DetailedWeaponData` format.
* @param unk na
*/
final case class DetailedREKData(unk : Int) extends ConstructorData {
override def bitsize : Long = 67L
}
object DetailedREKData extends Marshallable[DetailedREKData] {
implicit val codec : Codec[DetailedREKData] = (
("unk" | uint4L) ::
uint4L ::
uintL(20) ::
uint4L ::
uint16L ::
uint4L ::
uintL(15)
).exmap[DetailedREKData] (
{
case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil =>
Attempt.successful(DetailedREKData(code))
case _ =>
Attempt.failure(Err("invalid rek data format"))
},
{
case DetailedREKData(code) =>
Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,64 @@
// Copyright (c) 2017 PSForever
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 `ObjectCreateDetailedMessage` 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.
* This format only handles one type of ammunition at a time.
* Any weapon that has two types of ammunition simultaneously loaded 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 DetailedAmmoBoxData
*/
final case class DetailedWeaponData(unk : Int,
ammo : InternalSlot) extends ConstructorData {
override def bitsize : Long = 61L + ammo.bitsize
}
object DetailedWeaponData extends Marshallable[DetailedWeaponData] {
/**
* An abbreviated constructor for creating `DetailedWeaponData` while masking use of `InternalSlot` for its `DetailedAmmoBoxData`.
* @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 DetailedWeaponData object
*/
def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : DetailedAmmoBoxData) : DetailedWeaponData =
new DetailedWeaponData(unk, InternalSlot(cls, guid, parentSlot, ammo))
implicit val codec : Codec[DetailedWeaponData] = (
("unk" | uint4L) ::
uint4L ::
uint24 ::
uint16L ::
uint2 ::
uint8 :: //size = 1 type of ammunition loaded
uint2 ::
("ammo" | InternalSlot.codec_detailed) ::
bool
).exmap[DetailedWeaponData] (
{
case code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil =>
Attempt.successful(DetailedWeaponData(code, ammo))
case _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid weapon data format"))
},
{
case DetailedWeaponData(code, ammo) =>
Attempt.successful(code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil)
}
)
}

View file

@ -0,0 +1,25 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
/**
* Values for the equipment holster slot whose contained ("held") equipment can be drawn.
* The values for these Enums match the slot number by index for Infantry weapons.<br>
* <br>
* `None` is not a kludge.
* While any "not a holster" number can be used to indicate "no weapon drawn," seven is the value PlanetSide is looking for.
* Using five or six delays the first weapon draw while the client corrects its internal state.
*/
object DrawnSlot extends Enumeration {
type Type = Value
val Pistol1 = Value(0)
val Pistol2 = Value(1)
val Rifle1 = Value(2)
val Rifle2 = Value(3)
val Melee = Value(4)
val None = Value(7)
import net.psforever.packet.PacketHelpers
import scodec.codecs._
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(3))
}

View file

@ -0,0 +1,62 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* Provide information that positions a given object on the ground in the game world.
* @param pos where and how the object is oriented
* @param obj the object on the ground
* @tparam T a subclass of `ConstructorData` that indicates what type the object is
*/
final case class DroppedItemData[T <: ConstructorData](pos : PlacementData, obj : T) extends ConstructorData {
override def bitsize : Long = pos.bitsize + obj.bitsize
}
object DroppedItemData {
/**
* Transform `DroppedItemData[T]` for object type `T` into `ConstructorData.genericPattern`.<br>
* <br>
* This function eliminates the need to have a separate "DroppedFooData" class for every object "Foo."
* Two functions normally perform this transformation: an `implicit` `codec` used in a `genericCodec`.
* Since actual Generics are utilized, combining the processes eliminates defining to the type data multiple times.
* (If that is even possible here.)
* Knowledge of the object type is still necessary to recover the original object's data through casting.
* Not having to explicitly cast would have been the main upside of having specialized "DroppedFooData" classes.<br>
* <br>
* Use:<br>
* `DroppedItemCodec.genericCodec(T.codec)`
* @param objCodec a `Codec` that satisfies the transformation `Codec[T] -> T`
* @param objType a `String` that explains what the object should be identified as in the log;
* defaults to "object"
* @tparam T a subclass of `ConstructorData` that indicates what type the object is
* @return `ConstructorData.genericPattern`
* @see `ConstructorData.genericPattern` (function)
*/
def genericCodec[T <: ConstructorData](objCodec : Codec[T], objType : String = "object") : Codec[ConstructorData.genericPattern] = (
("pos" | PlacementData.codec) ::
("obj" | objCodec)
).xmap[DroppedItemData[T]] (
{
case pos :: obj :: HNil =>
DroppedItemData[T](pos, obj)
},
{
case DroppedItemData(pos, obj) =>
pos :: obj :: HNil
}
).exmap[ConstructorData.genericPattern] (
{
case x =>
Attempt.successful(Some(x.asInstanceOf[ConstructorData]))
},
{
case Some(x) =>
Attempt.successful(x.asInstanceOf[DroppedItemData[T]])
case _ =>
Attempt.failure(Err(s"can not encode dropped $objType data"))
}
)
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2017 PSForever
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 an object that can be interacted with when using an implant terminal.
* This object is generally invisible.
*/
final case class ImplantInterfaceData() extends ConstructorData {
override def bitsize : Long = 24L
}
object ImplantInterfaceData extends Marshallable[ImplantInterfaceData] {
implicit val codec : Codec[ImplantInterfaceData] = (
bool ::
uint(23)
).exmap[ImplantInterfaceData] (
{
case true :: 0 :: HNil =>
Attempt.successful(ImplantInterfaceData())
case _ :: _ :: HNil =>
Attempt.failure(Err("invalid interface data format"))
},
{
case ImplantInterfaceData() =>
Attempt.successful(true :: 0 :: HNil)
}
)
}

View file

@ -1,46 +1,47 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.{Marshallable, PacketHelpers}
import net.psforever.packet.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>
* An intermediate class for the primary fields of `ObjectCreate*Message` 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>
* This prior object will clarify the identity of the "parent" object that owns the given `parentSlot`.
* As the name implies, this should never have to be used in the representation of a non-child object.<br>
* <br>
* Try to avoid exposing `InternalSlot` in the process of implementing code.
* Try to avoid exposing `InternalSlot` in the process of implementing object code.
* (Provide overrode constructors where applicable.)
* @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
* @see `ObjectClass.selectDataCodec`
* @see `ObjectClass.selectDataDetailedCodec`
*/
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] = (
object InternalSlot {
/**
* Used for `0x18` `ObjectCreateDetailedMessage` packets
*/
val codec_detailed : 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
("obj" | ObjectClass.selectDataDetailedCodec(obj_cls)) //it's fine for this call to fail
}
).xmap[InternalSlot] (
{
@ -51,5 +52,25 @@ object InternalSlot extends Marshallable[InternalSlot] {
case InternalSlot(cls, guid, slot, obj) =>
cls :: guid :: slot :: Some(obj) :: HNil
}
).as[InternalSlot]
)
/**
* Used for `0x17` `ObjectCreateMessage` packets
*/
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
}
)
}

View file

@ -1,52 +1,33 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.{Marshallable, PacketHelpers}
import net.psforever.packet.PacketHelpers
import scodec.Codec
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the inventory portion of `ObjectCreateMessage` packet data for avatars.<br>
* A representation of the inventory portion of `ObjectCreate*Message` 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.
* The slot number refers to the position occupied by the item.
* In icon format, all-encompassing slots are absolute positions; and, grid-distributed icons use the upper-left corner.
* 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>
* If there is even a minor failure, the remainder 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`?
* Inventories are usually prefaced with a `bin1` value not accounted for here.
* It can be treated as optional.
* @param contents the items in the inventory
* @param unk1 na
* @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
*/
final case class InventoryData(contents : List[InventoryItem] = List.empty,
unk1 : Boolean = false,
unk2 : Boolean = false) extends StreamBitSize {
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
val base : Long = 10L //8u + 1u + 1u
var invSize : Long = 0L //length of all items in inventory
for(item <- contents) {
invSize += item.bitsize
}
@ -54,23 +35,49 @@ final case class InventoryData(unk1 : Boolean,
}
}
object InventoryData extends Marshallable[InventoryData] {
implicit val codec : Codec[InventoryData] = (
("unk1" | bool) ::
(("len" | uint8L) >>:~ { len =>
object InventoryData {
private def inventoryCodec(itemCodec : Codec[InventoryItem]) : Codec[InventoryData] = (
uint8L >>:~ { len =>
("unk1" | bool) ::
("unk2" | bool) ::
("unk3" | bool) ::
("contents" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) ::
ignore(4)
})
).xmap[InventoryData] (
("contents" | PacketHelpers.listOfNSized(len, itemCodec))
}
).xmap[InventoryData] (
{
case u1 :: _ :: a :: b :: ctnt :: _ :: HNil =>
InventoryData(u1, a, b, ctnt)
case _ :: a :: b :: c :: HNil =>
InventoryData(c, a, b)
},
{
case InventoryData(u1, a, b, ctnt) =>
u1 :: ctnt.size :: a :: b :: ctnt :: () :: HNil
case InventoryData(c, a, b) =>
c.size :: a :: b :: c :: HNil
}
).as[InventoryData]
)
/**
* A `Codec` for `0x17` `ObjectCreateMessage` data.
*/
val codec : Codec[InventoryData] = inventoryCodec(InventoryItem.codec).hlist.xmap[InventoryData] (
{
case inventory :: HNil =>
inventory
},
{
case InventoryData(a, b, c) =>
InventoryData(a, b, c) :: HNil
}
)
/**
* A `Codec` for `0x18` `ObjectCreateDetailedMessage` data.
*/
val codec_detailed : Codec[InventoryData] = inventoryCodec(InventoryItem.codec_detailed).hlist.xmap[InventoryData] (
{
case inventory :: HNil =>
inventory
},
{
case InventoryData(a, b, c) =>
InventoryData(a, b, c) :: HNil
}
)
}

View file

@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.Codec
import scodec.codecs._
@ -16,16 +15,12 @@ import scodec.codecs._
* @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
*/
final case class InventoryItem(item : InternalSlot
) extends StreamBitSize {
override def bitsize : Long = item.bitsize
}
object InventoryItem extends Marshallable[InventoryItem] {
object 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
@ -37,7 +32,13 @@ object InventoryItem extends Marshallable[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]
/**
* A `Codec` for `0x17` `ObjectCreateMessage` data.
*/
val codec : Codec[InventoryItem] = ("item" | InternalSlot.codec).as[InventoryItem]
/**
* A `Codec` for `0x18` `ObjectCreateDetailedMessage` data.
*/
val codec_detailed : Codec[InventoryItem] = ("item" | InternalSlot.codec_detailed).as[InventoryItem]
}

View file

@ -0,0 +1,38 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation for a game object that can contain items.<br>
* <br>
* For whatever reason, these "lockers" are typically placed at the origin coordinates.
* @param inventory the items inside his locker
*/
final case class LockerContainerData(inventory : InventoryData) extends ConstructorData {
override def bitsize : Long = 105L + inventory.bitsize //81u + 2u + 21u + 1u
}
object LockerContainerData extends Marshallable[LockerContainerData] {
implicit val codec : Codec[LockerContainerData] = (
uint32 :: uint32 :: uint(17) :: //can substitute with PlacementData, if ever necessary
uint2L ::
uint(21) ::
bool ::
InventoryData.codec
).exmap[LockerContainerData] (
{
case 0 :: 0 :: 0 :: 3 :: 0 :: true :: inv :: HNil =>
Attempt.successful(LockerContainerData(inv))
case _ =>
Attempt.failure(Err("invalid locker container format"))
},
{
case LockerContainerData(inv) =>
Attempt.successful(0L :: 0L :: 0 :: 3 :: 0 :: true :: inv :: HNil)
}
)
}

View file

@ -0,0 +1,181 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.PacketHelpers
import net.psforever.packet.game.PlanetSideGUID
import scodec.{Attempt, Codec, DecodeResult, Err}
import scodec.bits.BitVector
import scodec.codecs.{bool, either, uintL}
import shapeless.{::, HNil}
import scodec.codecs._
/**
* The parent information of a created object.<br>
* <br>
* In normal packet data order, there are two ways the parent object can be assigned.
* The first is an implicit association between a parent object and a child object that are both created at the same time.
* A player character object, for example, is initialized in the same breath as the objects in his inventory are initialized.
* A weapon object is constructed with an ammunition object already included within itself.
* The second is an explicit association between the child and the parent where the parent exists before the child is created.
* When a new inventory object is produced, it is usually assigned to some other existing object's inventory.
* That is the relationship to the role of "parent" that this object defines.
* As such, only its current unique identifier needs to be provided.
* If the parent can not be found, the child object is not created.<br>
* <br>
* A third form of parent object to child object association involves the impromptu assignment of an existing child to an existing parent.
* Since no objects are being created, that is unrelated to `ObjectCreateMessage`.
* Refer to `ObjectAttachMessage`, `MountVehicleMsg`, and `MountVehicleCargoMsg`.<br>
* <br>
* When associated, the child object is "attached" to the parent object at a specific location called a "slot."
* "Slots" are internal to the object and are (typically) invisible to the player.
* Any game object can possess any number of "slots" that serve specific purposes.
* Player objects have equipment holsters and grid inventory capacity.
* Weapon objects have magazine feed positions.
* Vehicle objects have seating for players and trunk inventory capacity.
* @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;
* encoded as the length field of a Pascal string
*/
final case class ObjectCreateMessageParent(guid : PlanetSideGUID,
slot : Int)
object ObjectCreateBase {
private type basePattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: BitVector :: HNil
private type parentPattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil
/**
* 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
*/
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
}
/**
* 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
* @param getCodecFunc a lookup function that returns a `Codec` for this object class
* @return the optional constructed object
* @see `ObjectClass`
*/
def decodeData(objectClass : Int, data : BitVector, getCodecFunc : (Int) => Codec[ConstructorData.genericPattern]) : Option[ConstructorData] = {
var out : Option[ConstructorData] = None
try {
val outOpt : Option[DecodeResult[_]] = getCodecFunc(objectClass).decode(data).toOption
if(outOpt.isDefined)
out = outOpt.get.value.asInstanceOf[ConstructorData.genericPattern]
}
catch {
case _ : 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
* @param getCodecFunc a lookup function that returns a `Codec` for this object class
* @return the bitstream data
* @see `ObjectClass`
*/
def encodeData(objClass : Int, obj : ConstructorData, getCodecFunc : (Int) => Codec[ConstructorData.genericPattern]) : BitVector = {
var out = BitVector.empty
try {
val outOpt : Option[BitVector] = getCodecFunc(objClass).encode(Some(obj.asInstanceOf[ConstructorData])).toOption
if(outOpt.isDefined)
out = outOpt.get
}
catch {
case _ : Exception =>
//catch and release, any sort of parse error
}
out
}
/**
* `Codec` for formatting around the lack of parent data in the stream.
*/
private val noParent : Codec[parentPattern] = (
("objectClass" | uintL(0xb)) :: //11u
("guid" | PlanetSideGUID.codec) //16u
).xmap[parentPattern](
{
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[parentPattern] = (
("parentGuid" | PlanetSideGUID.codec) :: //16u
("objectClass" | uintL(0xb)) :: //11u
("guid" | PlanetSideGUID.codec) :: //16u
("parentSlotIndex" | PacketHelpers.encodedStringSize) //8u or 16u
).xmap[parentPattern](
{
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
}
)
/**
* `Codec` for handling the primary fields of both `ObjectCreateMessage` packets and `ObjectCreateDetailedMessage` packets.
*/
val baseCodec : Codec[basePattern] =
("streamLength" | uint32L) ::
(either(bool, parent, noParent).exmap[parentPattern] (
{
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(_ :: _ :: None :: HNil) =>
Attempt.failure(Err("missing parent structure")) //true, _, _, None
case Right(_ :: _ :: Some(_) :: 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
}

View file

@ -0,0 +1,165 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of the player-mountable large field turrets deployed using an advanced adaptive construction engine.<br>
* <br>
* Field turrets are divided into the turret base, the mounted turret weapon, and the turret's ammunition.
* The ammunition is always the same regardless of which faction owns the turret.
* Turret bases and turret weapons are generally paired by the faction.<br>
* <br>
* If the turret has no `health`, it is rendered as destroyed.
* If the turret has no internal weapon, it is safest rendered as destroyed.
* Trying to fire a turret with no internal weapon will soft-lock the PlanetSide client.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param player_guid the player who owns this object
* @param health the amount of health the object has, as a percentage of a filled bar
* @param internals data regarding the mountable weapon
*/
final case class OneMannedFieldTurretData(deploy : ACEDeployableData,
player_guid : PlanetSideGUID, //might be able to re-package into field above
health : Int,
internals : Option[InternalSlot] = None
) extends ConstructorData {
override def bitsize : Long = {
val deploySize = deploy.bitsize
val internalSize = if(internals.isDefined) { ACEDeployableData.internalWeapon_bitsize + internals.get.bitsize } else { 0L }
38L + deploySize + internalSize //16u + 8u + 8u + 2u + 4u
}
}
object OneMannedFieldTurretData extends Marshallable[OneMannedFieldTurretData] {
/**
* Overloaded constructor that mandates information about the internal weapon of the field turret.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param player_guid the player who owns this object
* @param health the amount of health the object has, as a percentage of a filled bar
* @param internals data regarding the mountable weapon
* @return a `OneMannedFieldTurretData` object
*/
def apply(deploy : ACEDeployableData, player_guid : PlanetSideGUID, health : Int, internals : InternalSlot) : OneMannedFieldTurretData =
new OneMannedFieldTurretData(deploy, player_guid, health, Some(internals))
/**
* Prefabricated weapon data for a weaponless field turret mount (`portable_manned_turret`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
*
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def generic(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.energy_gun, wep_guid, 1,
WeaponData(wep_unk1, wep_unk2, ObjectClass.energy_gun_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
/**
* Prefabricated weapon data for the Terran Republic field turret, the Avenger (`portable_manned_turret_tr`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
*
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def avenger(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.energy_gun_tr, wep_guid, 1,
WeaponData(wep_unk1, wep_unk2, ObjectClass.energy_gun_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
/**
* Prefabricated weapon data for the New Conglomerate field turret, the Osprey (`portable_manned_turret_vnc`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def osprey(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.energy_gun_nc, wep_guid, 1,
WeaponData(wep_unk1, wep_unk2, ObjectClass.energy_gun_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
/**
* Prefabricated weapon data for the Vanu Soveriegnty field turret, the Orion (`portable_manned_turret_vs`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def orion(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.energy_gun_vs, wep_guid, 1,
WeaponData(wep_unk1, wep_unk2, ObjectClass.energy_gun_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
implicit val codec : Codec[OneMannedFieldTurretData] = (
("deploy" | ACEDeployableData.codec) ::
bool ::
("player_guid" | PlanetSideGUID.codec) ::
bool ::
("health" | uint8L) ::
uint2L ::
uint8L ::
bool ::
optional(bool, "internals" | ACEDeployableData.internalWeaponCodec)
).exmap[OneMannedFieldTurretData] (
{
case deploy :: false :: player :: false :: health :: 0 :: 0x1E :: false :: internals :: HNil =>
var newHealth : Int = health
var newInternals : Option[InternalSlot] = internals
if(health == 0 || internals.isEmpty) {
newHealth = 0
newInternals = None
}
Attempt.successful(OneMannedFieldTurretData(deploy, player, newHealth, newInternals))
case _ =>
Attempt.failure(Err("invalid omft data format"))
},
{
case OneMannedFieldTurretData(deploy, player, health, internals) =>
var newHealth : Int = health
var newInternals : Option[InternalSlot] = internals
if(health == 0 || internals.isEmpty) {
newHealth = 0
newInternals = None
}
Attempt.successful(deploy :: false :: player :: false :: newHealth :: 0 :: 0x1E :: false :: newInternals :: HNil)
case _ =>
Attempt.failure(Err("invalid omft data format"))
}
)
}

View file

@ -0,0 +1,74 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.types.Vector3
import scodec.codecs._
import scodec.Codec
/**
* A specific location and heading in game world coordinates and game world measurements.
* @param coord the xyz-coordinate location in the world
* @param roll the amount of roll that affects orientation
* @param pitch the amount of pitch that affects orientation
* @param yaw the amount of yaw that affects orientation
* @param init_move optional movement data that occurs upon placement
*/
final case class PlacementData(coord : Vector3,
roll : Int,
pitch : Int,
yaw : Int,
init_move : Option[Vector3] = None
) extends StreamBitSize {
override def bitsize : Long = {
val moveLength = if(init_move.isDefined) { 42 } else { 0 }
81L + moveLength
}
}
object PlacementData extends Marshallable[PlacementData] {
/**
* An abbreviated constructor for creating `PlacementData`, ignoring the `Vector3` for position data.
* @param x the x-coordinate location in the world
* @param y the y-coordinate location in the world
* @param z the z-coordinate location in the world
* @return a `PlacementData` object
*/
def apply(x : Float, y : Float, z : Float) : PlacementData =
new PlacementData(Vector3(x, y, z), 0, 0, 0)
/**
* An abbreviated constructor for creating `PlacementData`, ignoring the `Vector3` for position data, supplying other important fields.
* @param x the x-coordinate location in the world
* @param y the y-coordinate location in the world
* @param z the z-coordinate location in the world
* @param roll the amount of roll that affects orientation
* @param pitch the amount of pitch that affects orientation
* @param yaw the amount of yaw that affects orientation
* @return a `PlacementData` object
*/
def apply(x : Float, y : Float, z : Float, roll : Int, pitch : Int, yaw : Int) : PlacementData =
new PlacementData(Vector3(x, y, z), roll, pitch, yaw)
/**
* An abbreviated constructor for creating `PlacementData`, ignoring the `Vector3` for position data, supplying all other fields.
* @param x the x-coordinate location in the world
* @param y the y-coordinate location in the world
* @param z the z-coordinate location in the world
* @param roll the amount of roll that affects orientation
* @param pitch the amount of pitch that affects orientation
* @param yaw the amount of yaw that affects orientation
* @param init_move optional movement data that occurs upon placement
* @return a `PlacementData` object
*/
def apply(x : Float, y : Float, z : Float, roll : Int, pitch : Int, yaw : Int, init_move : Vector3) : PlacementData =
new PlacementData(Vector3(x, y, z), roll, pitch, yaw, Some(init_move))
implicit val codec : Codec[PlacementData] = (
("coord" | Vector3.codec_pos) ::
("roll" | uint8L) ::
("pitch" | uint8L) ::
("yaw" | uint8L) ::
optional(bool, "init_move" | Vector3.codec_vel)
).as[PlacementData]
}

View file

@ -7,56 +7,36 @@ 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
* na
* @param unk1 na
* @param unk2 na;
* defaults to 0
* @see `DetailedREKData`
*/
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
final case class REKData(unk1 : Int,
unk2 : Int,
unk3 : Int = 0
) extends ConstructorData {
override def bitsize : Long = 50L
}
object REKData extends Marshallable[REKData] {
implicit val codec : Codec[REKData] = (
("unk" | uint4L) ::
uint4L ::
uintL(20) ::
uint4L ::
uint16L ::
uint4L ::
uintL(15)
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(28) ::
("unk3" | uint4L) ::
uint(10)
).exmap[REKData] (
{
case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil =>
Attempt.successful(REKData(code))
case code :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
case unk1 :: unk2 :: 0 :: unk3 :: 0 :: HNil =>
Attempt.successful(REKData(unk1, unk2, unk3))
case _ :: _ :: _ :: _ :: _ :: 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"))
case REKData(unk1, unk2, unk3) =>
Attempt.successful(unk1 :: unk2 :: 0 :: unk3 :: 0 :: HNil)
}
)
}

View file

@ -17,19 +17,16 @@ import scodec.codecs._
* @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
*/
final case class RibbonBars(upper : Long = RibbonBars.noRibbon,
middle : Long = RibbonBars.noRibbon,
lower : Long = RibbonBars.noRibbon,
tos : Long = RibbonBars.noRibbon) extends StreamBitSize {
override def bitsize : Long = 128L
}
object RibbonBars extends Marshallable[RibbonBars] {
val noRibbon : Long = 0xFFFFFFFFL
implicit val codec : Codec[RibbonBars] = (
("upper" | uint32L) ::
("middle" | uint32L) ::

View file

@ -0,0 +1,34 @@
// Copyright (c) 2017 PSForever
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 simple objects that are spawned by the adaptive construction engine.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
*/
final case class SmallDeployableData(deploy : ACEDeployableData) extends ConstructorData {
override def bitsize : Long = deploy.bitsize + 1L
}
object SmallDeployableData extends Marshallable[SmallDeployableData] {
implicit val codec : Codec[SmallDeployableData] = (
("deploy" | ACEDeployableData.codec) ::
bool
).exmap[SmallDeployableData] (
{
case deploy :: false :: HNil =>
Attempt.successful(SmallDeployableData(deploy))
case _ =>
Attempt.failure(Err("invalid small deployable data format"))
},
{
case SmallDeployableData(deploy) =>
Attempt.successful(deploy :: false :: HNil)
}
)
}

View file

@ -0,0 +1,116 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of the Spitfire-based small turrets deployed using an adaptive construction engine.<br>
* <br>
* The turret may contain substructure defining a weapon is a turret weapon contained within the turret itself.
* Furthermore, that turret-like weapon is loaded with turret-like ammunition.
* In other words, this outer turret can be considered a weapons platform for the inner turret weapon.<br>
* <br>
* If the turret has no `health`, it is rendered as destroyed.
* If the turret has no internal weapon, it is safest rendered as destroyed.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param health the amount of health the object has, as a percentage of a filled bar
* @param internals data regarding the mounted weapon
*/
final case class SmallTurretData(deploy : ACEDeployableData,
health : Int,
internals : Option[InternalSlot] = None
) extends ConstructorData {
override def bitsize : Long = {
val deploySize = deploy.bitsize
val internalSize = if(internals.isDefined) { ACEDeployableData.internalWeapon_bitsize + internals.get.bitsize } else { 0L }
23L + deploySize + internalSize //1u + 8u + 7u + 4u + 2u + 1u
}
}
object SmallTurretData extends Marshallable[SmallTurretData] {
/**
* Overloaded constructor that mandates information about the internal weapon of the small turret.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param health the amount of health the object has, as a percentage of a filled bar
* @param internals data regarding the mounted weapon
* @return a `SmallTurretData` object
*/
def apply(deploy : ACEDeployableData, health : Int, internals : InternalSlot) : SmallTurretData =
new SmallTurretData(deploy, health, Some(internals))
/**
* Prefabricated weapon data for both Spitfires (`spitfire_turret`) and Shadow Turrets (`spitfire_cloaked`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def spitfire(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.spitfire_weapon, wep_guid, 0,
WeaponData(wep_unk1, wep_unk2, ObjectClass.spitfire_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
/**
* Prefabricated weapon data for Cerebus turrets (`spitfire_aa`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def cerebus(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.spitfire_aa_weapon, wep_guid, 0,
WeaponData(wep_unk1, wep_unk2, ObjectClass.spitfire_aa_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
implicit val codec : Codec[SmallTurretData] = (
("deploy" | ACEDeployableData.codec) ::
bool ::
("health" | uint8L) ::
uintL(7) ::
uint4L ::
uint2L ::
optional(bool, "internals" | ACEDeployableData.internalWeaponCodec)
).exmap[SmallTurretData] (
{
case deploy :: false :: health :: 0 :: 0xF :: 0 :: internals :: HNil =>
var newHealth : Int = health
var newInternals : Option[InternalSlot] = internals
if(health == 0 || internals.isEmpty) {
newHealth = 0
newInternals = None
}
Attempt.successful(SmallTurretData(deploy, newHealth, newInternals))
case _ =>
Attempt.failure(Err("invalid small turret data format"))
},
{
case SmallTurretData(deploy, health, internals) =>
var newHealth : Int = health
var newInternals : Option[InternalSlot] = internals
if(health == 0 || internals.isEmpty) {
newHealth = 0
newInternals = None
}
Attempt.successful(deploy :: false :: newHealth :: 0 :: 0xF :: 0 :: newInternals :: HNil)
}
)
}

View file

@ -2,17 +2,33 @@
package net.psforever.packet.game.objectcreate
/**
* Apply this trait to a class that needs to have its size in bits calculated.
* 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.
* Performs a "sizeof()" analysis of the given object.<br>
* <br>
* 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;
* For example, a traditional `Int` is normally a 32-bit number, often rendered as a `32u` number.
* When parsed with a `uintL(7)`, it's length will be considered 7 bits (`7u`).
* (Note: being permanently signed, an `scodec` value of `32u` or longer must fit into a `Long` type.)
* @return the number of bits necessary to measure an object of this class;
* defaults to `0L`
*/
def bitsize : Long = 0L
}
object StreamBitSize {
/**
* Calculate the bit size of a Pascal string.
* @param str a length-prefixed string
* @param width the width of the character encoding;
* defaults to 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)
}
}

View file

@ -0,0 +1,44 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of the tactical resonance area protection unit deployed using an advanced adaptive construction engine.
* Three metal beams, erect and tangled, block passage to enemies and their vehicles.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param health the amount of health the object has, as a percentage of a filled bar
*/
final case class TRAPData(deploy : ACEDeployableData,
health : Int
) extends ConstructorData {
override def bitsize : Long = {
23L + deploy.bitsize //8u + 7u + 4u + 3u + 1u
}
}
object TRAPData extends Marshallable[TRAPData] {
implicit val codec : Codec[TRAPData] = (
("deploy" | ACEDeployableData.codec) ::
bool ::
("health" | uint8L) ::
uint(7) ::
uint4L ::
uint(3)
).exmap[TRAPData] (
{
case deploy :: false :: health :: 0 :: 15 :: 0 :: HNil =>
Attempt.successful(TRAPData(deploy, health))
case _ =>
Attempt.failure(Err("invalid trap data format"))
},
{
case TRAPData(deploy, health) =>
Attempt.successful(deploy :: false :: health :: 0 :: 15 :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,86 @@
// Copyright (c) 2017 PSForever
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 a projectile that the server must intentionally convey to players other than the shooter.
* @param pos where and how the projectile is oriented
* @param unk1 na
* @param unk2 na;
* data specific to the type of projectile(?)
*/
final case class TrackedProjectileData(pos : PlacementData,
unk1 : Int,
unk2 : Int
) extends ConstructorData {
override def bitsize : Long = 56L + pos.bitsize
}
object TrackedProjectileData extends Marshallable[TrackedProjectileData] {
final val oicw_projectile_data = 3355587
final val striker_missile_targetting_projectile_data = 6710918
final val hunter_seeker_missile_projectile_data = 10131913
final val starfire_projectile_data = 10131961
/**
* Overloaded constructor specifically for OICW projectiles.
* @param pos where and how the projectile is oriented
* @param unk na
* @return a `TrackedProjectileData` object
*/
def oicw(pos : PlacementData, unk : Int) : TrackedProjectileData =
new TrackedProjectileData(pos, unk, oicw_projectile_data)
/**
* Overloaded constructor specifically for Striker projectiles.
* @param pos where and how the projectile is oriented
* @param unk na
* @return a `TrackedProjectileData` object
*/
def striker(pos : PlacementData, unk : Int) : TrackedProjectileData =
new TrackedProjectileData(pos, unk, striker_missile_targetting_projectile_data)
/**
* Overloaded constructor specifically for Hunter Seeker (Phoenix) projectiles.
* @param pos where and how the projectile is oriented
* @param unk na
* @return a `TrackedProjectileData` object
*/
def hunter_seeker(pos : PlacementData, unk : Int) : TrackedProjectileData =
new TrackedProjectileData(pos, unk, hunter_seeker_missile_projectile_data)
/**
* Overloaded constructor specifically for Starfire projectiles.
* @param pos where and how the projectile is oriented
* @param unk na
* @return a `TrackedProjectileData` object
*/
def starfire(pos : PlacementData, unk : Int) : TrackedProjectileData =
new TrackedProjectileData(pos, unk, starfire_projectile_data)
implicit val codec : Codec[TrackedProjectileData] = (
("pos" | PlacementData.codec) ::
("unk1" | uint(3)) ::
uint4L ::
uint16L ::
("unk2" | uint24) ::
uint4L ::
uint(5)
).exmap[TrackedProjectileData] (
{
case pos :: unk1 :: 4 :: 0 :: unk2 :: 4 :: 0 :: HNil =>
Attempt.successful(TrackedProjectileData(pos, unk1, unk2))
case _ =>
Attempt.failure(Err("invalid projectile data format"))
},
{
case TrackedProjectileData(pos, unk1, unk2) =>
Attempt.successful(pos :: unk1 :: 4 :: 0 :: unk2 :: 4 :: 0 :: HNil)
}
)
}

View file

@ -3,87 +3,80 @@ 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 scodec.{Attempt, Codec, Err}
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
* Common uses include items deposited on the ground and items in another player's visible inventory (holsters).
* @param unk1 na;
* commonly 8
* @param unk2 na;
* commonly 12
* @param fire_mode the current mode of weapon's fire;
* zero-indexed
* @param ammo data regarding the currently loaded ammunition type
* @see `WeaponData`
* @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
final case class WeaponData(unk1 : Int,
unk2 : Int,
fire_mode : Int,
ammo : InternalSlot
) extends ConstructorData {
override def bitsize : Long = 44L + 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
* An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.
* @param unk1 na
* @param unk2 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
* @param ammo the ammunition object
* @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))
def apply(unk1 : Int, unk2 : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData =
new WeaponData(unk1, unk2, 0, InternalSlot(cls, guid, parentSlot, ammo))
/**
* An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.
* @param unk1 na
* @param unk2 na
* @param fire_mode data regarding the currently loaded ammunition type
* @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 ammunition object
* @return a `WeaponData` object
*/
def apply(unk1 : Int, unk2 : Int, fire_mode : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData =
new WeaponData(unk1, unk2, fire_mode, InternalSlot(cls, guid, parentSlot, ammo))
implicit val codec : Codec[WeaponData] = (
("unk" | uint4L) ::
uint4L ::
uint24 ::
uint16L ::
uint2 ::
uint8 :: //size = 1 type of ammunition loaded
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(20) ::
("fire_mode" | int(3)) ::
bool ::
bool ::
uint8L :: //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 =>
case unk1 :: unk2 :: 0 :: fmode :: false :: true :: 1 :: 0 :: ammo :: false :: HNil =>
Attempt.successful(WeaponData(unk1, unk2, fmode, ammo))
case _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: 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"))
case WeaponData(unk1, unk2, fmode, ammo) =>
Attempt.successful(unk1 :: unk2 :: 0 :: fmode :: false :: true :: 1 :: 0 :: ammo :: false :: HNil)
}
)
}

View file

@ -0,0 +1,16 @@
// Copyright (c) 2017 PSForever
package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs.uint2L
/**
* Values for two genders, Male and Female, starting at 1 = Male.
*/
object CharacterGender extends Enumeration(1) {
type Type = Value
val Male, Female = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L)
}

View file

@ -0,0 +1,19 @@
// Copyright (c) 2017 PSForever
package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs._
/**
* Values for the the different types of exo-suits that players can wear.
*/
object ExoSuitType extends Enumeration {
type Type = Value
val Agile,
Reinforced,
MAX,
Infiltration,
Standard= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(3))
}

View file

@ -0,0 +1,21 @@
// Copyright (c) 2017 PSForever
package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs._
/**
* An `Enumeration` of the kinds of states applicable to the grenade animation.
*/
object GrenadeState extends Enumeration(1) {
type Type = Value
val Primed, //avatars and other depicted player characters
Thrown, //avatars only
None //non-actionable state of rest
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
val codec_2u = PacketHelpers.createEnumerationCodec(this, uint2L)
}

View file

@ -4,6 +4,7 @@ package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.ExoSuitType
import scodec.bits._
class ArmorChangedMessageTest extends Specification {
@ -13,7 +14,7 @@ class ArmorChangedMessageTest extends Specification {
PacketCoding.DecodePacket(string).require match {
case ArmorChangedMessage(player_guid, armor, subtype) =>
player_guid mustEqual PlanetSideGUID(273)
armor mustEqual 2
armor mustEqual ExoSuitType.MAX
subtype mustEqual 3
case _ =>
ko
@ -21,7 +22,7 @@ class ArmorChangedMessageTest extends Specification {
}
"encode" in {
val msg = ArmorChangedMessage(PlanetSideGUID(273), 2, 3)
val msg = ArmorChangedMessage(PlanetSideGUID(273), ExoSuitType.MAX, 3)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string

View file

@ -4,6 +4,7 @@ package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.GrenadeState
import scodec.bits._
class AvatarGrenadeStateMessageTest extends Specification {
@ -13,14 +14,14 @@ class AvatarGrenadeStateMessageTest extends Specification {
PacketCoding.DecodePacket(string).require match {
case AvatarGrenadeStateMessage(player_guid, state) =>
player_guid mustEqual PlanetSideGUID(4570)
state mustEqual GrenadeState.PRIMED
state mustEqual GrenadeState.Primed
case _ =>
ko
}
}
"encode" in {
val msg = AvatarGrenadeStateMessage(PlanetSideGUID(4570), GrenadeState.PRIMED)
val msg = AvatarGrenadeStateMessage(PlanetSideGUID(4570), GrenadeState.Primed)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string

View file

@ -0,0 +1,423 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate._
import net.psforever.types._
import scodec.bits._
class ObjectCreateDetailedMessageTest extends Specification {
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_detonater = hex"18 87000000 6506 EA8 7420 80 8000000200008"
val string_ace = hex"18 87000000 1006 100 C70B 80 8800000200008"
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_boomer_trigger = hex"18 87000000 6304CA8760B 80 C800000200008"
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 (2)" in {
//an invalid bit representation will fail to turn into an object
PacketCoding.DecodePacket(packet2).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 248
cls mustEqual ObjectClass.avatar
guid mustEqual PlanetSideGUID(2497)
parent mustEqual None
data.isDefined mustEqual false
case _ =>
ko
}
}
"decode (detonater)" in {
PacketCoding.DecodePacket(string_detonater).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 135
cls mustEqual ObjectClass.command_detonater
guid mustEqual PlanetSideGUID(8308)
parent.isDefined mustEqual true
parent.get.guid mustEqual PlanetSideGUID(3530)
parent.get.slot mustEqual 0
data.isDefined mustEqual true
data.get.isInstanceOf[DetailedCommandDetonaterData] mustEqual true
case _ =>
ko
}
}
"decode (ace)" in {
PacketCoding.DecodePacket(string_ace).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 135
cls mustEqual ObjectClass.ace
guid mustEqual PlanetSideGUID(3015)
parent.isDefined mustEqual true
parent.get.guid mustEqual PlanetSideGUID(3104)
parent.get.slot mustEqual 0
data.isDefined mustEqual true
data.get.isInstanceOf[DetailedACEData] mustEqual true
data.get.asInstanceOf[DetailedACEData].unk mustEqual 8
case _ =>
ko
}
}
"decode (9mm)" in {
PacketCoding.DecodePacket(string_9mm).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 124
cls mustEqual ObjectClass.bullet_9mm
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[DetailedAmmoBoxData].magazine mustEqual 50
case _ =>
ko
}
}
"decode (gauss)" in {
PacketCoding.DecodePacket(string_gauss).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 220
cls mustEqual ObjectClass.gauss
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[DetailedWeaponData]
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[DetailedAmmoBoxData].magazine mustEqual 30
case _ =>
ko
}
}
"decode (punisher)" in {
PacketCoding.DecodePacket(string_punisher).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 295
cls mustEqual ObjectClass.punisher
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[DetailedConcurrentFeedWeaponData]
obj_wep.unk1 mustEqual 0
obj_wep.unk2 mustEqual 8
val obj_ammo = obj_wep.ammo
obj_ammo.size mustEqual 2
obj_ammo.head.objectClass mustEqual ObjectClass.bullet_9mm
obj_ammo.head.guid mustEqual PlanetSideGUID(1693)
obj_ammo.head.parentSlot mustEqual 0
obj_ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 30
obj_ammo(1).objectClass mustEqual ObjectClass.jammer_cartridge
obj_ammo(1).guid mustEqual PlanetSideGUID(1564)
obj_ammo(1).parentSlot mustEqual 1
obj_ammo(1).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 1
case _ =>
ko
}
}
"decode (rek)" in {
PacketCoding.DecodePacket(string_rek).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 151
cls mustEqual ObjectClass.remote_electronics_kit
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[DetailedREKData].unk mustEqual 4
case _ =>
ko
}
}
"decode (boomer trigger)" in {
PacketCoding.DecodePacket(string_boomer_trigger).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 135
cls mustEqual ObjectClass.boomer_trigger
guid mustEqual PlanetSideGUID(2934)
parent.isDefined mustEqual true
parent.get.guid mustEqual PlanetSideGUID(2502)
parent.get.slot mustEqual 0
data.isDefined mustEqual true
data.get.isInstanceOf[DetailedBoomerTriggerData] mustEqual true
case _ =>
ko
}
}
"decode (character)" in {
PacketCoding.DecodePacket(string_testchar).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 3159
cls mustEqual ObjectClass.avatar
guid mustEqual PlanetSideGUID(75)
parent.isDefined mustEqual false
data.isDefined mustEqual true
val char = data.get.asInstanceOf[DetailedCharacterData]
char.appearance.pos.coord.x mustEqual 3674.8438f
char.appearance.pos.coord.y mustEqual 2726.789f
char.appearance.pos.coord.z mustEqual 91.15625f
char.appearance.pos.roll mustEqual 0
char.appearance.pos.pitch mustEqual 0
char.appearance.pos.yaw mustEqual 19
char.appearance.basic_appearance.name mustEqual "IlllIIIlllIlIllIlllIllI"
char.appearance.basic_appearance.faction mustEqual PlanetSideEmpire.VS
char.appearance.basic_appearance.sex mustEqual CharacterGender.Female
char.appearance.basic_appearance.head mustEqual 41
char.appearance.basic_appearance.voice mustEqual 1 //female 1
char.appearance.voice2 mustEqual 3
char.appearance.black_ops mustEqual false
char.appearance.jammered mustEqual false
char.appearance.exosuit mustEqual ExoSuitType.Standard
char.appearance.outfit_name mustEqual ""
char.appearance.outfit_logo mustEqual 0
char.appearance.backpack mustEqual false
char.appearance.facingPitch mustEqual 127
char.appearance.facingYawUpper mustEqual 181
char.appearance.lfs mustEqual true
char.appearance.grenade_state mustEqual GrenadeState.None
char.appearance.is_cloaking mustEqual false
char.appearance.charging_pose mustEqual false
char.appearance.on_zipline mustEqual false
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.isDefined mustEqual true
val inventory = char.inventory.get.contents
inventory.size mustEqual 10
//0
inventory.head.item.objectClass mustEqual ObjectClass.beamer
inventory.head.item.guid mustEqual PlanetSideGUID(76)
inventory.head.item.parentSlot mustEqual 0
var wep = inventory.head.item.obj.asInstanceOf[DetailedWeaponData]
wep.ammo.objectClass mustEqual ObjectClass.energy_cell
wep.ammo.guid mustEqual PlanetSideGUID(77)
wep.ammo.parentSlot mustEqual 0
wep.ammo.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 16
//1
inventory(1).item.objectClass mustEqual ObjectClass.suppressor
inventory(1).item.guid mustEqual PlanetSideGUID(78)
inventory(1).item.parentSlot mustEqual 2
wep = inventory(1).item.obj.asInstanceOf[DetailedWeaponData]
wep.ammo.objectClass mustEqual ObjectClass.bullet_9mm
wep.ammo.guid mustEqual PlanetSideGUID(79)
wep.ammo.parentSlot mustEqual 0
wep.ammo.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 25
//2
inventory(2).item.objectClass mustEqual ObjectClass.forceblade
inventory(2).item.guid mustEqual PlanetSideGUID(80)
inventory(2).item.parentSlot mustEqual 4
wep = inventory(2).item.obj.asInstanceOf[DetailedWeaponData]
wep.ammo.objectClass mustEqual ObjectClass.melee_ammo
wep.ammo.guid mustEqual PlanetSideGUID(81)
wep.ammo.parentSlot mustEqual 0
wep.ammo.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 1
//3
inventory(3).item.objectClass mustEqual ObjectClass.locker_container
inventory(3).item.guid mustEqual PlanetSideGUID(82)
inventory(3).item.parentSlot mustEqual 5
inventory(3).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 1
//4
inventory(4).item.objectClass mustEqual ObjectClass.bullet_9mm
inventory(4).item.guid mustEqual PlanetSideGUID(83)
inventory(4).item.parentSlot mustEqual 6
inventory(4).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50
//5
inventory(5).item.objectClass mustEqual ObjectClass.bullet_9mm
inventory(5).item.guid mustEqual PlanetSideGUID(84)
inventory(5).item.parentSlot mustEqual 9
inventory(5).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50
//6
inventory(6).item.objectClass mustEqual ObjectClass.bullet_9mm
inventory(6).item.guid mustEqual PlanetSideGUID(85)
inventory(6).item.parentSlot mustEqual 12
inventory(6).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50
//7
inventory(7).item.objectClass mustEqual ObjectClass.bullet_9mm_AP
inventory(7).item.guid mustEqual PlanetSideGUID(86)
inventory(7).item.parentSlot mustEqual 33
inventory(7).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50
//8
inventory(8).item.objectClass mustEqual ObjectClass.energy_cell
inventory(8).item.guid mustEqual PlanetSideGUID(87)
inventory(8).item.parentSlot mustEqual 36
inventory(8).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50
//9
inventory(9).item.objectClass mustEqual ObjectClass.remote_electronics_kit
inventory(9).item.guid mustEqual PlanetSideGUID(88)
inventory(9).item.parentSlot mustEqual 39
//the rek has data but none worth testing here
char.drawn_slot mustEqual DrawnSlot.Pistol1
case _ =>
ko
}
}
"encode (2)" in {
//the lack of an object will fail to turn into a bad bitstream
val msg = ObjectCreateDetailedMessage(0L, ObjectClass.avatar, PlanetSideGUID(2497), None, None)
PacketCoding.EncodePacket(msg).isFailure mustEqual true
}
"encode (detonater)" in {
val obj = DetailedCommandDetonaterData()
val msg = ObjectCreateDetailedMessage(ObjectClass.command_detonater, PlanetSideGUID(8308), ObjectCreateMessageParent(PlanetSideGUID(3530), 0), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_detonater
}
"encode (ace)" in {
val obj = DetailedACEData(8)
val msg = ObjectCreateDetailedMessage(ObjectClass.ace, PlanetSideGUID(3015), ObjectCreateMessageParent(PlanetSideGUID(3104), 0), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_ace
}
"encode (9mm)" in {
val obj = DetailedAmmoBoxData(8, 50)
val msg = ObjectCreateDetailedMessage(ObjectClass.bullet_9mm, PlanetSideGUID(1280), ObjectCreateMessageParent(PlanetSideGUID(75), 33), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_9mm
}
"encode (gauss)" in {
val obj = DetailedWeaponData(4, ObjectClass.bullet_9mm, PlanetSideGUID(1286), 0, DetailedAmmoBoxData(8, 30))
val msg = ObjectCreateDetailedMessage(ObjectClass.gauss, PlanetSideGUID(1465), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_gauss
}
"encode (punisher)" in {
val obj = DetailedConcurrentFeedWeaponData(0, 8, DetailedAmmoBoxData(ObjectClass.bullet_9mm, PlanetSideGUID(1693), 0, DetailedAmmoBoxData(8, 30)) :: DetailedAmmoBoxData(ObjectClass.jammer_cartridge, PlanetSideGUID(1564), 1, DetailedAmmoBoxData(8, 1)) :: Nil)
val msg = ObjectCreateDetailedMessage(ObjectClass.punisher, PlanetSideGUID(1703), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj)
var pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_punisher
}
"encode (rek)" in {
val obj = DetailedREKData(4)
val msg = ObjectCreateDetailedMessage(ObjectClass.remote_electronics_kit, PlanetSideGUID(1439), ObjectCreateMessageParent(PlanetSideGUID(75), 1), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_rek
}
"encode (boomer trigger)" in {
val obj = DetailedBoomerTriggerData()
val msg = ObjectCreateDetailedMessage(ObjectClass.boomer_trigger, PlanetSideGUID(2934), ObjectCreateMessageParent(PlanetSideGUID(2502), 0), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_boomer_trigger
}
"encode (character)" in {
val app = CharacterAppearanceData(
PlacementData(
Vector3(3674.8438f, 2726.789f, 91.15625f),
0, 0,
19
),
BasicCharacterData(
"IlllIIIlllIlIllIlllIllI",
PlanetSideEmpire.VS,
CharacterGender.Female,
41,
1
),
3,
false,
false,
ExoSuitType.Standard,
"",
0,
false,
127, 181,
true,
GrenadeState.None,
false,
false,
false,
RibbonBars()
)
val inv = InventoryItem(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) ::
InventoryItem(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) ::
InventoryItem(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) ::
InventoryItem(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedAmmoBoxData(8, 1)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) ::
Nil
val obj = DetailedCharacterData(
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(inv),
DrawnSlot.Pistol1
)
val msg = ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
val pkt_bitv = pkt.toBitVector
val ori_bitv = string_testchar.toBitVector
pkt_bitv.take(153) mustEqual ori_bitv.take(153) //skip 1
pkt_bitv.drop(154).take(422) mustEqual ori_bitv.drop(154).take(422) //skip 126
pkt_bitv.drop(702) mustEqual ori_bitv.drop(702)
//TODO work on DetailedCharacterData to make this pass as a single stream
}
}

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ import scodec.bits._
import org.log4s.MDC
import MDCContextAware.Implicits._
import net.psforever.packet.game.objectcreate._
import net.psforever.types.{ChatMessageType, TransactionType, PlanetSideEmpire, Vector3}
import net.psforever.types._
class WorldSessionActor extends Actor with MDCContextAware {
private[this] val log = org.log4s.getLogger
@ -111,33 +111,45 @@ class WorldSessionActor extends Actor with MDCContextAware {
//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,
PlanetSideEmpire.VS,
PlacementData(
Vector3(3674.8438f, 2726.789f, 91.15625f),
0, 0,
19
),
BasicCharacterData(
"IlllIIIlllIlIllIlllIllI",
PlanetSideEmpire.VS,
CharacterGender.Female,
41,
1
),
3,
false,
false,
ExoSuitType.Standard,
"",
0,
false,
0, 181,
true,
GrenadeState.None,
false,
false,
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.bullet_9mm, PlanetSideGUID(79), 0, AmmoBoxData(25))) ::
InventoryItem(ObjectClass.forceblade, PlanetSideGUID(80), 4, WeaponData(8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, AmmoBoxData(1))) ::
InventoryItem(ObjectClass.locker_container, PlanetSideGUID(82), 5, AmmoBoxData(1)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, AmmoBoxData(50)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, AmmoBoxData(50)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, AmmoBoxData(50)) ::
InventoryItem(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, AmmoBoxData(50)) ::
InventoryItem(ObjectClass.energy_cell, PlanetSideGUID(87), 36, AmmoBoxData(50)) ::
InventoryItem(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, REKData(8)) ::
val inv = InventoryItem(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) ::
InventoryItem(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) ::
InventoryItem(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) ::
InventoryItem(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedAmmoBoxData(8, 1)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) ::
Nil
val obj = CharacterData(
val obj = DetailedCharacterData(
app,
100, 100,
50,
@ -146,11 +158,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
28, 4, 44, 84, 104, 1900,
"xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil,
List.empty,
InventoryData(
true, false, false, inv
)
InventoryData(inv),
DrawnSlot.None
)
val objectHex = ObjectCreateMessage(0, ObjectClass.avatar, PlanetSideGUID(75), obj)
val objectHex = ObjectCreateDetailedMessage(ObjectClass.avatar, PlanetSideGUID(75), obj)
def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match {
case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) =>
@ -175,7 +186,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1))))
case CharacterRequestAction.Select =>
objectHex match {
case obj @ ObjectCreateMessage(len, cls, guid, _, _) =>
case obj @ ObjectCreateDetailedMessage(len, cls, guid, _, _) =>
log.debug("Object: " + obj)
// LoadMapMessage 13714 in mossy .gcap
// XXX: hardcoded shit
@ -216,7 +227,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0)))
sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)))
sendResponse(PacketCoding.CreateGamePacket(0, ReplicationStreamMessage(5, Some(6), Vector(SquadListing(255))))) //clear squad list
sendResponse(PacketCoding.CreateGamePacket(0, ReplicationStreamMessage(5, Some(6), Vector(SquadListing())))) //clear squad list
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
@ -286,7 +297,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ DropItemMessage(item_guid) =>
//item dropped where you spawn in VS Sanctuary
sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(PlanetSideGUID(75), item_guid, app.pos, 0, 0, 0)))
sendResponse(PacketCoding.CreateGamePacket(0, ObjectDetachMessage(PlanetSideGUID(75), item_guid, app.pos.coord, 0, 0, 0)))
log.info("DropItem: " + msg)
case msg @ PickupItemMessage(item_guid, player_guid, unk1, unk2) =>
@ -402,6 +413,18 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info("PlanetsideAttributeMessage: "+msg)
sendResponse(PacketCoding.CreateGamePacket(0,PlanetsideAttributeMessage(avatar_guid, attribute_type, attribute_value)))
case msg @ CreateShortcutMessage(player_guid, slot, unk, add, shortcut) =>
log.info("CreateShortcutMessage: "+msg)
case msg @ FriendsRequest(action, friend) =>
log.info("FriendsRequest: "+msg)
case msg @ HitHint(source, player) =>
log.info("HitHint: "+msg)
case msg @ WeaponDryFireMessage(weapon) =>
log.info("WeaponDryFireMessage: "+msg)
case default => log.error(s"Unhandled GamePacket ${pkt}")
}