removed Option from InternalSlot

refactored a chunk of CharacterData as an example
This commit is contained in:
FateJH 2016-12-06 21:15:29 -05:00
parent 9adb077d8c
commit f98a648db1
12 changed files with 672 additions and 218 deletions

View file

@ -15,7 +15,7 @@ import shapeless.{::, HNil}
* The parent is a pre-existing object into which the (created) child is attached.<br>
* <br>
* The slot is encoded as a string length integer commonly used by PlanetSide.
* It is either a 0-127 eight bit number (0 = 0x80), or a 128-32767 sixteen bit number (128 = 0x0080).
* It is either a 0-127 eight bit number (0 = `0x80`), or a 128-32767 sixteen bit number (128 = `0x0080`).
* @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
*/
@ -59,6 +59,18 @@ case class ObjectCreateMessage(streamLength : Long,
}
object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
/**
* An abbreviated constructor for creating `ObjectCreateMessages`, ignoring the optional aspect of some fields.
* @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding
* @param objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param parentInfo the relationship between this object and another object (its parent)
* @param data the data used to construct this type of object
* @return an ObjectCreateMessage
*/
def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage =
ObjectCreateMessage(streamLength, objectClass, guid, Some(parentInfo), Some(data))
type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil
type outPattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil
/**
@ -146,44 +158,7 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
/**
* Calculate the stream length in number of bits by factoring in the whole message in two portions.
* @param parentInfo if defined, information about the parent
* @param data the data length is indeterminate until it is read
* @return the total length of the stream in bits
*/
private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : BitVector) : Long = {
//knowable length
val first : Long = commonMsgLen(parentInfo)
//data length
var second : Long = data.size
val secondMod4 : Long = second % 4L
if(secondMod4 > 0L) {
//pad to include last whole nibble
second += 4L - secondMod4
}
first + second
}
/**
* Calculate the stream length in number of bits by factoring in the whole message in two portions.
* @param parentInfo if defined, information about the parent
* @param data the data length is indeterminate until it is read
* @return the total length of the stream in bits
*/
private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : ConstructorData) : Long = {
//knowable length
val first : Long = commonMsgLen(parentInfo)
//data length
var second : Long = data.bitsize
val secondMod4 : Long = second % 4L
if(secondMod4 > 0L) {
//pad to include last whole nibble
second += 4L - secondMod4
}
first + second
}
/**
* Calculate the length (in number of bits) of the basic packet message region.<br>
* This process automates for: object encoding.<br>
* <br>
* Ignoring the parent data, constant field lengths have already been factored into the results.
* That includes:
@ -192,17 +167,26 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
* 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 parentInfo adds either 24u or 32u
* @return the length, including the optional parent data
* @param parentInfo if defined, information about the parent adds either 24u or 32u
* @param data the data length is indeterminate until it is walked-through
* @return the total length of the stream in bits
*/
private def commonMsgLen(parentInfo : Option[ObjectCreateMessageParent]) : Long = {
if(parentInfo.isDefined) {
//(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u))
if(parentInfo.get.slot > 127) 92L else 84L
private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : ConstructorData) : Long = {
//knowable length
val first : Long = if(parentInfo.isDefined) {
if(parentInfo.get.slot > 127) 92L else 84L //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u))
}
else {
60L
}
//object length
var second : Long = data.bitsize
val secondMod4 : Long = second % 4L
if(secondMod4 > 0L) {
//pad to include last whole nibble
second += 4L - secondMod4
}
first + second
}
implicit val codec : Codec[ObjectCreateMessage] = (
@ -226,25 +210,27 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
}
) :+
("data" | bits)) //greed is good
).xmap[outPattern](
).exmap[outPattern] (
{
case _ :: _ :: _ :: _ :: BitVector.empty :: HNil =>
Attempt.failure(Err("no data to decode"))
case len :: cls :: guid :: par :: data :: HNil =>
len :: cls :: guid :: par :: decodeData(cls, data) :: HNil
}, {
Attempt.successful(len :: cls :: guid :: par :: decodeData(cls, data) :: HNil)
},
{
case _ :: _ :: _ :: _ :: None :: HNil =>
Attempt.failure(Err("no object to encode"))
case _ :: cls :: guid :: par :: Some(obj) :: HNil =>
streamLen(par, obj) :: cls :: guid :: par :: encodeData(cls, obj) :: HNil
case _ :: cls :: guid :: par :: None :: HNil =>
streamLen(par, BitVector.empty) :: cls :: guid :: par :: BitVector.empty :: HNil
Attempt.successful(streamLen(par, obj) :: cls :: guid :: par :: encodeData(cls, obj) :: HNil)
}
).exmap[ObjectCreateMessage](
).xmap[ObjectCreateMessage] (
{
case len :: cls :: guid :: par :: obj :: HNil =>
Attempt.successful(ObjectCreateMessage(len, cls, guid, par, obj))
}, {
case ObjectCreateMessage(_, _, _, _, None) =>
Attempt.failure(Err("no object to encode"))
ObjectCreateMessage(len, cls, guid, par, obj)
},
{
case ObjectCreateMessage(len, cls, guid, par, obj) =>
Attempt.successful(len :: cls :: guid :: par :: obj :: HNil)
len :: cls :: guid :: par :: obj :: HNil
}
).as[ObjectCreateMessage]
}
}

