diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala index 0205775a..9db2022d 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -18,8 +18,8 @@ import shapeless.{::, HNil} * @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 */ -case class ObjectCreateMessageParent(guid : PlanetSideGUID, - slot : Int) +final case class ObjectCreateMessageParent(guid : PlanetSideGUID, + slot : Int) /** * 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 guid the GUID this object will be assigned * @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 */ -case class ObjectCreateMessage(streamLength : Long, - objectClass : Int, - guid : PlanetSideGUID, - parentInfo : Option[ObjectCreateMessageParent], - data : Option[ConstructorData]) +final case class ObjectCreateMessage(streamLength : Long, + objectClass : Int, + guid : PlanetSideGUID, + parentInfo : Option[ObjectCreateMessageParent], + data : Option[ConstructorData]) extends PlanetSideGamePacket { def opcode = GamePacketOpcode.ObjectCreateMessage def encode = ObjectCreateMessage.encode(this) diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala index ed8fed41..2d33df7a 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala @@ -2,6 +2,7 @@ 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} @@ -13,12 +14,11 @@ import shapeless.{::, HNil} *
* 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 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 * @see WeaponData */ -case class AmmoBoxData(magazine : Int - ) extends ConstructorData { +final case class AmmoBoxData(magazine : Int) extends ConstructorData { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize @@ -28,9 +28,20 @@ case class AmmoBoxData(magazine : Int } 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] = ( - uintL(8) :: - uintL(15) :: + uint8L :: + uint(15) :: ("magazine" | uint16L) :: bool ).exmap[AmmoBoxData] ( @@ -38,7 +49,7 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] { case 0xC8 :: 0 :: mag :: false :: HNil => Attempt.successful(AmmoBoxData(mag)) case a :: b :: _ :: d :: HNil => - Attempt.failure(Err("illegal ammunition data format")) + Attempt.failure(Err("invalid ammunition data format")) }, { case AmmoBoxData(mag) => diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala index 019ed856..b614fc70 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala @@ -89,27 +89,27 @@ import shapeless.{::, HNil} * @param unk8 na * @param ribbons the four merit commendation ribbon medals */ -case class CharacterAppearanceData(pos : Vector3, - objYaw : Int, - faction : Int, - bops : Boolean, - unk1 : Int, - name : String, - exosuit : Int, - sex : Int, - face1 : Int, - face2 : Int, - voice : Int, - unk2 : Int, - unk3 : Int, - unk4 : Int, - unk5 : Int, - unk6 : Int, - unk7 : Int, - viewPitch : Int, - viewYaw : Int, - unk8 : Int, - ribbons : RibbonBars) extends StreamBitSize { +final case class CharacterAppearanceData(pos : Vector3, + objYaw : Int, + faction : Int, + bops : Boolean, + unk1 : Int, + name : String, + exosuit : Int, + sex : Int, + face1 : Int, + face2 : Int, + voice : Int, + unk2 : Int, + unk3 : Int, + unk4 : Int, + unk5 : Int, + unk6 : Int, + unk7 : Int, + viewPitch : Int, + viewYaw : Int, + unk8 : Int, + ribbons : RibbonBars) extends StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize @@ -138,18 +138,18 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { ignore(16) :: ("objYaw" | uint8L) :: ignore(1) :: - ("faction" | uintL(2)) :: + ("faction" | uint2L) :: ("bops" | bool) :: ("unk1" | uint4L) :: ignore(16) :: ("name" | PacketHelpers.encodedWideStringAligned( namePadding )) :: ("exosuit" | uintL(3)) :: ignore(2) :: - ("sex" | uintL(2)) :: + ("sex" | uint2L) :: ("face1" | uint4L) :: ("face2" | uint4L) :: ("voice" | uintL(3)) :: - ("unk2" | uintL(2)) :: + ("unk2" | uint2L) :: ignore(4) :: ("unk3" | uint8L) :: ("unk4" | uint8L) :: @@ -219,29 +219,29 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { * 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 + * the size field is a 32-bit number; + * the first entry may be padded * @param inventory the avatar's inventory */ -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 { +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 @@ -310,7 +310,7 @@ object CharacterData extends Marshallable[CharacterData] { if(len > 0) //automatic alignment from previous List 0 else if(len2 > 0) //need to align for elements - 1 + 5 else //both lists are empty 0 } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConcurrentFeedWeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConcurrentFeedWeaponData.scala new file mode 100644 index 00000000..ae151e0f --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConcurrentFeedWeaponData.scala @@ -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.
+ *
+ * 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`.
+ *
+ * Exploration:
+ * 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")) + } + ) +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala index f634a599..4fef95ef 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala @@ -8,22 +8,22 @@ import scodec.codecs._ import shapeless.{::, HNil} /** - * Similar fields as required for a formal `ObjectCreateMessage` but with a required but implicit parent relationship. - * 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`.
+ * An intermediate class for the primary fields of `ObjectCreateMessage` with an implicit parent-child relationship.
*
- * An `InternalSlot` object is not a top-level object. - * Extra effort should be made to ensure the user does not have to directly construct an `InternalSlot`. + * 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`.
+ *
+ * Try to avoid exposing `InternalSlot` in the process of implementing code. * @param objectClass the code for the type of object being constructed * @param guid the GUID this object will be assigned * @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent * @param obj the data used as representation of the object to be constructed * @see ObjectClass.selectDataCodec */ -case class InternalSlot(objectClass : Int, - guid : PlanetSideGUID, - parentSlot : Int, - obj : ConstructorData) extends StreamBitSize { +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 diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala index d2a597e7..9a7232e8 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala @@ -14,12 +14,18 @@ import shapeless.{::, HNil} * 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.
*
+ * 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.
+ *
* Exploration:
- * 4u of ignored bits are tagged onto the end of this field for purposes of finding four missing bits of stream length. - * The rest of the encoding is valid. - * Conditions must certainly decide whether these bits are present or not. + * 4u of ignored bits have been added to the end of the inventory to make up for missing stream length. + * They do not actually seem to be part of the inventory. + * Are these bits always at the end of the packet data and what is the significance? * @param unk1 na; * `true` to mark the start of the inventory data? + * is explicitly declaring the bit necessary when it always seems to be `true`? * @param unk2 na * @param unk3 na * @param contents the actual items in the inventory; @@ -27,17 +33,17 @@ import shapeless.{::, HNil} * 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, - unk2 : Boolean, - unk3 : Boolean, - contents : List[InventoryItem]) extends StreamBitSize { +final case class InventoryData(unk1 : Boolean, + unk2 : Boolean, + unk3 : Boolean, + contents : List[InventoryItem]) extends StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize * @return the number of bits necessary to represent this object */ override def bitsize : Long = { - //three booleans, the 4u and the 8u length field + //three booleans, the 4u extra, and the 8u length field val base : Long = 15L //length of all items in inventory var invSize : Long = 0L @@ -57,14 +63,14 @@ object InventoryData extends Marshallable[InventoryData] { ("contents" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) :: ignore(4) }) - ).xmap[InventoryData] ( + ).xmap[InventoryData] ( { - case u1 :: _ :: u2 :: u3 :: ctnt :: _ :: HNil => - InventoryData(u1, u2, u3, ctnt) + case u1 :: _ :: a :: b :: ctnt :: _ :: HNil => + InventoryData(u1, a, b, ctnt) }, { - case InventoryData(u1, u2, u3, ctnt) => - u1 :: ctnt.size :: u2 :: u3 :: ctnt :: () :: HNil + case InventoryData(u1, a, b, ctnt) => + u1 :: ctnt.size :: a :: b :: ctnt :: () :: HNil } ).as[InventoryData] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala index 90fc0fc0..25f17abe 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala @@ -8,15 +8,15 @@ import scodec.codecs._ /** * A representation of an item in an avatar's inventory. - * Reliance on `InternalSlot` indicates that this item is applicable to the same implicit parental relationship. - * (That is, its parent object will be clarified earlier on in the data stream.) + * Reliance on `InternalSlot` indicates that this item is applicable to the same implicit parent-child relationship. + * (That is, its parent object will be clarified by the containing element, e.g., the inventory or its owner.) * Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible.
*
- * 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 * @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. * @see ConstructorData.bitsize diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala index 995ee934..e351bc3d 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -20,6 +20,7 @@ object ObjectClass { final val BULLETS_9MM = 0x1C final val BULLETS_9MM_AP = 0x1D final val ENERGY_CELL = 0x110 + final val JAMMER_GRENADE_PACK = 0x19D final val JAMMER_GRENADE_AMMO = 0x1A1 final val FORCE_BLADE_AMMO = 0x21C final val PLASMA_GRENADE_AMMO = 0x2A9 @@ -32,6 +33,7 @@ object ObjectClass { final val GAUSS = 0x159 final val JAMMER_GRENADE = 0x1A0 final val PLASMA_GRENADE = 0x2A8 + final val PUNISHER = 0x2C2 //tools final val MEDKIT = 0x218 final val REK = 0x2D8 @@ -62,9 +64,11 @@ object ObjectClass { case ObjectClass.GAUSS => WeaponData.genericCodec case ObjectClass.JAMMER_GRENADE => WeaponData.genericCodec case ObjectClass.JAMMER_GRENADE_AMMO => AmmoBoxData.genericCodec + case ObjectClass.JAMMER_GRENADE_PACK => AmmoBoxData.genericCodec case ObjectClass.MEDKIT => AmmoBoxData.genericCodec case ObjectClass.PLASMA_GRENADE => WeaponData.genericCodec case ObjectClass.PLASMA_GRENADE_AMMO => AmmoBoxData.genericCodec + case ObjectClass.PUNISHER => ConcurrentFeedWeaponData.genericCodec case ObjectClass.REK => REKData.genericCodec case ObjectClass.SLOT_BLOCKER => AmmoBoxData.genericCodec case ObjectClass.SUPPRESSOR => WeaponData.genericCodec diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala index b2c32a2b..89078a59 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala @@ -13,8 +13,7 @@ import shapeless.{::, HNil} * Of note is the first portion of the data which resembles the `WeaponData` format. * @param unk na */ -case class REKData(unk : Int - ) extends ConstructorData { +final case class REKData(unk : Int) extends ConstructorData { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize @@ -29,7 +28,7 @@ object REKData extends Marshallable[REKData] { uint4L :: uintL(20) :: uint4L :: - uintL(16) :: + uint16L :: uint4L :: uintL(15) ).exmap[REKData] ( @@ -37,7 +36,7 @@ object REKData extends Marshallable[REKData] { case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil => Attempt.successful(REKData(code)) case code :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => - Attempt.failure(Err("illegal rek data format")) + Attempt.failure(Err("invalid rek data format")) }, { case REKData(code) => diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala index e4dee136..28358a97 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala @@ -17,10 +17,10 @@ import scodec.codecs._ * @param lower the lower configurable merit ribbon * @param tos the top-most term of service merit ribbon */ -case class RibbonBars(upper : Long = 0xFFFFFFFFL, - middle : Long = 0xFFFFFFFFL, - lower : Long = 0xFFFFFFFFL, - tos : Long = 0xFFFFFFFFL) extends StreamBitSize { +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 diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala index 9f9d562a..bbdd12ce 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala @@ -8,8 +8,9 @@ trait StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * The calculation reflects the `scodec Codec` definition rather than the explicit parameter fields. - * For example, an `Int` is normally a 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. + * (Note: being permanently signed, an `scodec` 32u value must fit into a `Long` type.) * @return the number of bits necessary to represent this object; * defaults to `0L` */ diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala index 175b2547..41dded1d 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala @@ -8,11 +8,11 @@ import scodec.codecs._ import shapeless.{::, HNil} /** - * A representation of the weapon portion of `ObjectCreateMessage` packet data. - * This data will help construct a "weapon" such as a Suppressor or a Gauss.
+ * 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.
*
* 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. * 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; @@ -21,8 +21,8 @@ import shapeless.{::, HNil} * @param ammo data regarding the currently loaded ammunition type and quantity * @see AmmoBoxData */ -case class WeaponData(unk : Int, - ammo : InternalSlot) extends ConstructorData { +final case class WeaponData(unk : Int, + ammo : InternalSlot) extends ConstructorData { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize @@ -51,23 +51,23 @@ object WeaponData extends Marshallable[WeaponData] { implicit val codec : Codec[WeaponData] = ( ("unk" | uint4L) :: uint4L :: - uintL(20) :: - uint4L :: - uintL(16) :: - uintL(11) :: - bool :: + uint24 :: + uint16L :: + uint2 :: + uint8 :: //size = 1 type of ammunition loaded + uint2 :: ("ammo" | InternalSlot.codec) :: bool ).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)) case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => - Attempt.failure(Err("illegal weapon data format")) + Attempt.failure(Err("invalid weapon data format")) }, { 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] diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index b9b8da40..8ecccd69 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -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" val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" + val string_punisher = hex"18 27010000 2580 612 a706 82 080000020000c08 1c13a0d01900000780 13a4701a072000000800" val string_rek = hex"18 97000000 2580 6C2 9F05 81 48000002000080000" val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" @@ -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 { PacketCoding.DecodePacket(string_rek).require match { case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => @@ -373,6 +401,14 @@ class GamePacketTest extends Specification { pkt mustEqual string_gauss } + "encode (punisher)" in { + val obj = ConcurrentFeedWeaponData(0, AmmoBoxData(28, PlanetSideGUID(1693), 0, AmmoBoxData(30)) :: AmmoBoxData(413, PlanetSideGUID(1564), 1, AmmoBoxData(1)) :: Nil) + val msg = ObjectCreateMessage(0, 706, PlanetSideGUID(1703), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj) + var pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_punisher + } + "encode (rek)" in { val obj = REKData(4) val msg = ObjectCreateMessage(0, 0x2D8, PlanetSideGUID(1439), ObjectCreateMessageParent(PlanetSideGUID(75), 1), obj) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 27c607b2..de389012 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -9,7 +9,8 @@ import scodec.Attempt.{Failure, Successful} import scodec.bits._ import org.log4s.MDC import MDCContextAware.Implicits._ -import net.psforever.types.ChatMessageType +import net.psforever.packet.game.objectcreate._ +import net.psforever.types.{ChatMessageType, Vector3} class WorldSessionActor extends Actor with MDCContextAware { private[this] val log = org.log4s.getLogger @@ -107,8 +108,49 @@ class WorldSessionActor extends Actor with MDCContextAware { } } - // XXX: hard coded ObjectCreateMessage - val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00 " + //val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" + //currently, the character's starting BEP is discarded due to unknown bit format + val app = CharacterAppearanceData( + Vector3(3674.8438f, 2726.789f, 91.15625f), + 19, + 2, + false, + 4, + "IlllIIIlllIlIllIlllIllI", + 4, + 2, + 2, 9, + 1, + 3, 118, 30, 0x8080, 0xFFFF, 2, + 255, 106, 7, + RibbonBars() + ) + val inv = + InventoryItem(ObjectClass.BEAMER, PlanetSideGUID(76), 0, WeaponData(8, ObjectClass.ENERGY_CELL, PlanetSideGUID(77), 0, AmmoBoxData(16))) :: + InventoryItem(ObjectClass.SUPPRESSOR, PlanetSideGUID(78), 2, WeaponData(8, ObjectClass.BULLETS_9MM, PlanetSideGUID(79), 0, AmmoBoxData(25))) :: + InventoryItem(ObjectClass.FORCE_BLADE, PlanetSideGUID(80), 4, WeaponData(8, ObjectClass.FORCE_BLADE_AMMO, PlanetSideGUID(81), 0, AmmoBoxData(1))) :: + InventoryItem(ObjectClass.SLOT_BLOCKER, PlanetSideGUID(82), 5, AmmoBoxData(1)) :: + InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(83), 6, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(84), 9, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(85), 12, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.BULLETS_9MM_AP, PlanetSideGUID(86), 33, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.ENERGY_CELL, PlanetSideGUID(87), 36, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.REK, PlanetSideGUID(88), 39, REKData(8)) :: + Nil + val obj = CharacterData( + app, + 100, 100, + 50, + 1, 7, 7, + 100, 100, + 28, 4, 44, 84, 104, 1900, + "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, + List.empty, + InventoryData( + true, false, false, inv + ) + ) + val objectHex = ObjectCreateMessage(0, ObjectClass.AVATAR, PlanetSideGUID(75), obj) def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match { case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) => @@ -118,7 +160,7 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"New world login to ${server} with Token:${token}. ${clientVersion}") // ObjectCreateMessage - sendRawResponse(objectHex) + sendResponse(PacketCoding.CreateGamePacket(0, objectHex)) // XXX: hard coded message sendRawResponse(hex"14 0F 00 00 00 10 27 00 00 C1 D8 7A 02 4B 00 26 5C B0 80 00 ") @@ -132,14 +174,14 @@ class WorldSessionActor extends Actor with MDCContextAware { case CharacterRequestAction.Delete => sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1)))) case CharacterRequestAction.Select => - PacketCoding.DecodeGamePacket(objectHex).require match { + objectHex match { case obj @ ObjectCreateMessage(len, cls, guid, _, _) => log.debug("Object: " + obj) // LoadMapMessage 13714 in mossy .gcap // XXX: hardcoded shit sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0))) - sendRawResponse(objectHex) + sendResponse(PacketCoding.CreateGamePacket(0, objectHex)) // These object_guids are specfic to VS Sanc sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C