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 19119ff0..dd211a76 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -1,7 +1,8 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -//import net.psforever.types.Vector3 +import scodec.DecodeResult +import net.psforever.types.Vector3 import scodec.bits._ import scodec.{Attempt, Codec, Err} import scodec.codecs._ @@ -19,25 +20,25 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] { ignore(15) :: ("magazine" | uint16L) ).exmap[AmmoBoxData] ( - { - case 0xC8 :: _ :: mag :: HNil => - Attempt.successful(AmmoBoxData(mag)) - case x :: _ :: _ :: HNil => - Attempt.failure(Err("code wrong - looking for 200, found "+x)) - }, - { - case AmmoBoxData(mag) => - Attempt.successful(0xC8 :: () :: mag :: HNil) - } - ).as[AmmoBoxData] + { + case 0xC8 :: _ :: mag :: HNil => + Attempt.successful(AmmoBoxData(mag)) + case x :: _ :: _ :: HNil => + Attempt.failure(Err("looking for 200, found "+x)) + }, + { + case AmmoBoxData(mag) => + Attempt.successful(0xC8 :: () :: mag :: HNil) + } + ).as[AmmoBoxData] } case class WeaponData(unk : Int, - ammo : InternalMold) extends ConstructorData + ammo : InternalSlot) extends ConstructorData object WeaponData extends Marshallable[WeaponData] { def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData = - new WeaponData(unk, InternalMold(cls, guid, parentSlot, Some(ammo))) + new WeaponData(unk, InternalSlot(cls, guid, parentSlot, Some(ammo))) implicit val codec : Codec[WeaponData] = ( ("unk" | uint4L) :: @@ -46,13 +47,13 @@ object WeaponData extends Marshallable[WeaponData] { uint4L :: ignore(16) :: uintL(11) :: - InternalMold.codec + ("ammo" | InternalSlot.codec) ).exmap[WeaponData] ( { case code :: 8 :: _ :: 2 :: _ :: 0x2C0 :: ammo :: HNil => Attempt.successful(WeaponData(code, ammo)) case _ :: x :: _ :: y :: _ :: z :: _ :: HNil => - Attempt.failure(Err("looking for 8-2-704 pattern, found %d-%d-%d".format(x,y,z))) + Attempt.failure(Err("looking for 8-2-704 pattern, found %d-%d-%d".format(x,y,z))) //TODO I actually don't know what of this is actually important }, { case WeaponData(code, ammo) => @@ -61,26 +62,39 @@ object WeaponData extends Marshallable[WeaponData] { ).as[WeaponData] } -//case class CharacterData(pos : Vector3, -// obj_yaw : 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 -// anchor : Boolean, -// viewPitch : Int, -// viewYaw : Int, -// upperMerit : Int, //0xFFFF means no merit (for all ...) -// middleMerit : Int, -// lowerMerit : Int, -// termOfServiceMerit : Int, +case class RibbonBars(upper : Int, //0xFFFF means no merit (for all ...) + middle : Int, + lower : Int, + tos : Int) + +object RibbonBars extends Marshallable[RibbonBars] { + implicit val codec : Codec[RibbonBars] = ( + ("upper" | uint16L) :: + ("middle" | uint16L) :: + ("lower" | uint16L) :: + ("tos" | uint16L) + ).as[RibbonBars] +} + +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, + upperMerit : Int, //0xFFFF means no merit (for all ...) + middleMerit : Int, + lowerMerit : Int, + termOfServiceMerit : Int, // healthMax : Int, // health : Int, // armor : Int, @@ -96,72 +110,62 @@ object WeaponData extends Marshallable[WeaponData] { // unk11 : Int, //134 // unk12 : Int, //199 // firstTimeEvent_length : Long, -// firstEntry : String, -// firstTimEvent_list : List[String], +// firstEntry : Option[String], +// firstTimeEvent_list : List[String], // tutorial_list : List[String], -// inventory : BitVector -// ) extends ConstructorData -// -//object CharacterData extends Marshallable[CharacterData] { -// //all ignore()s are intentional; please do not mess with them -// implicit val codec : Codec[CharacterData] = ( -// ("pos" | Vector3.codec_pos) :: -// ignore(16) :: -// ("obj_yaw" | uint8L) :: -// ignore(1) :: -// ("faction" | uintL(2)) :: -// ("bops" | bool) :: -// ignore(4) :: -// ignore(16) :: -// ("name" | PacketHelpers.encodedWideStringAligned(4)) :: -// ("exosuit" | uintL(3)) :: -// ignore(1) :: -// ignore(1) :: -// ("sex" | uintL(2)) :: -// ("face1" | uint4L) :: -// ("face2" | uint4L) :: -// ("voice" | uintL(3)) :: -// ignore(2) :: -// ignore(4) :: -// ignore(16) :: -// ("unk1" | uint16L) :: -// ignore(40) :: -// ignore(2) :: -// ("unk2" | uint16L) :: -// ignore(2) :: -// ignore(28) :: -// ("unk3" | uintL(4)) :: -// ignore(4) :: -// ignore(16) :: -// ignore(2) :: -// ("anchor" | bool) :: -// ignore(1) :: -// ("viewPitch" | uint8L) :: -// ("viewYaw" | uint8L) :: -// ignore(4) :: -// ignore(4) :: -// ignore(2) :: -// ("upperMerit" | uint16L) :: -// ("middleMerit" | uint16L) :: -// ("lowerMerit" | uint16L) :: -// ("termOfServiceMerit" | uint16L) :: -// ignore(2) :: -// ignore(156) :: -// ignore(2) :: + inventory : BitVector + ) extends ConstructorData + +object CharacterData extends Marshallable[CharacterData] { + val ribbonBars : Codec[RibbonBars] = ( + ("upper" | uint16L) :: + ("middle" | uint16L) :: + ("lower" | uint16L) :: + ("tos" | uint16L) + ).as[RibbonBars] + + implicit val codec : Codec[CharacterData] = ( + ("pos" | Vector3.codec_pos) :: + ignore(16) :: + ("objYaw" | uint8L) :: + ignore(1) :: + ("faction" | uintL(2)) :: + ("bops" | bool) :: + ignore(20) :: + ("name" | PacketHelpers.encodedWideStringAligned(4)) :: + ("exosuit" | uintL(3)) :: + ignore(2) :: + ("sex" | uintL(2)) :: + ("face1" | uint4L) :: + ("face2" | uint4L) :: + ("voice" | uintL(3)) :: + ignore(22) :: + ("unk1" | uint16L) :: + ignore(42) :: + ("unk2" | uint16L) :: + ignore(30) :: + ("unk3" | uintL(4)) :: + ignore(24) :: + ("viewPitch" | uint8L) :: + ("viewYaw" | uint8L) :: + ignore(10) :: + ("upperMerit" | uint16L) :: + ("middleMerit" | uint16L) :: + ("lowerMerit" | uint16L) :: + ("termOfServiceMerit" | uint16L) :: +// ignore(160) :: // ("healthMax" | uint16L) :: // ("health" | uint16L) :: // ignore(1) :: // ("armor" | uint16L) :: -// ignore(1) :: -// ignore(8) :: +// ignore(9) :: // ("unk4" | uint8L) :: // ignore(8) :: // ("unk5" | uint4L) :: // ("unk6" | uintL(3)) :: // ("staminaMax" | uint16L) :: // ("stamina" | uint16L) :: -// ignore(148) :: -// ignore(4) :: +// ignore(152) :: // ("unk7" | uint16L) :: // ("unk8" | uint8L) :: // ("unk9" | uint8L) :: @@ -170,49 +174,49 @@ object WeaponData extends Marshallable[WeaponData] { // ("unk12" | uintL(12)) :: // ignore(3) :: // (("firstTimeEvent_length" | uint32L) >>:~ { len => -// ("firstEntry" | PacketHelpers.encodedStringAligned(5)) :: -// ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) -// }) :: -// ("tutorial_list" | PacketHelpers.listOfNAligned(uint32L, 0, PacketHelpers.encodedString)) :: -// ignore(204) :: -// ignore(3) :: -// ("inventory" | bits) -// ).as[CharacterData] -//} +// conditional(len > 0, "firstEntry" | PacketHelpers.encodedStringAligned(5)) :: +// ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: +// ("tutorial_list" | PacketHelpers.listOfNAligned(uint32L, 0, PacketHelpers.encodedString)) :: +// ignore(207) :: + ("inventory" | bits) +// }) + ).as[CharacterData] +} /** - * na + * 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 */ -case class InternalMold(objectClass : Int, +case class InternalSlot(objectClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : Option[ConstructorData]) -object InternalMold extends Marshallable[InternalMold] { +object InternalSlot extends Marshallable[InternalSlot] { type objPattern = Int :: PlanetSideGUID :: Int :: Option[ConstructorData] :: HNil - implicit val codec : Codec[InternalMold] = ( + implicit val codec : Codec[InternalSlot] = ( ignore(1) :: //TODO determine what this bit does ("objectClass" | uintL(11)) :: ("guid" | PlanetSideGUID.codec) :: ("parentSlot" | PacketHelpers.encodedStringSize) :: - ("data" | bits) + bits ).exmap[objPattern] ( { case _ :: cls :: guid :: slot :: data :: HNil => - Attempt.successful(cls :: guid :: slot :: Mold.selectMold(cls, data) :: HNil) + Attempt.successful(cls :: guid :: slot :: Mold.decode(cls, data) :: HNil) }, { case cls :: guid :: slot :: None :: HNil => Attempt.failure(Err("no constuctor data could be found")) case cls :: guid :: slot :: mold :: HNil => - Attempt.successful(() :: cls :: guid :: slot :: Mold.serialize(cls, mold.get) :: HNil) + Attempt.successful(() :: cls :: guid :: slot :: Mold.encode(cls, mold.get) :: HNil) } - ).exmap[objPattern] ( + ).exmap[objPattern] ( { case cls :: guid :: slot :: None :: HNil => Attempt.failure(Err("no decoded constructor data")) @@ -225,12 +229,12 @@ object InternalMold extends Marshallable[InternalMold] { case cls :: guid :: slot :: data :: HNil => Attempt.successful(cls :: guid :: slot :: data :: HNil) } - ).as[InternalMold] + ).as[InternalSlot] } case class Mold(objectClass : Int, data : BitVector) { - private var obj : Option[ConstructorData] = Mold.selectMold(objectClass, data) + private var obj : Option[ConstructorData] = Mold.decode(objectClass, data) def isDefined : Boolean = this.obj.isDefined @@ -248,55 +252,59 @@ case class Mold(objectClass : Int, object Mold { def apply(objectClass : Int, obj : ConstructorData) : Mold = - new Mold( objectClass, Mold.serialize(objectClass, obj) ) + new Mold( objectClass, Mold.encode(objectClass, obj) ) - def selectMold(objClass : Int, data : BitVector) : Option[ConstructorData] = { + def decode(objClass : Int, data : BitVector) : Option[ConstructorData] = { var out : Option[ConstructorData] = None if(!data.isEmpty) { - (objClass : @switch) match { - case 0x1C => //9mm - val opt = AmmoBoxData.codec.decode(data).toOption - if(opt.isDefined) - out = Some(opt.get.value) - case 0x46 => //beamer - val opt = WeaponData.codec.decode(data).toOption - if(opt.isDefined) - out = Some(opt.get.value) - case 0x159 => //gauss - val opt = WeaponData.codec.decode(data).toOption - if(opt.isDefined) - out = Some(opt.get.value) - case _ => + var outOpt : Option[DecodeResult[_]] = None + try { + (objClass : @switch) match { + case 0x79 => //avatars + outOpt = CharacterData.codec.decode(data).toOption + case 0x1C => //9mm + outOpt = AmmoBoxData.codec.decode(data).toOption + case 0x46 => //beamer + outOpt = WeaponData.codec.decode(data).toOption + case 0x159 => //gauss + outOpt = WeaponData.codec.decode(data).toOption + case _ => + } + if(outOpt.isDefined) + out = Some(outOpt.get.value.asInstanceOf[ConstructorData]) + } + catch { + case ex : ClassCastException => + //TODO generate and log wrong class error message + case ex : Exception => + //TODO generic error } } out } - def serialize(objClass : Int, obj : ConstructorData) : BitVector = { + def encode(objClass : Int, obj : ConstructorData) : BitVector = { var out = BitVector.empty try { + var outOpt : Option[BitVector] = None (objClass : @switch) match { case 0x1C => //9mm - val opt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption - if(opt.isDefined) - out = opt.get + outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption case 0x46 => //beamer - val opt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption - if(opt.isDefined) - out = opt.get + outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption case 0x159 => //gauss - val opt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption - if(opt.isDefined) - out = opt.get + outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption case _ => throw new ClassCastException("cannot find object code - "+objClass) } + if(outOpt.isDefined) + out = outOpt.get } catch { case ex : ClassCastException => - //TODO generate and log wrong class error message + //TODO generate and log wrong class error message case ex : Exception => - //TODO generic error + //TODO generic error } out } @@ -453,7 +461,6 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { ObjectCreateMessage(len, cls, guid, info, Mold(cls, data)) }, { - //the user should not have to manually supply a proper stream length, that's a restrictive requirement case ObjectCreateMessage(_, cls, guid, info, mold) => streamLen(info, mold.data) :: cls :: guid :: info :: mold.data :: HNil } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 90e9e8b4..0dce9916 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -150,6 +150,7 @@ class GamePacketTest extends Specification { val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16) 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_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 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" "decode (2)" in { PacketCoding.DecodePacket(packet2).require match { @@ -165,6 +166,42 @@ class GamePacketTest extends Specification { } } + "decode (char)" in { + PacketCoding.DecodePacket(string_testchar).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => + len mustEqual 3159 + cls mustEqual 0x79 + guid mustEqual PlanetSideGUID(75) + parent.isDefined mustEqual false + mold.isDefined mustEqual true + + val char = mold.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.upperMerit mustEqual 0xFFFF //none + char.middleMerit mustEqual 0xFFFF //none + char.lowerMerit mustEqual 0xFFFF //none + char.termOfServiceMerit mustEqual 0xFFFF //none + case default => + ko + } + } + "decode (9mm)" in { PacketCoding.DecodePacket(string_9mm).require match { case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => @@ -194,7 +231,7 @@ class GamePacketTest extends Specification { mold.isDefined mustEqual true val obj_wep = mold.get.asInstanceOf[WeaponData] obj_wep.unk mustEqual 4 - val obj_ammo = obj_wep.ammo//.asInstanceOf[InternalMold] + val obj_ammo = obj_wep.ammo//.asInstanceOf[InternalSlot] obj_ammo.objectClass mustEqual 28 obj_ammo.guid mustEqual PlanetSideGUID(1286) obj_ammo.parentSlot mustEqual 0