working encoding and decoding of some objects for ObjectCreateMessage; check tests and WSActor

This commit is contained in:
FateJH 2016-12-12 20:05:42 -05:00
parent 3b9f3a6f33
commit 509ab8ec57
14 changed files with 320 additions and 114 deletions

View file

@ -18,8 +18,8 @@ import shapeless.{::, HNil}
* @param guid the GUID of the parent object * @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 * @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent
*/ */
case class ObjectCreateMessageParent(guid : PlanetSideGUID, final case class ObjectCreateMessageParent(guid : PlanetSideGUID,
slot : Int) slot : Int)
/** /**
* Communicate with the client that a certain object with certain properties is to be created. * Communicate with the client that a certain object with certain properties is to be created.
@ -45,14 +45,15 @@ case class ObjectCreateMessageParent(guid : PlanetSideGUID,
* @param objectClass the code for the type of object being constructed * @param objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned * @param guid the GUID this object will be assigned
* @param parentInfo if defined, the relationship between this object and another object (its parent) * @param parentInfo if defined, the relationship between this object and another object (its parent)
* @param data if defined, the data used to construct this type of object * @param data the data used to construct this type of object;
* on decoding, set to `None` if the process failed
* @see ObjectClass.selectDataCodec * @see ObjectClass.selectDataCodec
*/ */
case class ObjectCreateMessage(streamLength : Long, final case class ObjectCreateMessage(streamLength : Long,
objectClass : Int, objectClass : Int,
guid : PlanetSideGUID, guid : PlanetSideGUID,
parentInfo : Option[ObjectCreateMessageParent], parentInfo : Option[ObjectCreateMessageParent],
data : Option[ConstructorData]) data : Option[ConstructorData])
extends PlanetSideGamePacket { extends PlanetSideGamePacket {
def opcode = GamePacketOpcode.ObjectCreateMessage def opcode = GamePacketOpcode.ObjectCreateMessage
def encode = ObjectCreateMessage.encode(this) def encode = ObjectCreateMessage.encode(this)

View file

@ -2,6 +2,7 @@
package net.psforever.packet.game.objectcreate package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.{Attempt, Codec, Err} import scodec.{Attempt, Codec, Err}
import scodec.codecs._ import scodec.codecs._
import shapeless.{::, HNil} import shapeless.{::, HNil}
@ -13,12 +14,11 @@ import shapeless.{::, HNil}
* <br> * <br>
* The maximum amount of ammunition that can be stored in a single box is 65535 units. * 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. * Regardless of the interface, however, the number will never be fully visible.
* Only the first three digits or first four digits may be represented. * Only the first three digits or the first four digits may be represented.
* @param magazine the number of rounds available * @param magazine the number of rounds available
* @see WeaponData * @see WeaponData
*/ */
case class AmmoBoxData(magazine : Int final case class AmmoBoxData(magazine : Int) extends ConstructorData {
) extends ConstructorData {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
@ -28,9 +28,20 @@ case class AmmoBoxData(magazine : Int
} }
object AmmoBoxData extends Marshallable[AmmoBoxData] { object AmmoBoxData extends Marshallable[AmmoBoxData] {
/**
* An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot`.
* @param cls the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent
* @param ammo the `AmmoBoxData`
* @return an `InternalSlot` object that encapsulates `AmmoBoxData`
*/
def apply(cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : InternalSlot =
new InternalSlot(cls, guid, parentSlot, ammo)
implicit val codec : Codec[AmmoBoxData] = ( implicit val codec : Codec[AmmoBoxData] = (
uintL(8) :: uint8L ::
uintL(15) :: uint(15) ::
("magazine" | uint16L) :: ("magazine" | uint16L) ::
bool bool
).exmap[AmmoBoxData] ( ).exmap[AmmoBoxData] (
@ -38,7 +49,7 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] {
case 0xC8 :: 0 :: mag :: false :: HNil => case 0xC8 :: 0 :: mag :: false :: HNil =>
Attempt.successful(AmmoBoxData(mag)) Attempt.successful(AmmoBoxData(mag))
case a :: b :: _ :: d :: HNil => case a :: b :: _ :: d :: HNil =>
Attempt.failure(Err("illegal ammunition data format")) Attempt.failure(Err("invalid ammunition data format"))
}, },
{ {
case AmmoBoxData(mag) => case AmmoBoxData(mag) =>

View file

@ -89,27 +89,27 @@ import shapeless.{::, HNil}
* @param unk8 na * @param unk8 na
* @param ribbons the four merit commendation ribbon medals * @param ribbons the four merit commendation ribbon medals
*/ */
case class CharacterAppearanceData(pos : Vector3, final case class CharacterAppearanceData(pos : Vector3,
objYaw : Int, objYaw : Int,
faction : Int, faction : Int,
bops : Boolean, bops : Boolean,
unk1 : Int, unk1 : Int,
name : String, name : String,
exosuit : Int, exosuit : Int,
sex : Int, sex : Int,
face1 : Int, face1 : Int,
face2 : Int, face2 : Int,
voice : Int, voice : Int,
unk2 : Int, unk2 : Int,
unk3 : Int, unk3 : Int,
unk4 : Int, unk4 : Int,
unk5 : Int, unk5 : Int,
unk6 : Int, unk6 : Int,
unk7 : Int, unk7 : Int,
viewPitch : Int, viewPitch : Int,
viewYaw : Int, viewYaw : Int,
unk8 : Int, unk8 : Int,
ribbons : RibbonBars) extends StreamBitSize { ribbons : RibbonBars) extends StreamBitSize {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
@ -138,18 +138,18 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
ignore(16) :: ignore(16) ::
("objYaw" | uint8L) :: ("objYaw" | uint8L) ::
ignore(1) :: ignore(1) ::
("faction" | uintL(2)) :: ("faction" | uint2L) ::
("bops" | bool) :: ("bops" | bool) ::
("unk1" | uint4L) :: ("unk1" | uint4L) ::
ignore(16) :: ignore(16) ::
("name" | PacketHelpers.encodedWideStringAligned( namePadding )) :: ("name" | PacketHelpers.encodedWideStringAligned( namePadding )) ::
("exosuit" | uintL(3)) :: ("exosuit" | uintL(3)) ::
ignore(2) :: ignore(2) ::
("sex" | uintL(2)) :: ("sex" | uint2L) ::
("face1" | uint4L) :: ("face1" | uint4L) ::
("face2" | uint4L) :: ("face2" | uint4L) ::
("voice" | uintL(3)) :: ("voice" | uintL(3)) ::
("unk2" | uintL(2)) :: ("unk2" | uint2L) ::
ignore(4) :: ignore(4) ::
("unk3" | uint8L) :: ("unk3" | uint8L) ::
("unk4" | uint8L) :: ("unk4" | uint8L) ::
@ -219,29 +219,29 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
* the size field is a 32-bit number; * the size field is a 32-bit number;
* the first entry may be padded * the first entry may be padded
* @param tutorials the list of tutorials completed by this avatar; * @param tutorials the list of tutorials completed by this avatar;
* the size field is a 32-bit number; * the size field is a 32-bit number;
* the first entry may be padded * the first entry may be padded
* @param inventory the avatar's inventory * @param inventory the avatar's inventory
*/ */
case class CharacterData(appearance : CharacterAppearanceData, final case class CharacterData(appearance : CharacterAppearanceData,
healthMax : Int, healthMax : Int,
health : Int, health : Int,
armor : Int, armor : Int,
unk1 : Int, //1 unk1 : Int, //1
unk2 : Int, //7 unk2 : Int, //7
unk3 : Int, //7 unk3 : Int, //7
staminaMax : Int, staminaMax : Int,
stamina : Int, stamina : Int,
unk4 : Int, //28 unk4 : Int, //28
unk5 : Int, //4 unk5 : Int, //4
unk6 : Int, //44 unk6 : Int, //44
unk7 : Int, //84 unk7 : Int, //84
unk8 : Int, //104 unk8 : Int, //104
unk9 : Int, //1900 unk9 : Int, //1900
firstTimeEvents : List[String], firstTimeEvents : List[String],
tutorials : List[String], tutorials : List[String],
inventory : InventoryData inventory : InventoryData
) extends ConstructorData { ) extends ConstructorData {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
@ -310,7 +310,7 @@ object CharacterData extends Marshallable[CharacterData] {
if(len > 0) //automatic alignment from previous List if(len > 0) //automatic alignment from previous List
0 0
else if(len2 > 0) //need to align for elements else if(len2 > 0) //need to align for elements
1 5
else //both lists are empty else //both lists are empty
0 0
} }

View file

@ -0,0 +1,106 @@
// Copyright (c) 2016 PSForever.net to present
package net.psforever.packet.game.objectcreate
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.packet.{Marshallable, PacketHelpers}
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of a class of weapons that can be created using `ObjectCreateMessage` packet data.
* A "concurrent feed weapon" refers to a weapon system that can chamber multiple types of ammunition simultaneously.
* This data will help construct a "weapon" such as a Punisher.<br>
* <br>
* The data for the weapons nests information for the default (current) type and number of ammunition in its magazine.
* This ammunition data essentially is the weapon's magazines as numbered slots.
* @param unk na
* @param ammo `List` data regarding the currently loaded ammunition types and quantities
* @see WeaponData
* @see AmmoBoxData
*/
final case class ConcurrentFeedWeaponData(unk : Int,
ammo : List[InternalSlot]) extends ConstructorData {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @see InternalSlot.bitsize
* @see AmmoBoxData.bitsize
* @return the number of bits necessary to represent this object
*/
override def bitsize : Long = {
var bitsize : Long = 0L
for(o <- ammo) {
bitsize += o.bitsize
}
61L + bitsize
}
}
object ConcurrentFeedWeaponData extends Marshallable[ConcurrentFeedWeaponData] {
/**
* An abbreviated constructor for creating `ConcurrentFeedWeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.<br>
* <br>
* Exploration:<br>
* This class may need to be rewritten later to support objects spawned in the world environment.
* @param unk na
* @param cls the code for the type of object (ammunition) being constructed
* @param guid the globally unique id assigned to the ammunition
* @param parentSlot the slot where the ammunition is to be installed in the weapon
* @param ammo the constructor data for the ammunition
* @return a WeaponData object
*/
def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : ConcurrentFeedWeaponData =
new ConcurrentFeedWeaponData(unk, InternalSlot(cls, guid, parentSlot, ammo) :: Nil)
implicit val codec : Codec[ConcurrentFeedWeaponData] = (
("unk" | uint4L) ::
uint4L ::
uint24 ::
uint16 ::
uint2L ::
(uint8L >>:~ { size =>
uint2L ::
("ammo" | PacketHelpers.listOfNSized(size, InternalSlot.codec)) ::
bool
})
).exmap[ConcurrentFeedWeaponData] (
{
case code :: 8 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil =>
if(size != ammo.size)
Attempt.failure(Err("weapon encodes wrong number of ammunition"))
else if(size == 0)
Attempt.failure(Err("weapon needs to encode at least one type of ammunition"))
else
Attempt.successful(ConcurrentFeedWeaponData(code, ammo))
case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid weapon data format"))
},
{
case ConcurrentFeedWeaponData(code, ammo) =>
val size = ammo.size
if(size == 0)
Attempt.failure(Err("weapon needs to encode at least one type of ammunition"))
else if(size >= 255)
Attempt.failure(Err("weapon has too much ammunition (255+ types!)"))
else
Attempt.successful(code :: 8 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil)
}
).as[ConcurrentFeedWeaponData]
/**
* Transform between WeaponData and ConstructorData.
*/
val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] (
{
case x =>
Attempt.successful(Some(x.asInstanceOf[ConstructorData]))
},
{
case Some(x) =>
Attempt.successful(x.asInstanceOf[ConcurrentFeedWeaponData])
case _ =>
Attempt.failure(Err("can not encode weapon data"))
}
)
}

View file

@ -8,22 +8,22 @@ import scodec.codecs._
import shapeless.{::, HNil} import shapeless.{::, HNil}
/** /**
* Similar fields as required for a formal `ObjectCreateMessage` but with a required but implicit parent relationship. * An intermediate class for the primary fields of `ObjectCreateMessage` with an implicit parent-child relationship.<br>
* Specifically, the purpose of the packet is to start to define a new object within the definition of a previous object.
* This prior object will clarify the identity of the parent object that owns the given `parentSlot`.<br>
* <br> * <br>
* An `InternalSlot` object is not a top-level object. * Any object that is contained in a "slot" of another object will use `InternalSlot` to hold the anchoring data.
* Extra effort should be made to ensure the user does not have to directly construct an `InternalSlot`. * This prior object will clarify the identity of the "parent" object that owns the given `parentSlot`.<br>
* <br>
* Try to avoid exposing `InternalSlot` in the process of implementing code.
* @param objectClass the code for the type of object being constructed * @param objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned * @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 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 * @param obj the data used as representation of the object to be constructed
* @see ObjectClass.selectDataCodec * @see ObjectClass.selectDataCodec
*/ */
case class InternalSlot(objectClass : Int, final case class InternalSlot(objectClass : Int,
guid : PlanetSideGUID, guid : PlanetSideGUID,
parentSlot : Int, parentSlot : Int,
obj : ConstructorData) extends StreamBitSize { obj : ConstructorData) extends StreamBitSize {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize

View file

@ -14,12 +14,18 @@ import shapeless.{::, HNil}
* No values are allowed to be misplaced and no unexpected regions of data can be discovered. * 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 whole of the inventory will fail to translate.<br>
* <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> * Exploration:<br>
* 4u of ignored bits are tagged onto the end of this field for purposes of finding four missing bits of stream length. * 4u of ignored bits have been added to the end of the inventory to make up for missing stream length.
* The rest of the encoding is valid. * They do not actually seem to be part of the inventory.
* Conditions must certainly decide whether these bits are present or not. * Are these bits always at the end of the packet data and what is the significance?
* @param unk1 na; * @param unk1 na;
* `true` to mark the start of the inventory data? * `true` to mark the start of the inventory data?
* is explicitly declaring the bit necessary when it always seems to be `true`?
* @param unk2 na * @param unk2 na
* @param unk3 na * @param unk3 na
* @param contents the actual items in the inventory; * @param contents the actual items in the inventory;
@ -27,17 +33,17 @@ import shapeless.{::, HNil}
* an inaccessible slot is 5; * an inaccessible slot is 5;
* internal capacity is 6-`n`, where `n` is defined by exosuit type and is mapped into a grid * internal capacity is 6-`n`, where `n` is defined by exosuit type and is mapped into a grid
*/ */
case class InventoryData(unk1 : Boolean, final case class InventoryData(unk1 : Boolean,
unk2 : Boolean, unk2 : Boolean,
unk3 : Boolean, unk3 : Boolean,
contents : List[InventoryItem]) extends StreamBitSize { contents : List[InventoryItem]) extends StreamBitSize {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object * @return the number of bits necessary to represent this object
*/ */
override def bitsize : Long = { override def bitsize : Long = {
//three booleans, the 4u and the 8u length field //three booleans, the 4u extra, and the 8u length field
val base : Long = 15L val base : Long = 15L
//length of all items in inventory //length of all items in inventory
var invSize : Long = 0L var invSize : Long = 0L
@ -57,14 +63,14 @@ object InventoryData extends Marshallable[InventoryData] {
("contents" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) :: ("contents" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) ::
ignore(4) ignore(4)
}) })
).xmap[InventoryData] ( ).xmap[InventoryData] (
{ {
case u1 :: _ :: u2 :: u3 :: ctnt :: _ :: HNil => case u1 :: _ :: a :: b :: ctnt :: _ :: HNil =>
InventoryData(u1, u2, u3, ctnt) InventoryData(u1, a, b, ctnt)
}, },
{ {
case InventoryData(u1, u2, u3, ctnt) => case InventoryData(u1, a, b, ctnt) =>
u1 :: ctnt.size :: u2 :: u3 :: ctnt :: () :: HNil u1 :: ctnt.size :: a :: b :: ctnt :: () :: HNil
} }
).as[InventoryData] ).as[InventoryData]
} }

View file

@ -8,15 +8,15 @@ import scodec.codecs._
/** /**
* A representation of an item in an avatar's inventory. * A representation of an item in an avatar's inventory.
* Reliance on `InternalSlot` indicates that this item is applicable to the same implicit parental relationship. * Reliance on `InternalSlot` indicates that this item is applicable to the same implicit parent-child relationship.
* (That is, its parent object will be clarified earlier on in the data stream.) * (That is, its parent object will be clarified by the containing element, e.g., the inventory or its owner.)
* Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible.<br> * Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible.<br>
* <br> * <br>
* This intermediary object is primarily intended to mask external use of `InternalSlot`. * This intermediary object is primarily intended to mask external use of `InternalSlot`, as specified by the class.
* @param item the object in inventory * @param item the object in inventory
* @see InternalSlot * @see InternalSlot
*/ */
case class InventoryItem(item : InternalSlot) extends StreamBitSize { final case class InventoryItem(item : InternalSlot) extends StreamBitSize {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize

View file

@ -20,6 +20,7 @@ object ObjectClass {
final val BULLETS_9MM = 0x1C final val BULLETS_9MM = 0x1C
final val BULLETS_9MM_AP = 0x1D final val BULLETS_9MM_AP = 0x1D
final val ENERGY_CELL = 0x110 final val ENERGY_CELL = 0x110
final val JAMMER_GRENADE_PACK = 0x19D
final val JAMMER_GRENADE_AMMO = 0x1A1 final val JAMMER_GRENADE_AMMO = 0x1A1
final val FORCE_BLADE_AMMO = 0x21C final val FORCE_BLADE_AMMO = 0x21C
final val PLASMA_GRENADE_AMMO = 0x2A9 final val PLASMA_GRENADE_AMMO = 0x2A9
@ -32,6 +33,7 @@ object ObjectClass {
final val GAUSS = 0x159 final val GAUSS = 0x159
final val JAMMER_GRENADE = 0x1A0 final val JAMMER_GRENADE = 0x1A0
final val PLASMA_GRENADE = 0x2A8 final val PLASMA_GRENADE = 0x2A8
final val PUNISHER = 0x2C2
//tools //tools
final val MEDKIT = 0x218 final val MEDKIT = 0x218
final val REK = 0x2D8 final val REK = 0x2D8
@ -62,9 +64,11 @@ object ObjectClass {
case ObjectClass.GAUSS => WeaponData.genericCodec case ObjectClass.GAUSS => WeaponData.genericCodec
case ObjectClass.JAMMER_GRENADE => WeaponData.genericCodec case ObjectClass.JAMMER_GRENADE => WeaponData.genericCodec
case ObjectClass.JAMMER_GRENADE_AMMO => AmmoBoxData.genericCodec case ObjectClass.JAMMER_GRENADE_AMMO => AmmoBoxData.genericCodec
case ObjectClass.JAMMER_GRENADE_PACK => AmmoBoxData.genericCodec
case ObjectClass.MEDKIT => AmmoBoxData.genericCodec case ObjectClass.MEDKIT => AmmoBoxData.genericCodec
case ObjectClass.PLASMA_GRENADE => WeaponData.genericCodec case ObjectClass.PLASMA_GRENADE => WeaponData.genericCodec
case ObjectClass.PLASMA_GRENADE_AMMO => AmmoBoxData.genericCodec case ObjectClass.PLASMA_GRENADE_AMMO => AmmoBoxData.genericCodec
case ObjectClass.PUNISHER => ConcurrentFeedWeaponData.genericCodec
case ObjectClass.REK => REKData.genericCodec case ObjectClass.REK => REKData.genericCodec
case ObjectClass.SLOT_BLOCKER => AmmoBoxData.genericCodec case ObjectClass.SLOT_BLOCKER => AmmoBoxData.genericCodec
case ObjectClass.SUPPRESSOR => WeaponData.genericCodec case ObjectClass.SUPPRESSOR => WeaponData.genericCodec

View file

@ -13,8 +13,7 @@ import shapeless.{::, HNil}
* Of note is the first portion of the data which resembles the `WeaponData` format. * Of note is the first portion of the data which resembles the `WeaponData` format.
* @param unk na * @param unk na
*/ */
case class REKData(unk : Int final case class REKData(unk : Int) extends ConstructorData {
) extends ConstructorData {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
@ -29,7 +28,7 @@ object REKData extends Marshallable[REKData] {
uint4L :: uint4L ::
uintL(20) :: uintL(20) ::
uint4L :: uint4L ::
uintL(16) :: uint16L ::
uint4L :: uint4L ::
uintL(15) uintL(15)
).exmap[REKData] ( ).exmap[REKData] (
@ -37,7 +36,7 @@ object REKData extends Marshallable[REKData] {
case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil => case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil =>
Attempt.successful(REKData(code)) Attempt.successful(REKData(code))
case code :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => case code :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("illegal rek data format")) Attempt.failure(Err("invalid rek data format"))
}, },
{ {
case REKData(code) => case REKData(code) =>

View file

@ -17,10 +17,10 @@ import scodec.codecs._
* @param lower the lower configurable merit ribbon * @param lower the lower configurable merit ribbon
* @param tos the top-most term of service merit ribbon * @param tos the top-most term of service merit ribbon
*/ */
case class RibbonBars(upper : Long = 0xFFFFFFFFL, final case class RibbonBars(upper : Long = 0xFFFFFFFFL,
middle : Long = 0xFFFFFFFFL, middle : Long = 0xFFFFFFFFL,
lower : Long = 0xFFFFFFFFL, lower : Long = 0xFFFFFFFFL,
tos : Long = 0xFFFFFFFFL) extends StreamBitSize { tos : Long = 0xFFFFFFFFL) extends StreamBitSize {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize

View file

@ -8,8 +8,9 @@ trait StreamBitSize {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* The calculation reflects the `scodec Codec` definition rather than the explicit parameter fields. * The calculation reflects the `scodec Codec` definition rather than the explicit parameter fields.
* For example, an `Int` is normally a 32-bit number; * For example, an `Int` is normally a 32u number;
* when parsed with a `uintL(7)`, it's length will be considered 7u. * 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; * @return the number of bits necessary to represent this object;
* defaults to `0L` * defaults to `0L`
*/ */

View file

@ -8,11 +8,11 @@ import scodec.codecs._
import shapeless.{::, HNil} import shapeless.{::, HNil}
/** /**
* A representation of the weapon portion of `ObjectCreateMessage` packet data. * A representation of a class of weapons that can be created using `ObjectCreateMessage` packet data.
* This data will help construct a "weapon" such as a Suppressor or a Gauss.<br> * This data will help construct a "loaded weapon" such as a Suppressor or a Gauss.<br>
* <br> * <br>
* The data for the weapons nests information for the default (current) type and number of ammunition in its magazine. * 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 magazine as numbered slots. * 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. * 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`. * 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; * This functionality is unrelated to a weapon that switches ammunition type;
@ -21,8 +21,8 @@ import shapeless.{::, HNil}
* @param ammo data regarding the currently loaded ammunition type and quantity * @param ammo data regarding the currently loaded ammunition type and quantity
* @see AmmoBoxData * @see AmmoBoxData
*/ */
case class WeaponData(unk : Int, final case class WeaponData(unk : Int,
ammo : InternalSlot) extends ConstructorData { ammo : InternalSlot) extends ConstructorData {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
@ -51,23 +51,23 @@ object WeaponData extends Marshallable[WeaponData] {
implicit val codec : Codec[WeaponData] = ( implicit val codec : Codec[WeaponData] = (
("unk" | uint4L) :: ("unk" | uint4L) ::
uint4L :: uint4L ::
uintL(20) :: uint24 ::
uint4L :: uint16L ::
uintL(16) :: uint2 ::
uintL(11) :: uint8 :: //size = 1 type of ammunition loaded
bool :: uint2 ::
("ammo" | InternalSlot.codec) :: ("ammo" | InternalSlot.codec) ::
bool bool
).exmap[WeaponData] ( ).exmap[WeaponData] (
{ {
case code :: 8 :: 0 :: 2 :: 0 :: 0x2C0 :: false :: ammo :: false :: HNil => case code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil =>
Attempt.successful(WeaponData(code, ammo)) Attempt.successful(WeaponData(code, ammo))
case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("illegal weapon data format")) Attempt.failure(Err("invalid weapon data format"))
}, },
{ {
case WeaponData(code, ammo) => case WeaponData(code, ammo) =>
Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 0x2C0 :: false :: ammo :: false :: HNil) Attempt.successful(code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil)
} }
).as[WeaponData] ).as[WeaponData]

View file

@ -153,6 +153,7 @@ class GamePacketTest extends Specification {
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" var string_inventoryItem = hex"46 04 C0 08 08 80 00 00 20 00 0C 04 10 29 A0 10 19 00 00 04 00 00"
val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000"
val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" 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_rek = hex"18 97000000 2580 6C2 9F05 81 48000002000080000"
val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" 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"
@ -208,6 +209,33 @@ class GamePacketTest extends Specification {
} }
} }
"decode (punisher)" in {
PacketCoding.DecodePacket(string_punisher).require match {
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
len mustEqual 295
cls mustEqual 706
guid mustEqual PlanetSideGUID(1703)
parent.isDefined mustEqual true
parent.get.guid mustEqual PlanetSideGUID(75)
parent.get.slot mustEqual 2
data.isDefined mustEqual true
val obj_wep = data.get.asInstanceOf[ConcurrentFeedWeaponData]
obj_wep.unk mustEqual 0
val obj_ammo = obj_wep.ammo
obj_ammo.size mustEqual 2
obj_ammo.head.objectClass mustEqual 28
obj_ammo.head.guid mustEqual PlanetSideGUID(1693)
obj_ammo.head.parentSlot mustEqual 0
obj_ammo.head.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30
obj_ammo(1).objectClass mustEqual 413
obj_ammo(1).guid mustEqual PlanetSideGUID(1564)
obj_ammo(1).parentSlot mustEqual 1
obj_ammo(1).obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1
case _ =>
ko
}
}
"decode (rek)" in { "decode (rek)" in {
PacketCoding.DecodePacket(string_rek).require match { PacketCoding.DecodePacket(string_rek).require match {
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
@ -373,6 +401,14 @@ class GamePacketTest extends Specification {
pkt mustEqual string_gauss pkt mustEqual string_gauss
} }
"encode (punisher)" in {
val obj = ConcurrentFeedWeaponData(0, AmmoBoxData(28, PlanetSideGUID(1693), 0, AmmoBoxData(30)) :: AmmoBoxData(413, PlanetSideGUID(1564), 1, AmmoBoxData(1)) :: Nil)
val msg = ObjectCreateMessage(0, 706, PlanetSideGUID(1703), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj)
var pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_punisher
}
"encode (rek)" in { "encode (rek)" in {
val obj = REKData(4) val obj = REKData(4)
val msg = ObjectCreateMessage(0, 0x2D8, PlanetSideGUID(1439), ObjectCreateMessageParent(PlanetSideGUID(75), 1), obj) val msg = ObjectCreateMessage(0, 0x2D8, PlanetSideGUID(1439), ObjectCreateMessageParent(PlanetSideGUID(75), 1), obj)

View file

@ -9,7 +9,8 @@ import scodec.Attempt.{Failure, Successful}
import scodec.bits._ import scodec.bits._
import org.log4s.MDC import org.log4s.MDC
import MDCContextAware.Implicits._ import MDCContextAware.Implicits._
import net.psforever.types.ChatMessageType import net.psforever.packet.game.objectcreate._
import net.psforever.types.{ChatMessageType, Vector3}
class WorldSessionActor extends Actor with MDCContextAware { class WorldSessionActor extends Actor with MDCContextAware {
private[this] val log = org.log4s.getLogger private[this] val log = org.log4s.getLogger
@ -107,8 +108,49 @@ class WorldSessionActor extends Actor with MDCContextAware {
} }
} }
// XXX: hard coded ObjectCreateMessage //val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00"
val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00 " //currently, the character's starting BEP is discarded due to unknown bit format
val app = CharacterAppearanceData(
Vector3(3674.8438f, 2726.789f, 91.15625f),
19,
2,
false,
4,
"IlllIIIlllIlIllIlllIllI",
4,
2,
2, 9,
1,
3, 118, 30, 0x8080, 0xFFFF, 2,
255, 106, 7,
RibbonBars()
)
val inv =
InventoryItem(ObjectClass.BEAMER, PlanetSideGUID(76), 0, WeaponData(8, ObjectClass.ENERGY_CELL, PlanetSideGUID(77), 0, AmmoBoxData(16))) ::
InventoryItem(ObjectClass.SUPPRESSOR, PlanetSideGUID(78), 2, WeaponData(8, ObjectClass.BULLETS_9MM, PlanetSideGUID(79), 0, AmmoBoxData(25))) ::
InventoryItem(ObjectClass.FORCE_BLADE, PlanetSideGUID(80), 4, WeaponData(8, ObjectClass.FORCE_BLADE_AMMO, PlanetSideGUID(81), 0, AmmoBoxData(1))) ::
InventoryItem(ObjectClass.SLOT_BLOCKER, PlanetSideGUID(82), 5, AmmoBoxData(1)) ::
InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(83), 6, AmmoBoxData(50)) ::
InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(84), 9, AmmoBoxData(50)) ::
InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(85), 12, AmmoBoxData(50)) ::
InventoryItem(ObjectClass.BULLETS_9MM_AP, PlanetSideGUID(86), 33, AmmoBoxData(50)) ::
InventoryItem(ObjectClass.ENERGY_CELL, PlanetSideGUID(87), 36, AmmoBoxData(50)) ::
InventoryItem(ObjectClass.REK, PlanetSideGUID(88), 39, REKData(8)) ::
Nil
val obj = CharacterData(
app,
100, 100,
50,
1, 7, 7,
100, 100,
28, 4, 44, 84, 104, 1900,
"xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil,
List.empty,
InventoryData(
true, false, false, inv
)
)
val objectHex = ObjectCreateMessage(0, ObjectClass.AVATAR, PlanetSideGUID(75), obj)
def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match { def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match {
case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) => case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) =>
@ -118,7 +160,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info(s"New world login to ${server} with Token:${token}. ${clientVersion}") log.info(s"New world login to ${server} with Token:${token}. ${clientVersion}")
// ObjectCreateMessage // ObjectCreateMessage
sendRawResponse(objectHex) sendResponse(PacketCoding.CreateGamePacket(0, objectHex))
// XXX: hard coded message // XXX: hard coded message
sendRawResponse(hex"14 0F 00 00 00 10 27 00 00 C1 D8 7A 02 4B 00 26 5C B0 80 00 ") sendRawResponse(hex"14 0F 00 00 00 10 27 00 00 C1 D8 7A 02 4B 00 26 5C B0 80 00 ")
@ -132,14 +174,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
case CharacterRequestAction.Delete => case CharacterRequestAction.Delete =>
sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1)))) sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1))))
case CharacterRequestAction.Select => case CharacterRequestAction.Select =>
PacketCoding.DecodeGamePacket(objectHex).require match { objectHex match {
case obj @ ObjectCreateMessage(len, cls, guid, _, _) => case obj @ ObjectCreateMessage(len, cls, guid, _, _) =>
log.debug("Object: " + obj) log.debug("Object: " + obj)
// LoadMapMessage 13714 in mossy .gcap // LoadMapMessage 13714 in mossy .gcap
// XXX: hardcoded shit // XXX: hardcoded shit
sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary
sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0))) sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0)))
sendRawResponse(objectHex) sendResponse(PacketCoding.CreateGamePacket(0, objectHex))
// These object_guids are specfic to VS Sanc // These object_guids are specfic to VS Sanc
sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C