View file

@ -6,8 +6,21 @@ import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the ammunition portion of `ObjectCreateMessage` packet data.
* When alone, this data will help construct a "box" of that type of ammunition, hence the name.<br>
* <br>
* Exploration:<br>
* This class may need to be rewritten later to support objects spawned in the world environment.
* @param magazine the number of rounds available
*/
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 = 39L
}
@ -29,6 +42,9 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] {
}
)
/**
* Transform between AmmoBoxData and ConstructorData.
*/
val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] (
{
case x =>

View file

@ -5,73 +5,27 @@ import net.psforever.packet.{Marshallable, PacketHelpers}
import net.psforever.types.Vector3
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
case class CharacterData(pos : Vector3,
objYaw : Int,
faction : Int,
bops : Boolean,
name : String,
exosuit : Int,
sex : Int,
face1 : Int,
face2 : Int,
voice : Int,
unk1 : Int, //0x8080
unk2 : Int, //0xFFFF or 0x0
unk3 : Int, //2
viewPitch : Int,
viewYaw : Int,
ribbons : RibbonBars,
healthMax : Int,
health : Int,
armor : Int,
unk4 : Int, //1
unk5 : Int, //7
unk6 : Int, //7
staminaMax : Int,
stamina : Int,
unk7 : Int, // 28
unk8 : Int, //4
unk9 : Int, //44
unk10 : Int, //84
unk11 : Int, //104
unk12 : Int, //1900
firstTimeEvent_length : Long,
firstEntry : Option[String],
firstTimeEvent_list : List[String],
tutorial_list : List[String],
inventory : InventoryData
) extends ConstructorData {
override def bitsize : Long = {
//represents static fields (includes medals.bitsize)
val first : Long = 1194L //TODO due to changing understanding of the bit patterns in this data, this value will change
//name
val second : Long = CharacterData.stringBitSize(name, 16) + 4L //plus the padding
//fte_list
var third : Long = 32L
if(firstEntry.isDefined) {
third += CharacterData.stringBitSize(firstEntry.get) + 5L //plus the padding
for(str <- firstTimeEvent_list) {
third += CharacterData.stringBitSize(str)
}
}
//tutorial list
var fourth : Long = 32L
for(str <- tutorial_list) {
fourth += CharacterData.stringBitSize(str)
}
first + second + third + fourth + inventory.bitsize
}
}
case class CharacterAppearanceData(pos : Vector3,
objYaw : Int,
faction : Int,
bops : Boolean,
name : String,
exosuit : Int,
sex : Int,
face1 : Int,
face2 : Int,
voice : Int,
unk1 : Int, //0x8080
unk2 : Int, //0xFFFF or 0x0
unk3 : Int, //2
viewPitch : Int,
viewYaw : Int,
ribbons : RibbonBars)
object CharacterData extends Marshallable[CharacterData] {
private def stringBitSize(str : String, width : Int = 8) : Long = {
val strlen = str.length
val lenSize = if(strlen > 127) 16L else 8L
lenSize + strlen * width
}
implicit val codec : Codec[CharacterData] = (
object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
implicit val codec : Codec[CharacterAppearanceData] = (
("pos" | Vector3.codec_pos) ::
ignore(16) ::
("objYaw" | uint8L) ::
@ -91,12 +45,215 @@ object CharacterData extends Marshallable[CharacterData] {
ignore(42) ::
("unk2" | uint16L) ::
ignore(30) ::
("unk3" | uintL(4)) ::
("unk3" | uint4L) ::
ignore(24) ::
("viewPitch" | uint8L) ::
("viewYaw" | uint8L) ::
ignore(10) ::
("ribbons" | RibbonBars.codec) ::
("ribbons" | RibbonBars.codec)
).as[CharacterAppearanceData]
}
/**
* A representation of the avatar portion of `ObjectCreateMessage` packet data.<br>
* <br>
* This object is huge, representing the quantity of densely-encoded data in its packet.
* Although the actual organization is ill-defined, the packet can be divided into seven parts.
* The first part maintains information about the avatar as a game object in the game environment.
* The second part maintains information as an ongoing representation of the avatar.
* This includes fixed details like name and gender, though it also includes mutable aspects like exosuit type.
* The third part maintains information about career in the game.
* The fourth part maintains miscellaneous status and pose information.
* The fifth part maintains part of the statistical information about participation in the game.
* The sixth part maintains a stream of typically zero'd unknown information.
* The seventh part maintains the inventory.
* The fifth and seventh parts can inflate the size of packet significantly due to their encoding.
* The fifth, in particular, is string data that can number in the hundreds of strings(!).<br>
* <br>
* Ignoring the strings, lists of strings, and the inventory, the base length of the packet is currently __1138__ bits.
* Some undefined bits in the packet can change the length of the packet by being set or unset.
* This will mess with the encoding and the decoding of later fields.
* Any data that is padded for byte-alignment will also have its padding adjusted.
* Each string adds either 8 or 16, plus an additional 8 or 16 per the number of characters.
* For the name, that's 16 per character, a minimum of two characters, plus the (current) padding.
* for the first time events and tutorials, that's 8 per character, plus the (current) padding of the first entry.
* For the first time events and tutorials, however, the size of the list is always a 32-bit number.
* The formal inventory entries are preceded by 1 absolute bit.<br>
* <br>
* The adjusted base length is therefore __1203__ bits (1138 + 32 + 32 + 1).
* Of that, __720__ bits are unknown.
* Including the values that are defaulted, __831__ bits are perfectly unknown.
* This data is accurate as of 2016-12-07.<br>
* <br>
* Faction:<br>
* `0 - Terran Republic`<br>
* `1 - New Comglomerate`<br>
* `2 - Vanu Sovereignty`<br>
* <br>
* Exosuit:<br>
* `0 - Agile`<br>
* `1 - Refinforced`<br>
* `2 - Mechanized Assault`<br>
* `3 - Infiltration`<br>
* `4 - Standard`<br>
* <br>
* Sex:<br>
* `1 - Male`<br>
* `2 - Female`<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 `0x80`
// * (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 name the wide character name of the avatar
// * @param exosuit the type of exosuit the avatar will be depicted in;
// * for Black OPs, the agile exosuit and the reinforced exosuit are replaced with the Black OPs exosuits
// * @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 unk1 na;
// * defaults to `0x8080`
// * @param unk2 na;
// * defaults to `0xFFFF`;
// * may be `0x0`
// * @param unk3 na;
// * defaults to 2
// * @param viewPitch the angle with respect to the horizon 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 ground directions towards which the avatar is looking;
// * every `0x1` is 2.813 degrees counter clockwise from North;
// * every `0x10` is 45-degrees;
// * it wraps at `0x80`
// * @param ribbons the four merit commendation ribbon medals displayed on the avatar's left pauldron
// * @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 exosuit type
// * @param unk4 na;
// * defaults to 1
// * @param unk5 na;
// * defaults to 7
// * @param unk6 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 unk7 na;
// * defaults to 28
// * @param unk8 na;
// * defaults to 4
// * @param unk9 na;
// * defaults to 44
// * @param unk10 na;
// * defaults to 84
// * @param unk11 na;
// * defaults to 104
// * @param unk12 na;
// * defaults to 1900
// * @param firstTimeEvent_length the total number of first time events performed by this avatar
// * @param firstTimeEvent_firstEntry the separated "first entry" of the list of first time events performed by this avatar
// * @param firstTimeEvent_list the list of first time events performed by this avatar
// * @param tutorial_length the total number of tutorials completed by this avatar
// * @param tutorial_firstEntry the separated "first entry" of the list of tutorials completed by this avatar
// * @param tutorial_list the list of tutorials completed by this avatar
// * @param inventory the avatar's inventory
*/
case class CharacterData(appearance : CharacterAppearanceData,
healthMax : Int,
health : Int,
armor : Int,
unk4 : Int, //1
unk5 : Int, //7
unk6 : Int, //7
staminaMax : Int,
stamina : Int,
unk7 : Int, //28
unk8 : Int, //4
unk9 : Int, //44
unk10 : Int, //84
unk11 : Int, //104
unk12 : Int, //1900
firstTimeEvents : List[String],
tutorials : List[String],
inventory : InventoryData
) extends ConstructorData {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
override def bitsize : Long = {
// //represents static fields (includes medals.bitsize)
// val base : Long = 1138L //TODO ongoing analysis, this value will be subject to change
// //name
// val nameSize : Long = CharacterData.stringBitSize(appearance.name, 16) + 4L //plus the current padding
// //fte_list
// var eventListSize : Long = 32L
// if(firstTimeEvent_firstEntry.isDefined) {
// eventListSize += CharacterData.stringBitSize(firstTimeEvent_firstEntry.get) + 5L //plus the current padding
// for(str <- firstTimeEvent_list) {
// eventListSize += CharacterData.stringBitSize(str)
// }
// }
// //tutorial list
// var tutorialListSize : Long = 32L
// for(str <- tutorial_list) {
// tutorialListSize += CharacterData.stringBitSize(str)
// }
// base + nameSize + eventListSize + tutorialListSize + inventory.bitsize
0L
}
}
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
*/
private def stringBitSize(str : String, width : Int = 8) : Long = {
val strlen = str.length
val lenSize = if(strlen > 127) 16L else 8L
lenSize + (strlen * width)
}
private def ftePadding(len : Long) : Int = {
//TODO determine how this should be padded better
5
}
private def tutListPadding(len : Long) : Int = {
//TODO determine how this should be padded when len == 0
if(len > 0) 0 else 0
}
implicit val codec : Codec[CharacterData] = (
("appearance" | CharacterAppearanceData.codec) ::
ignore(160) ::
("healthMax" | uint16L) ::
("health" | uint16L) ::
@ -118,14 +275,45 @@ object CharacterData extends Marshallable[CharacterData] {
("unk12" | uintL(12)) ::
ignore(19) ::
(("firstTimeEvent_length" | uint32L) >>:~ { len =>
conditional(len > 0, "firstEntry" | PacketHelpers.encodedStringAligned(5)) ::
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) ::
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
("tutorial_list" | PacketHelpers.listOfNAligned(uint32L, 0, PacketHelpers.encodedString)) ::
ignore(207) ::
("inventory" | InventoryData.codec)
(("tutorial_length" | uint32L) >>:~ { len2 =>
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutListPadding(len) )) ::
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
ignore(207) ::
("inventory" | InventoryData.codec)
})
})
).as[CharacterData]
).xmap[CharacterData] (
{
case app :: _ :: b :: c :: _ :: d :: _ :: e :: _ :: f :: g :: h :: i :: _ :: j :: k :: l :: m :: n :: o :: _ :: p :: q :: r :: s :: t :: u :: _ :: v :: HNil =>
//prepend the displaced first elements to their lists
val fteList : List[String] = if(q.isDefined) { q.get :: r } else r
val tutList : List[String] = if(t.isDefined) { t.get :: u } else u
CharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, v)
},
{
case CharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, p) =>
//shift the first elements off their lists
var fteListCopy = fteList
var firstEvent : Option[String] = None
if(fteList.nonEmpty) {
firstEvent = Some(fteList.head)
fteListCopy = fteList.drop(1)
}
var tutListCopy = tutList
var firstTutorial : Option[String] = None
if(tutList.nonEmpty) {
firstTutorial = Some(tutList.head)
tutListCopy = tutList.drop(1)
}
app :: () :: b :: c :: () :: d :: () :: e :: () :: f :: g :: h :: i :: () :: j :: k :: l :: m :: n :: o :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: p :: HNil
}
).as[CharacterData]
/**
* Transform between CharacterData and ConstructorData.
*/
val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] (
{
case x =>

View file

@ -1,10 +1,29 @@
// Copyright (c) 2016 PSForever.net to present
package net.psforever.packet.game.objectcreate
/**
* The base type for the representation of any data used to produce objects from `ObjectCreateMessage` packet data.
* There is no reason to instantiate this class as-is.
* Children of this class are expected to be able to translate through `scodec` operations into packet data.<br>
* <br>
* The object data is uncoupled from the object class as multiple classes use the same format for their data.
* For example, both the Suppressor and the Gauss use a weapon data format.
* For example, both 9mm Bullets and energy cells use am ammunition data format.
*/
abstract class ConstructorData() {
/**
* Performs a "sizeof()" analysis of the given object.
* @return the number of bits necessary to represent this object;
* reflects the `Codec` definition rather than the parameter fields;
* defaults to `0L`
*/
def bitsize : Long = 0L
}
object ConstructorData {
/**
* This pattern is intended to provide common conversion between all of the `Codec`s of the children of this class.
* The casting will be performed through use of `exmap`.
*/
type genericPattern = Option[ConstructorData]
}
}

View file

@ -8,27 +8,36 @@ import scodec.codecs._
import shapeless.{::, HNil}
/**
* The same kind of data as required for a formal ObjectCreateMessage but with a required and implicit parent relationship.
* Data preceding this entry will define the existence of the parent.
* @param objectClass na
* @param guid na
* @param parentSlot na
* @param obj na
* The same kind of data as required for a formal `ObjectCreateMessage` but with a required and implicit parent relationship.
* Some data preceding this entry will clarify the existence of the parent.<br>
* <br>
* As indicated, an `InternalSlot` object is not a top-level object.
* This is true in relation between one object and another, as well as in how this object is sorted in the `ObjectCreateMessage` data.
* The data outlined by this class encompasses the same kind as the outer-most `ObjectCreateMessage`.
* By contrast, this object always has a dedicated parent object and a known slot to be attached to that parent.
* It's not optional.
* @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
*/
case class InternalSlot(objectClass : Int,
guid : PlanetSideGUID,
parentSlot : Int,
obj : Option[ConstructorData]) {
obj : ConstructorData) {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
def bitsize : Long = {
val first : Long = if(parentSlot > 127) 44L else 36L
val second : Long = if(obj.isDefined) obj.get.bitsize else 0L
val second : Long = obj.bitsize
first + second
}
}
object InternalSlot extends Marshallable[InternalSlot] {
type objPattern = Int :: PlanetSideGUID :: Int :: ConstructorData :: HNil
implicit val codec : Codec[InternalSlot] = (
ignore(1) :: //TODO determine what this bit does
(("objectClass" | uintL(11)) >>:~ { obj_cls =>
@ -36,5 +45,14 @@ object InternalSlot extends Marshallable[InternalSlot] {
("parentSlot" | PacketHelpers.encodedStringSize) ::
("obj" | ObjectClass.selectDataCodec(obj_cls))
})
).as[InternalSlot]
}
).xmap[InternalSlot] (
{
case _ :: cls :: guid :: slot :: Some(obj) :: HNil =>
InternalSlot(cls, guid, slot, obj)
},
{
case InternalSlot(cls, guid, slot, obj) =>
() :: cls :: guid :: slot :: Some(obj) :: HNil
}
).as[InternalSlot]
}

View file

@ -1,25 +1,59 @@
// Copyright (c) 2016 PSForever.net to present
package net.psforever.packet.game.objectcreate
import net.psforever.packet.{Marshallable, PacketHelpers}
import net.psforever.packet.Marshallable
import scodec.Codec
import scodec.codecs._
import shapeless.{::,HNil}
/**
* A representation of the inventory portion of `ObjectCreateMessage` packet data for avatars.<br>
* <br>
* Unfortunately, the inventory is a fail-fast greedy thing.
* Any format discrepancies will cause it to fail and that will cause character encoding to fail as well.
* Care should be taken that all possible item encodings are representable.
* @param unk1 na;
* always `true` to mark the start of the inventory data?
* @param unk2 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
*/
case class InventoryData(unk1 : Boolean,
size : Int,
unk2 : Boolean){//,
//inv : List[InventoryItem]) {
unk2 : Boolean,
contents : Vector[InventoryItem]) {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
def bitsize : Long = {
10L
//two booleans and the 8-bit length field
val first : Long = 10L
//length of all items in inventory
var second : Long = 0L
for(item <- contents) {
second += item.bitsize
}
first + second
}
}
object InventoryData extends Marshallable[InventoryData] {
implicit val codec : Codec[InventoryData] = (
("unk1" | bool) ::
(("len" | uint8L) >>:~ { len =>
("unk2" | bool).hlist// ::
//("inv" | PacketHelpers.listOfNSized(len, InventoryItem.codec))
})
).as[InventoryData]
("len" | uint8L) ::
("unk2" | bool) ::
("contents" | vector(InventoryItem.codec))
).xmap[InventoryData] (
{
case u1 :: _ :: u2 :: vector :: HNil =>
InventoryData(u1, u2, vector)
},
{
case InventoryData(u1, u2, vector) =>
u1 :: vector.length :: u2 :: vector :: HNil
}
).as[InventoryData]
}

View file

@ -2,13 +2,60 @@
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.Codec
import scodec.codecs._
case class InventoryItem(item : InternalSlot)
/**
* Represent an item in inventory.<br>
* <br>
* Note the use of `InternalSlot` to indicate the implicit parent ownership of the resulting item.
* Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible.
* @param item the object in inventory
* @param na the user should not have to worry about this potential bit;
* it follows after weapon entries, allegedly
*/
case class InventoryItem(item : InternalSlot,
na : Option[Boolean] = None) {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
def bitsize : Long = {
//item
val first : Long = item.bitsize
//trailing bit
val second : Long = if(na.isDefined) 1L else 0L
first + second
}
}
object InventoryItem extends Marshallable[InventoryItem] {
/**
* An abbreviated constructor for creating an `InventoryItem` without interacting with `InternalSlot` directly.
* @param objClass the code for the type of object (ammunition) being constructed
* @param guid the globally unique id assigned to the ammunition
* @param parentSlot the slot where the ammunition is to be installed in the weapon
* @param obj the constructor data
* @return an InventoryItem
*/
def apply(objClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : ConstructorData) : InventoryItem = {
val isWep = if(obj.isInstanceOf[WeaponData]) Some(false) else None
//TODO is this always Some(false)?
InventoryItem(InternalSlot(objClass, guid, parentSlot, obj), isWep)
}
/**
* Determine whether the allocated item is a weapon.
* @param itm the inventory item
* @return true, if the item is a weapon; false, otherwise
*/
def wasWeapon(itm : InternalSlot) : Boolean = itm.obj.isInstanceOf[WeaponData]
implicit val codec : Codec[InventoryItem] = (
"item" | InternalSlot.codec
).as[InventoryItem]
("item" | InternalSlot.codec) >>:~ { item =>
conditional(wasWeapon(item), bool).hlist
}
).as[InventoryItem]
}

View file

@ -6,37 +6,68 @@ import scodec.codecs._
import scala.annotation.switch
/**
* A reference between all object class codes and the name of the object they represent.<br>
* <br>
* Object classes compose a number between `0` and (probably) `2047`, always translating into an 11-bit value.
* They are recorded as little-endian hexadecimal values here.
*/
object ObjectClass {
//character
final val PLAYER = 0x79
final val AVATAR = 0x79
//ammunition
final val BULLETS_9MM = 0x1C
final val BULLETS_9MM_AP = 0x1D
final val ENERGY_CELL = 0x110
final val JAMMER_GRENADE_AMMO = 0x1A1
final val FORCE_BLADE_AMMO = 0x21C
final val PLASMA_GRENADE_AMMO = 0x2A9
final val BUCKSHOT = 0x2F3 //TODO apply internal name
//weapons
final val SUPPRESSOR = 0x34D
final val BEAMER = 0x8C
final val SWEEPER = 0x130
final val FORCE_BLADE = 0x144
final val GAUSS = 0x159
final val SUPPRESSOR = 0x34D
final val JAMMER_GRENADE = 0x1A0
final val PLASMA_GRENADE = 0x2A8
//tools
final val MEDKIT = 0x218
final val REK = 0x2D8
//unknown
final val SLOT_BLOCKER = 0x1C8
final val SLOT_BLOCKER = 0x1C8 //strange item found in slot #5, between holsters and inventory
//TODO refactor this function into another object later
/**
* Given an object class, retrieve the `Codec` used to parse and translate the constructor data for that type.<br>
* <br>
* This function serves as a giant `switch` statement that loosely connects object data to object class.
* All entries, save the default, merely point to the `Codec` of pattern `ConstructorData.genericPattern`.
* This pattern connects all `Codec`s back to the superclass `ConstructorData`.
* The default case is a failure case for trying to either decode or encode an unknown class of object.
* @param objClass the code for the type of object being constructed
* @return the `Codec` that handles the format of data for that particular item class, or a failing codec
*/
def selectDataCodec(objClass : Int) : Codec[ConstructorData.genericPattern] = {
(objClass : @switch) match {
case ObjectClass.PLAYER => CharacterData.genericCodec
case ObjectClass.AVATAR => CharacterData.genericCodec
case ObjectClass.BEAMER => WeaponData.genericCodec
case ObjectClass.BUCKSHOT => AmmoBoxData.genericCodec
case ObjectClass.BULLETS_9MM => AmmoBoxData.genericCodec
case ObjectClass.BULLETS_9MM_AP => AmmoBoxData.genericCodec
case ObjectClass.ENERGY_CELL => AmmoBoxData.genericCodec
case ObjectClass.FORCE_BLADE_AMMO => AmmoBoxData.genericCodec
case ObjectClass.BEAMER => WeaponData.genericCodec
case ObjectClass.FORCE_BLADE => WeaponData.genericCodec
case ObjectClass.GAUSS => WeaponData.genericCodec
case ObjectClass.SUPPRESSOR => WeaponData.genericCodec
case ObjectClass.JAMMER_GRENADE => WeaponData.genericCodec
case ObjectClass.JAMMER_GRENADE_AMMO => AmmoBoxData.genericCodec
case ObjectClass.MEDKIT => AmmoBoxData.genericCodec
case ObjectClass.PLASMA_GRENADE => WeaponData.genericCodec
case ObjectClass.PLASMA_GRENADE_AMMO => AmmoBoxData.genericCodec
case ObjectClass.REK => REKData.genericCodec
case ObjectClass.SLOT_BLOCKER => AmmoBoxData.genericCodec
case ObjectClass.SUPPRESSOR => WeaponData.genericCodec
case ObjectClass.SWEEPER => WeaponData.genericCodec
//failure case
case _ => conditional(false, bool).exmap[ConstructorData.genericPattern] (
{

View file

@ -6,8 +6,18 @@ import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the REK portion of `ObjectCreateMessage` packet data.
* When alone, this data will help construct the "tool" called a Remote Electronics Kit.
* @param unk na
*/
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 = 72L
}
@ -33,8 +43,9 @@ object REKData extends Marshallable[REKData] {
}
).as[REKData]
/**
* Transform between REKData and ConstructorData.
*/
val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] (
{
case x =>

View file

@ -5,10 +5,26 @@ import net.psforever.packet.Marshallable
import scodec.Codec
import scodec.codecs._
case class RibbonBars(upper : Long = 0xFFFFFFFFL, //0xFFFFFFFF means no merit (for all ...)
/**
* Enumerate the player-displayed merit commendation awards granted for excellence (or tenacity) in combat.
* These are the medals players wish to brandish on their left pauldron.<br>
* <br>
* All merit commendation ribbons are represented by a 32-bit signature.
* The default "no-ribbon" value is `0xFFFFFFFF`.
* @param upper the "top" configurable merit ribbon
* @param middle the central configurable merit ribbon
* @param lower the lower configurable merit ribbon
* @param tos the automatic top-most term of service merit ribbon
*/
case class RibbonBars(upper : Long = 0xFFFFFFFFL,
middle : Long = 0xFFFFFFFFL,
lower : Long = 0xFFFFFFFFL,
tos : Long = 0xFFFFFFFFL) {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
def bitsize : Long = 128L
}

View file

@ -7,14 +7,42 @@ import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the weapon portion of `ObjectCreateMessage` packet data.
* When alone, this data will help construct a "weapon" such as Suppressor.<br>
* <br>
* The data for the weapon also nests required default ammunition data.
* Where the ammunition is loaded is considered the "first slot."
* @param unk na
* @param ammo data regarding the currently loaded ammunition type and quantity
* @see AmmoBoxData
*/
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 = 59L + ammo.bitsize
}
object WeaponData extends Marshallable[WeaponData] {
/**
* An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.<br>
* <br>
* Exploration:<br>
* This class may need to be rewritten later to support objects spawned in the world environment.
* @param unk na
* @param cls the code for the type of object (ammunition) being constructed
* @param guid the globally unique id assigned to the ammunition
* @param parentSlot the slot where the ammunition is to be installed in the weapon
* @param ammo the constructor data for the ammunition
* @return a WeaponData object
*/
def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData =
new WeaponData(unk, InternalSlot(cls, guid, parentSlot, Some(ammo)))
new WeaponData(unk, InternalSlot(cls, guid, parentSlot, ammo))
implicit val codec : Codec[WeaponData] = (
("unk" | uint4L) ::
@ -37,8 +65,9 @@ object WeaponData extends Marshallable[WeaponData] {
}
).as[WeaponData]
/**
* Transform between WeaponData and ConstructorData.
*/
val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] (
{
case x =>

View file

@ -6,7 +6,7 @@ import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate._
import net.psforever.types._
import scodec.Attempt
import scodec.{Attempt, Err}
import scodec.Attempt.Successful
import scodec.bits._
@ -164,16 +164,16 @@ class GamePacketTest extends Specification {
val invData = InventoryItem.codec.decode(invTestWep.toBitVector.drop(1)).toOption
invData.isDefined mustEqual true
InventoryData.codec.decode(invTest.toBitVector.drop(7)).toOption match {
case Some(x) =>
x.value.unk1 equals true
x.value.size mustEqual 1
x.value.unk2 mustEqual false
//x.value.inv.head.item.objectClass mustEqual 0x8C
//x.value.inv.head.na mustEqual false
case _ =>
ko
}
// InventoryData.codec.decode(invTest.toBitVector.drop(7)) match {
// case Attempt.Successful(x) =>
// x.value.unk1 equals true
// x.value.size mustEqual 1
// x.value.unk2 mustEqual false
// //x.value.inv.head.item.objectClass mustEqual 0x8C
// //x.value.inv.head.na mustEqual false
// case Attempt.Failure(x) =>
// x.message mustEqual ""
// }
}
"decode (2)" in {
@ -190,7 +190,7 @@ class GamePacketTest extends Specification {
}
}
"decode (char)" in {
"decode (character)" in {
PacketCoding.DecodePacket(string_testchar).require match {
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
len mustEqual 3159
@ -200,27 +200,27 @@ class GamePacketTest extends Specification {
data.isDefined mustEqual true
val char = data.get.asInstanceOf[CharacterData]
char.pos.x mustEqual 3674.8438f
char.pos.y mustEqual 2726.789f
char.pos.z mustEqual 91.15625f
char.objYaw mustEqual 19
char.faction mustEqual 2 //vs
char.bops mustEqual false
char.name mustEqual "IlllIIIlllIlIllIlllIllI"
char.exosuit mustEqual 4 //standard
char.sex mustEqual 2 //female
char.face1 mustEqual 2
char.face2 mustEqual 9
char.voice mustEqual 1 //female 1
char.unk1 mustEqual 0x8080
char.unk2 mustEqual 0xFFFF
char.unk3 mustEqual 2
char.viewPitch mustEqual 0xFF
char.viewYaw mustEqual 0x6A
char.ribbons.upper mustEqual 0xFFFFFFFFL //none
char.ribbons.middle mustEqual 0xFFFFFFFFL //none
char.ribbons.lower mustEqual 0xFFFFFFFFL //none
char.ribbons.tos mustEqual 0xFFFFFFFFL //none
char.appearance.pos.x mustEqual 3674.8438f
char.appearance.pos.y mustEqual 2726.789f
char.appearance.pos.z mustEqual 91.15625f
char.appearance.objYaw mustEqual 19
char.appearance.faction mustEqual 2 //vs
char.appearance.bops mustEqual false
char.appearance.name mustEqual "IlllIIIlllIlIllIlllIllI"
char.appearance.exosuit mustEqual 4 //standard
char.appearance.sex mustEqual 2 //female
char.appearance.face1 mustEqual 2
char.appearance.face2 mustEqual 9
char.appearance.voice mustEqual 1 //female 1
char.appearance.unk1 mustEqual 0x8080
char.appearance.unk2 mustEqual 0xFFFF
char.appearance.unk3 mustEqual 2
char.appearance.viewPitch mustEqual 0xFF
char.appearance.viewYaw mustEqual 0x6A
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
@ -235,16 +235,78 @@ class GamePacketTest extends Specification {
char.unk10 mustEqual 84
char.unk11 mustEqual 104
char.unk12 mustEqual 1900
char.firstTimeEvent_length mustEqual 4
char.firstEntry mustEqual Some("xpe_sanctuary_help")
char.firstTimeEvent_list.size mustEqual 3
char.firstTimeEvent_list.head mustEqual "xpe_th_firemodes"
char.firstTimeEvent_list(1) mustEqual "used_beamer"
char.firstTimeEvent_list(2) mustEqual "map13"
char.tutorial_list.size mustEqual 0
char.firstTimeEvents.size mustEqual 4
char.firstTimeEvents.head mustEqual "xpe_sanctuary_help"
char.firstTimeEvents(1) mustEqual "xpe_th_firemodes"
char.firstTimeEvents(2) mustEqual "used_beamer"
char.firstTimeEvents(3) mustEqual "map13"
char.tutorials.size mustEqual 0
char.inventory.unk1 mustEqual true
char.inventory.size mustEqual 10
char.inventory.unk2 mustEqual false
char.inventory.contents.length mustEqual 10
val inventory = char.inventory.contents
//0
inventory.head.item.objectClass mustEqual 0x8C //beamer
inventory.head.item.guid mustEqual PlanetSideGUID(76)
inventory.head.item.parentSlot mustEqual 0
var wep = inventory.head.item.obj.asInstanceOf[WeaponData]
wep.ammo.objectClass mustEqual 0x110 //plasma
wep.ammo.guid mustEqual PlanetSideGUID(77)
wep.ammo.parentSlot mustEqual 0
wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 16
//1
inventory(1).item.objectClass mustEqual 0x34D //suppressor
inventory(1).item.guid mustEqual PlanetSideGUID(78)
inventory(1).item.parentSlot mustEqual 2
wep = inventory(1).item.obj.asInstanceOf[WeaponData]
wep.ammo.objectClass mustEqual 0x1C //9mm
wep.ammo.guid mustEqual PlanetSideGUID(79)
wep.ammo.parentSlot mustEqual 0
wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 25
//2
inventory(2).item.objectClass mustEqual 0x144 //force blade
inventory(2).item.guid mustEqual PlanetSideGUID(80)
inventory(2).item.parentSlot mustEqual 4
wep = inventory(2).item.obj.asInstanceOf[WeaponData]
wep.ammo.objectClass mustEqual 0x21C //force blade ammo
wep.ammo.guid mustEqual PlanetSideGUID(81)
wep.ammo.parentSlot mustEqual 0
wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1
//3
inventory(3).item.objectClass mustEqual 0x1C8 //thing
inventory(3).item.guid mustEqual PlanetSideGUID(82)
inventory(3).item.parentSlot mustEqual 5
inventory(3).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1
//4
inventory(4).item.objectClass mustEqual 0x1C //9mm
inventory(4).item.guid mustEqual PlanetSideGUID(83)
inventory(4).item.parentSlot mustEqual 6
inventory(4).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50
//5
inventory(5).item.objectClass mustEqual 0x1C //9mm
inventory(5).item.guid mustEqual PlanetSideGUID(84)
inventory(5).item.parentSlot mustEqual 9
inventory(5).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50
//6
inventory(6).item.objectClass mustEqual 0x1C //9mm
inventory(6).item.guid mustEqual PlanetSideGUID(85)
inventory(6).item.parentSlot mustEqual 12
inventory(6).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50
//7
inventory(7).item.objectClass mustEqual 0x1D //9mm ap
inventory(7).item.guid mustEqual PlanetSideGUID(86)
inventory(7).item.parentSlot mustEqual 33
inventory(7).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50
//8
inventory(8).item.objectClass mustEqual 0x110 //plasma
inventory(8).item.guid mustEqual PlanetSideGUID(87)
inventory(8).item.parentSlot mustEqual 36
inventory(8).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50
//9
inventory(9).item.objectClass mustEqual 0x2D8 //rek
inventory(9).item.guid mustEqual PlanetSideGUID(88)
inventory(9).item.parentSlot mustEqual 39
//the rek has data but none worth testing here
case default =>
ko
}
@ -260,8 +322,7 @@ class GamePacketTest extends Specification {
parent.get.guid mustEqual PlanetSideGUID(75)
parent.get.slot mustEqual 33
data.isDefined mustEqual true
val obj = data.get.asInstanceOf[AmmoBoxData]
obj.magazine mustEqual 50
data.get.asInstanceOf[AmmoBoxData].magazine mustEqual 50
case default =>
ko
}
@ -283,9 +344,7 @@ class GamePacketTest extends Specification {
obj_ammo.objectClass mustEqual 28
obj_ammo.guid mustEqual PlanetSideGUID(1286)
obj_ammo.parentSlot mustEqual 0
obj_ammo.obj.isDefined mustEqual true
val ammo = obj_ammo.obj.get.asInstanceOf[AmmoBoxData]
ammo.magazine mustEqual 30
obj_ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30
case default =>
ko
}
@ -298,16 +357,16 @@ class GamePacketTest extends Specification {
}
"encode (9mm)" in {
val obj : ConstructorData = AmmoBoxData(50).asInstanceOf[ConstructorData]
val msg = ObjectCreateMessage(0, 28, PlanetSideGUID(1280), Some(ObjectCreateMessageParent(PlanetSideGUID(75), 33)), Some(obj))
val obj = AmmoBoxData(50)
val msg = ObjectCreateMessage(0, 28, PlanetSideGUID(1280), ObjectCreateMessageParent(PlanetSideGUID(75), 33), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_9mm
}
"encode (gauss)" in {
val obj : ConstructorData = WeaponData(4, 28, PlanetSideGUID(1286), 0, AmmoBoxData(30)).asInstanceOf[ConstructorData]
val msg = ObjectCreateMessage(0, 345, PlanetSideGUID(1465), Some(ObjectCreateMessageParent(PlanetSideGUID(75), 2)), Some(obj))
val obj = WeaponData(4, 28, PlanetSideGUID(1286), 0, AmmoBoxData(30))
val msg = ObjectCreateMessage(0, 345, PlanetSideGUID(1465), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_gauss