From 4e41468cd07a0f3497d1c703fa65a0fe97989445 Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 30 May 2018 08:27:50 -0400 Subject: [PATCH] meta-stability in terms of the underlying structure that will eventually read off seated passengers in vehicles; the padding offsets just need tuning --- .../scala/net/psforever/packet/PSPacket.scala | 15 + .../game/ObjectCreateDetailedMessage.scala | 62 ++-- .../CharacterAppearanceData.scala | 5 +- .../game/objectcreate/CharacterData.scala | 27 +- .../game/objectcreate/ObjectClass.scala | 10 +- .../packet/game/objectcreate/PlayerData.scala | 41 ++- .../game/objectcreate/VehicleData.scala | 290 ++++++++++++++---- .../DetailedCharacterDataTest.scala | 219 ++++++++++++- .../UtilityVehiclesTest.scala | 183 +++++------ 9 files changed, 665 insertions(+), 187 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 15bdaa72..bb516208 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -231,6 +231,21 @@ object PacketHelpers { * @return a codec that works on a List of A but excludes the size from the encoding */ def listOfNSized[A](size : Long, codec : Codec[A]) : Codec[List[A]] = PacketHelpers.listOfNAligned(provide(if(size < 0) 0 else size), 0, codec) + + /** + * A `peek` that decodes like the normal but encodes nothing. + * Decoding `Codec[A]` from the input vector emits a value but reverts to the prior read position. + * Encoding `Codec[A]` to the input vector appends no new data to the input vector. + * In effect, `peek` is a harmless meta-`Codec` that introduces no changes to the input vector. + * @see `scodec.codecs.peek` or `codecs/package.scala:peek` + * @param target codec that decodes the value + * @return codec that behaves the same as `target` but resets remainder to the input vector + */ + def peek[A](target: Codec[A]): Codec[A] = new Codec[A] { + def sizeBound = target.sizeBound + def encode(a: A) = Attempt.Successful(BitVector.empty) + def decode(b: BitVector) = target.decode(b).map { _.mapRemainder(_ => b) } + } } /** diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala index 4040714f..36766fbf 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala @@ -71,28 +71,28 @@ object ObjectCreateDetailedMessage extends Marshallable[ObjectCreateDetailedMess ObjectCreateDetailedMessage(ObjectCreateBase.streamLen(None, data), objectClass, guid, None, Some(data)) } - /** - * Take the important information of a game piece and transform it into bit data. - * This function is fail-safe because it catches errors involving bad parsing of the object data. - * Generally, the `Exception` messages themselves are not useful here. - * @param objClass the code for the type of object being deconstructed - * @param obj the object data - * @return the bitstream data - * @see ObjectClass.selectDataCodec - */ - def encodeData(objClass : Int, obj : ConstructorData, getCodecFunc : (Int) => Codec[ConstructorData.genericPattern]) : BitVector = { - var out = BitVector.empty - try { - val outOpt : Option[BitVector] = getCodecFunc(objClass).encode(Some(obj.asInstanceOf[ConstructorData])).toOption - if(outOpt.isDefined) - out = outOpt.get - } - catch { - case _ : Exception => - //catch and release, any sort of parse error - } - out - } +// /** +// * Take the important information of a game piece and transform it into bit data. +// * This function is fail-safe because it catches errors involving bad parsing of the object data. +// * Generally, the `Exception` messages themselves are not useful here. +// * @param objClass the code for the type of object being deconstructed +// * @param obj the object data +// * @return the bitstream data +// * @see ObjectClass.selectDataCodec +// */ +// def encodeData(objClass : Int, obj : ConstructorData, getCodecFunc : (Int) => Codec[ConstructorData.genericPattern]) : BitVector = { +// var out = BitVector.empty +// try { +// val outOpt : Option[BitVector] = getCodecFunc(objClass).encode(Some(obj.asInstanceOf[ConstructorData])).toOption +// if(outOpt.isDefined) +// out = outOpt.get +// } +// catch { +// case _ : Exception => +// //catch and release, any sort of parse error +// } +// out +// } implicit val codec : Codec[ObjectCreateDetailedMessage] = ObjectCreateBase.baseCodec.exmap[ObjectCreateDetailedMessage] ( { @@ -100,7 +100,14 @@ object ObjectCreateDetailedMessage extends Marshallable[ObjectCreateDetailedMess Attempt.failure(Err("no data to decode")) case len :: cls :: guid :: par :: data :: HNil => - val obj = ObjectCreateBase.decodeData(cls, data, ObjectClass.selectDataDetailedCodec) + val obj = ObjectCreateBase.decodeData(cls, data, + if(par.isDefined) { + ObjectClass.selectDataDetailedCodec + } + else { + ObjectClass.selectDataDroppedDetailedCodec + } + ) Attempt.successful(ObjectCreateDetailedMessage(len, cls, guid, par, obj)) }, { @@ -109,7 +116,14 @@ object ObjectCreateDetailedMessage extends Marshallable[ObjectCreateDetailedMess case ObjectCreateDetailedMessage(_, cls, guid, par, Some(obj)) => val len = ObjectCreateBase.streamLen(par, obj) //even if a stream length has been assigned, it can not be trusted during encoding - val bitvec = ObjectCreateBase.encodeData(cls, obj, ObjectClass.selectDataDetailedCodec) + val bitvec = ObjectCreateBase.encodeData(cls, obj, + if(par.isDefined) { + ObjectClass.selectDataDetailedCodec + } + else { + ObjectClass.selectDataDroppedDetailedCodec + } + ) Attempt.successful(len :: cls :: guid :: par :: bitvec :: HNil) } ) diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala index 581b81a2..a16de51e 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala @@ -148,7 +148,8 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { */ def namePaddingRule(pad : Int) : Int = if(pad == 0) { - 1 //normal alignment padding for the string + //note that this counts as 1 (local) + 4 (inherited from PlayerData OCM with parent) + 5 //normal alignment padding } else { pad //custom padding value @@ -171,7 +172,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { ("jammered" | bool) :: bool :: //crashes client uint(16) :: //unknown, but usually 0 - ("name" | PacketHelpers.encodedWideStringAligned( namePaddingRule(name_padding) )) :: + ("name" | PacketHelpers.encodedWideStringAligned(name_padding)) :: ("exosuit" | ExoSuitType.codec) :: ignore(2) :: //unknown ("sex" | CharacterGender.codec) :: 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 9ce35e9b..fae8ab44 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 @@ -115,7 +115,7 @@ object CharacterData extends Marshallable[CharacterData] { { case health :: armor :: uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: HNil => val newHealth = if(is_backpack) { 0 } else { health } - Attempt.Successful(new CharacterData(newHealth, armor, uniform, 0, cr, implant_effects, cosmetics)(is_backpack)) + Attempt.Successful(CharacterData(newHealth, armor, uniform, 0, cr, implant_effects, cosmetics)(is_backpack)) case _ => Attempt.Failure(Err("invalid character data; can not encode")) @@ -130,5 +130,30 @@ object CharacterData extends Marshallable[CharacterData] { } ) + def codec_seated(is_backpack : Boolean) : Codec[CharacterData] = ( + ("uniform_upgrade" | UniformStyle.codec) >>:~ { style => + ignore(3) :: //unknown + ("command_rank" | uintL(3)) :: + bool :: //stream misalignment when != 1 + optional(bool, "implant_effects" | ImplantEffects.codec) :: + conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec) + } + ).exmap[CharacterData] ( + { + case uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: HNil => + Attempt.Successful(new CharacterData(100, 0, uniform, 0, cr, implant_effects, cosmetics)(is_backpack)) + + case _ => + Attempt.Failure(Err("invalid character data; can not encode")) + }, + { + case CharacterData(_, _, uniform, _, cr, implant_effects, cosmetics) => + Attempt.Successful(uniform :: () :: cr :: false :: implant_effects :: cosmetics :: HNil) + + case _ => + Attempt.Failure(Err("invalid character data; can not decode")) + } + ) + implicit val codec : Codec[CharacterData] = codec(false) } 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 00135101..281360a9 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 @@ -666,13 +666,21 @@ object ObjectClass { case ObjectClass.advanced_ace => ConstructorData.genericCodec(DetailedACEData.codec, "advanced ace") case ObjectClass.boomer_trigger => ConstructorData.genericCodec(DetailedBoomerTriggerData.codec, "boomer trigger") //other - case ObjectClass.avatar => ConstructorData.genericCodec(DetailedPlayerData.codec(true), "avatar") + case ObjectClass.avatar => ConstructorData.genericCodec(DetailedPlayerData.codec(false), "avatar") case ObjectClass.locker_container => ConstructorData.genericCodec(DetailedLockerContainerData.codec, "locker container") //failure case case _ => defaultFailureCodec(objClass) } + def selectDataDroppedDetailedCodec(objClass : Int) : Codec[ConstructorData.genericPattern] = + (objClass : @switch) match { + //special cases + case ObjectClass.avatar => ConstructorData.genericCodec(DetailedPlayerData.codec(true), "avatar") + //defer to other codec selection + case _ => selectDataDetailedCodec(objClass) + } + /** * Given an object class, retrieve the `Codec` used to parse and translate the constructor data for that type. * This function services `0x17` `ObjectCreateMessage` packet data.
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala index f808af50..8045b069 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.packet.game.objectcreate +import net.psforever.newcodecs._ import net.psforever.packet.Marshallable import scodec.codecs._ import scodec.Codec @@ -94,21 +95,43 @@ object PlayerData extends Marshallable[PlayerData] { * @return the pad length in bits */ def placementOffset(pos : Option[PlacementData]) : Int = { - if(pos.isEmpty) { - 0 - } - else if(pos.get.vel.isDefined) { - 2 - } - else { - 4 + pos match { + case Some(place) => + if(place.vel.isDefined) { 2 } else { 4 } + case None => + 0 } } def codec(position_defined : Boolean) : Codec[PlayerData] = ( conditional(position_defined, "pos" | PlacementData.codec) >>:~ { pos => ("basic_appearance" | CharacterAppearanceData.codec(placementOffset(pos))) >>:~ { app => - ("character_data" | CharacterData.codec(app.backpack)) :: + ("character_data" | newcodecs.binary_choice(position_defined, + CharacterData.codec(app.backpack), + CharacterData.codec_seated(app.backpack))) :: + optional(bool, "inventory" | InventoryData.codec) :: + ("drawn_slot" | DrawnSlot.codec) :: + bool //usually false + } + }).xmap[PlayerData] ( + { + case pos :: app :: data :: inv :: hand :: _ :: HNil => + PlayerData(pos, app, data, inv, hand)(pos.isDefined) + }, + { + case PlayerData(pos, app, data, inv, hand) => + pos :: app :: data :: inv :: hand :: false :: HNil + } + ) + + + + def codec(position_defined : Boolean, offset : Int) : Codec[PlayerData] = ( + conditional(position_defined, "pos" | PlacementData.codec) >>:~ { pos => + ("basic_appearance" | CharacterAppearanceData.codec(offset)) >>:~ { app => + ("character_data" | newcodecs.binary_choice(position_defined, + CharacterData.codec(app.backpack), + CharacterData.codec_seated(app.backpack))) :: optional(bool, "inventory" | InventoryData.codec) :: ("drawn_slot" | DrawnSlot.codec) :: bool //usually false diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala index 34eac81d..412517e4 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala @@ -1,13 +1,16 @@ // Copyright (c) 2017 PSForever package net.psforever.packet.game.objectcreate + +import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.{Marshallable, PacketHelpers} import scodec.Attempt.{Failure, Successful} import scodec.{Attempt, Codec, Err} +import shapeless.HNil import scodec.codecs._ -import shapeless.{::, HNil} - import net.psforever.types.DriveState +import scala.collection.mutable.ListBuffer + /** * An `Enumeration` of the various formats that known structures that the stream of bits for `VehicleData` can assume. */ @@ -148,33 +151,39 @@ object VehicleData extends Marshallable[VehicleData] { /** * `Codec` for the "utility" format. */ - private val utility_data_codec : Codec[SpecificVehicleData] = uintL(6).hlist.exmap[SpecificVehicleData] ( - { - case n :: HNil => - Successful(UtilityVehicleData(n).asInstanceOf[SpecificVehicleData]) - }, - { - case UtilityVehicleData(n) => - Successful(n :: HNil) - case _ => - Failure(Err("wrong kind of vehicle data object (wants 'Utility')")) - } - ) + private val utility_data_codec : Codec[SpecificVehicleData] = { + import shapeless.:: + uintL(6).hlist.exmap[SpecificVehicleData] ( + { + case n :: HNil => + Successful(UtilityVehicleData(n).asInstanceOf[SpecificVehicleData]) + }, + { + case UtilityVehicleData(n) => + Successful(n :: HNil) + case _ => + Failure(Err("wrong kind of vehicle data object (wants 'Utility')")) + } + ) + } /** * `Codec` for the "variant" format. */ - private val variant_data_codec : Codec[SpecificVehicleData] = uint8L.hlist.exmap[SpecificVehicleData] ( - { - case n :: HNil => - Successful(VariantVehicleData(n).asInstanceOf[SpecificVehicleData]) - }, - { - case VariantVehicleData(n) => - Successful(n :: HNil) - case _ => - Failure(Err("wrong kind of vehicle data object (wants 'Variant')")) - } - ) + private val variant_data_codec : Codec[SpecificVehicleData] = { + import shapeless.:: + uint8L.hlist.exmap[SpecificVehicleData] ( + { + case n :: HNil => + Successful(VariantVehicleData(n).asInstanceOf[SpecificVehicleData]) + }, + { + case VariantVehicleData(n) => + Successful(n :: HNil) + case _ => + Failure(Err("wrong kind of vehicle data object (wants 'Variant')")) + } + ) + } /** * Select an appropriate `Codec` in response to the requested stream format @@ -190,47 +199,202 @@ object VehicleData extends Marshallable[VehicleData] { Failure(Err(s"$vehicleFormat is not a valid vehicle format for parsing data")).asInstanceOf[Codec[SpecificVehicleData]] } - def codec(vehicle_type : VehicleFormat.Value) : Codec[VehicleData] = ( - ("basic" | CommonFieldData.codec) :: - ("unk1" | uint2L) :: - ("health" | uint8L) :: - ("unk2" | bool) :: //usually 0 - ("no_mount_points" | bool) :: - ("driveState" | driveState8u) :: //used for deploy state - ("unk3" | bool) :: //unknown but generally false; can cause stream misalignment if set when unexpectedly - ("unk4" | bool) :: - ("cloak" | bool) :: //cloak as wraith, phantasm - conditional(vehicle_type != VehicleFormat.Normal, "unk5" | selectFormatReader(vehicle_type)) :: //padding? - optional(bool, "inventory" | InventoryData.codec) - ).exmap[VehicleData] ( - { - case basic :: u1 :: health :: u2 :: no_mount :: driveState :: u3 :: u4 :: u5 :: cloak :: inv :: HNil => - Attempt.successful(new VehicleData(basic, u1, health, u2, no_mount, driveState, u3, u4, u5, cloak, inv)(vehicle_type)) + def codec(vehicle_type : VehicleFormat.Value) : Codec[VehicleData] = { + import shapeless.:: + ( + ("basic" | CommonFieldData.codec) >>:~ { com => + ("unk1" | uint2L) :: + ("health" | uint8L) :: + ("unk2" | bool) :: //usually 0 + ("no_mount_points" | bool) :: + ("driveState" | driveState8u) :: //used for deploy state + ("unk3" | bool) :: //unknown but generally false; can cause stream misalignment if set when unexpectedly + ("unk4" | bool) :: + ("cloak" | bool) :: //cloak as wraith, phantasm + conditional(vehicle_type != VehicleFormat.Normal, "unk5" | selectFormatReader(vehicle_type)) :: //padding? + optional(bool, "inventory" | custom_inventory_codec(InitialStreamLengthToSeatEntries(com, vehicle_type))) + } + ).exmap[VehicleData] ( + { + case basic :: u1 :: health :: u2 :: no_mount :: driveState :: u3 :: u4 :: u5 :: cloak :: inv :: HNil => + Attempt.successful(new VehicleData(basic, u1, health, u2, no_mount, driveState, u3, u4, u5, cloak, inv)(vehicle_type)) - case _ => - Attempt.failure(Err("invalid vehicle data format")) - }, - { - case obj @ VehicleData(basic, u1, health, u2, no_mount, driveState, u3, u4, cloak, Some(u5), inv) => - if(obj.vehicle_type == VehicleFormat.Normal) { - Attempt.failure(Err("invalid vehicle data format; variable bits not expected; will ignore ...")) - } - else { - Attempt.successful(basic :: u1 :: health :: u2 :: no_mount :: driveState :: u3 :: u4 :: cloak :: Some(u5) :: inv :: HNil) - } + case _ => + Attempt.failure(Err("invalid vehicle data format")) + }, + { + case obj @ VehicleData(basic, u1, health, u2, no_mount, driveState, u3, u4, cloak, Some(u5), inv) => + if(obj.vehicle_type == VehicleFormat.Normal) { + Attempt.failure(Err("invalid vehicle data format; variable bits not expected; will ignore ...")) + } + else { + Attempt.successful(basic :: u1 :: health :: u2 :: no_mount :: driveState :: u3 :: u4 :: cloak :: Some(u5) :: inv :: HNil) + } - case obj @ VehicleData(basic, u1, health, u2, no_mount, driveState, u3, u4, cloak, None, inv) => - if(obj.vehicle_type != VehicleFormat.Normal) { - Attempt.failure(Err("invalid vehicle data format; variable bits expected")) - } - else { - Attempt.successful(basic :: u1 :: health :: u2 :: no_mount :: driveState :: u3 :: u4 :: cloak :: None :: inv :: HNil) - } + case obj @ VehicleData(basic, u1, health, u2, no_mount, driveState, u3, u4, cloak, None, inv) => + if(obj.vehicle_type != VehicleFormat.Normal) { + Attempt.failure(Err("invalid vehicle data format; variable bits expected")) + } + else { + Attempt.successful(basic :: u1 :: health :: u2 :: no_mount :: driveState :: u3 :: u4 :: cloak :: None :: inv :: HNil) + } - case _ => - Attempt.failure(Err("invalid vehicle data format")) + case _ => + Attempt.failure(Err("invalid vehicle data format")) + } + ) + } + + private def InitialStreamLengthToSeatEntries(com : CommonFieldData, format : VehicleFormat.Type) : Long = { + 198 + + (if(com.pos.vel.isDefined) { 42 } else { 0 }) + + (format match { + case VehicleFormat.Utility => 6 + case VehicleFormat.Variant => 8 + case _ => 0 + }) + } + + private def custom_inventory_codec(length : Long) : Codec[InventoryData] = { + import shapeless.:: + ( + uint8 >>:~ { size => + uint2 :: + (inventory_seat_codec( + length, //length of stream until current seat + { //calculated offset of name field in next seat + val next = length + 23 + 35 //in bits: InternalSlot lead + length of CharacterAppearanceData~>name + val pad =((next - math.floor(next / 8) * 8) % 8).toInt + if(pad > 0) { + 8 - pad + } + else { + 0 + } + } + ) >>:~ { seats => + PacketHelpers.listOfNSized(size - countSeats(seats), InternalSlot.codec).hlist + }) + } + ).xmap[InventoryData] ( + { + case _ :: _ :: None :: inv :: HNil => + InventoryData(inv) + + case _ :: _ :: seats :: inv :: HNil => + InventoryData(unlinkSeats(seats) ++ inv) + }, + { + case InventoryData(inv) => + val (seats, slots) = inv.partition(entry => entry.objectClass == ObjectClass.avatar) + inv.size :: 0 :: chainSeats(seats) :: slots :: HNil + } + ) + } + + private case class InventorySeat(seat : Option[InternalSlot], next : Option[InventorySeat]) + + private def inventory_seat_codec(length : Long, offset : Int) : Codec[Option[InventorySeat]] = { + import shapeless.:: + ( + PacketHelpers.peek(uintL(11)) >>:~ { objClass => + conditional(objClass == ObjectClass.avatar, seat_codec(offset)) >>:~ { seat => + conditional(objClass == ObjectClass.avatar, inventory_seat_codec( + { //length of stream until next seat + length + (seat match { + case Some(o) => o.bitsize + case None => 0 + }) + }, + { //calculated offset of name field in next seat + val next = length + 23 + 35 + (seat match { + case Some(o) => o.bitsize + case None => 0 + }) + val pad =((next - math.floor(next / 8) * 8) % 8).toInt + if(pad > 0) { + 8 - pad + } + else { + 0 + } + })).hlist + } + } + ).exmap[Option[InventorySeat]] ( + { + case _ :: None :: None :: HNil => + Successful(None) + + case _ :: slot :: Some(next) :: HNil => + Successful(Some(InventorySeat(slot, next))) + }, + { + case None => + Successful(0 :: None :: None :: HNil) + + case Some(InventorySeat(slot, None)) => + Successful(ObjectClass.avatar :: slot :: None :: HNil) + + case Some(InventorySeat(slot, next)) => + Successful(ObjectClass.avatar :: slot :: Some(next) :: HNil) + } + ) + } + + private def seat_codec(pad : Int) : Codec[InternalSlot] = { + import shapeless.:: + ( + ("objectClass" | uintL(11)) :: + ("guid" | PlanetSideGUID.codec) :: + ("parentSlot" | PacketHelpers.encodedStringSize) :: + ("obj" | PlayerData.codec(false, pad)) + ).xmap[InternalSlot] ( + { + case objectClass :: guid :: parentSlot :: obj :: HNil => + InternalSlot(objectClass, guid, parentSlot, obj) + }, + { + case InternalSlot(objectClass, guid, parentSlot, obj) => + objectClass :: guid :: parentSlot :: obj.asInstanceOf[PlayerData] :: HNil + } + ) + } + + private def countSeats(chain : Option[InventorySeat]) : Int = { + chain match { + case None => + 0 + case Some(link) => + if(link.seat.isDefined) { 1 } else { 0 } + countSeats(link.next) } - ) + } + + private def unlinkSeats(chain : Option[InventorySeat]) : List[InternalSlot] = { + var curr = chain + val out = new ListBuffer[InternalSlot] + while(curr.isDefined) { + curr.get.seat match { + case None => + curr = None + case Some(seat) => + out += seat + curr = curr.get.next + } + } + out.toList + } + + private def chainSeats(list : List[InternalSlot]) : Option[InventorySeat] = { + list match { + case Nil => + None + case x :: Nil => + Some(InventorySeat(Some(x), None)) + case x :: xs => + Some(InventorySeat(Some(x), chainSeats(xs))) + } + } implicit val codec : Codec[VehicleData] = codec(VehicleFormat.Normal) } diff --git a/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala b/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala index b7fb703b..fdc658ae 100644 --- a/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala +++ b/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala @@ -10,6 +10,14 @@ import scodec.bits._ class DetailedCharacterDataTest extends Specification { 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_seated = + hex"181f0c000066d5bc84b00808000012e049006c006c006c004900490049006c006c006c0049006c0049006c006c0049006c006c006c0049006c006c0049008452" ++ + hex"700000000000000000000000000000002000000fe6a703fffffffffffffffffffffffffffffffc00000000000000000000000000000000000000019001900064" ++ + hex"000001007ec800c80000000000000000000000000000000000000001c00042c54686c700000080000012407870655f73616e6374756172795f68656c70907870" ++ + hex"655f74685f666972656d6f6465738b757365645f6265616d6572856d617031330000000000000000000000000000000000000000000000000000000000010a23" ++ + hex"02600404400000100006020814d0080c80000200026b4e0082880000020000c041c09e01019000006400442a0010910000004000180838944020320000008019" ++ + hex"0548021720000008007029804364000032000e0540089c8000064001c0aa0119900000c8003a1580287200001900040ab805264000032006c25800a788000002" ++ + hex"0000800000" val string_testchar_br32 = hex"18 2c e0 00 00 bc 84 B0 00 0b ea 00 6c 7d f1 10 00 00 02 40 00 08 60 4b 00 69 00 43 00 6b 00 4a 00 72 00 02 31 3a cc 82 c0 00 00 00 00 00 00 00 00 3e df 42 00 20 00 0e 00 40 43 40 4c 04 00 02 e8 00 00 03 a8 00 00 01 9c 04 00 00 b8 99 84 00 0e 68 28 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 c8 00 00 01 00 7e c8 00 5c 00 00 01 29 c1 cc 80 00 00 00 00 00 00 00 00 00 00 00 00 03 c0 00 40 81 01 c4 45 46 86 c8 88 c9 09 4a 4a 80 50 0c 13 00 00 15 00 80 00 48 00 7870655f6f766572686561645f6d6170 8d7870655f776172705f676174658f7870655f666f726d5f6f75746669748c7870655f626c61636b6f7073927870655f636f6d6d616e645f72616e6b5f35927870655f636f6d6d616e645f72616e6b5f33927870655f73616e6374756172795f68656c70927870655f626174746c655f72616e6b5f3133927870655f626174746c655f72616e6b5f3132927870655f626174746c655f72616e6b5f3130927870655f626174746c655f72616e6b5f3134927870655f626174746c655f72616e6b5f3135937870655f6f72626974616c5f73687574746c658c7870655f64726f705f706f64917870655f62696e645f666163696c697479917870655f626174746c655f72616e6b5f33917870655f626174746c655f72616e6b5f35917870655f626174746c655f72616e6b5f348e7870655f6a6f696e5f73717561648e7870655f666f726d5f7371756164927870655f696e7374616e745f616374696f6e917870655f626174746c655f72616e6b5f32937870655f776172705f676174655f7573616765917870655f626174746c655f72616e6b5f38927870655f626174746c655f72616e6b5f3131917870655f626174746c655f72616e6b5f368e7870655f6d61696c5f616c657274927870655f636f6d6d616e645f72616e6b5f31927870655f626174746c655f72616e6b5f3230927870655f626174746c655f72616e6b5f3138927870655f626174746c655f72616e6b5f3139907870655f6a6f696e5f706c61746f6f6e927870655f626174746c655f72616e6b5f3137927870655f626174746c655f72616e6b5f31368f7870655f6a6f696e5f6f7574666974927870655f626174746c655f72616e6b5f3235927870655f626174746c655f72616e6b5f3234927870655f636f6d6d616e645f72616e6b5f34907870655f666f726d5f706c61746f6f6e8c7870655f62696e645f616d73917870655f626174746c655f72616e6b5f39917870655f626174746c655f72616e6b5f378d7870655f74685f726f757465728c7870655f74685f666c61696c8a7870655f74685f616e748a7870655f74685f616d738f7870655f74685f67726f756e645f708c7870655f74685f6169725f708c7870655f74685f686f7665728d7870655f74685f67726f756e648a7870655f74685f626672927870655f74685f61667465726275726e65728a7870655f74685f6169728c7870655f74685f636c6f616b89757365645f6f69637791757365645f616476616e6365645f61636597766973697465645f73706974666972655f74757272657498766973697465645f73706974666972655f636c6f616b656493766973697465645f73706974666972655f616192766973697465645f74616e6b5f7472617073a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f6e63a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f74728e757365645f6d61676375747465728f757365645f636861696e626c6164658f757365645f666f726365626c61646593766973697465645f77616c6c5f74757272657498766973697465645f616e6369656e745f7465726d696e616c8b766973697465645f616d738b766973697465645f616e7490766973697465645f64726f707368697091766973697465645f6c6962657261746f7294766973697465645f6c6967687467756e7368697091766973697465645f6c696768746e696e6790766973697465645f6d616772696465728f766973697465645f70726f776c657293766973697465645f71756164737465616c746890766973697465645f736b7967756172649a766973697465645f74687265656d616e686561767962756767799d766973697465645f74776f5f6d616e5f61737361756c745f627567677998766973697465645f74776f6d616e6865617679627567677998766973697465645f74776f6d616e686f766572627567677990766973697465645f76616e67756172648d766973697465645f666c61696c8e766973697465645f726f7574657293766973697465645f737769746368626c6164658e766973697465645f6175726f726193766973697465645f626174746c657761676f6e8c766973697465645f6675727993766973697465645f7175616461737361756c7496766973697465645f67616c6178795f67756e736869708e766973697465645f6170635f74728e766973697465645f6170635f767390766973697465645f6c6f64657374617290766973697465645f7068616e7461736d91766973697465645f7468756e64657265728e766973697465645f6170635f6e638f766973697465645f76756c747572658c766973697465645f7761737090766973697465645f6d6f73717569746f97766973697465645f617068656c696f6e5f666c6967687497766973697465645f617068656c696f6e5f67756e6e657297766973697465645f636f6c6f737375735f666c6967687497766973697465645f636f6c6f737375735f67756e6e657298766973697465645f706572656772696e655f666c6967687498766973697465645f706572656772696e655f67756e6e657289757365645f62616e6b95766973697465645f7265736f757263655f73696c6f9e766973697465645f63657274696669636174696f6e5f7465726d696e616c94766973697465645f6d65645f7465726d696e616c93757365645f6e616e6f5f64697370656e73657295766973697465645f73656e736f725f736869656c649a766973697465645f62726f6164636173745f77617270676174658c757365645f7068616c616e7894757365645f7068616c616e785f6176636f6d626f96757365645f7068616c616e785f666c616b636f6d626f96766973697465645f77617270676174655f736d616c6c91757365645f666c616d657468726f7765729a757365645f616e6369656e745f7475727265745f776561706f6e92766973697465645f4c4c555f736f636b657492757365645f656e657267795f67756e5f6e6397766973697465645f6d656469756d7472616e73706f72749f757365645f617068656c696f6e5f696d6d6f6c6174696f6e5f63616e6e6f6e93757365645f6772656e6164655f706c61736d6193757365645f6772656e6164655f6a616d6d657298766973697465645f736869656c645f67656e657261746f7295766973697465645f6d6f74696f6e5f73656e736f7296766973697465645f6865616c74685f6372797374616c96766973697465645f7265706169725f6372797374616c97766973697465645f76656869636c655f6372797374616c91757365645f6772656e6164655f6672616788757365645f61636598766973697465645f6164765f6d65645f7465726d696e616c8b757365645f6265616d657290757365645f626f6c745f6472697665728b757365645f6379636c65728a757365645f676175737391757365645f68756e7465727365656b657288757365645f6973708b757365645f6c616e6365728b757365645f6c61736865728e757365645f6d61656c7374726f6d8c757365645f70686f656e69788b757365645f70756c7361728d757365645f70756e69736865728e757365645f725f73686f7467756e8d757365645f7261646961746f7288757365645f72656b8d757365645f72657065617465728c757365645f726f636b6c65748c757365645f737472696b65728f757365645f73757070726573736f728c757365645f7468756d7065729c766973697465645f76616e755f636f6e74726f6c5f636f6e736f6c6598766973697465645f636170747572655f7465726d696e616c92757365645f6d696e695f636861696e67756e91757365645f6c617a655f706f696e7465728c757365645f74656c657061648b757365645f7370696b657291757365645f68656176795f736e6970657293757365645f636f6d6d616e645f75706c696e6b8d757365645f66697265626972648e757365645f666c6563686574746594757365645f68656176795f7261696c5f6265616d89757365645f696c63399a766973697465645f67656e657261746f725f7465726d696e616c8e766973697465645f6c6f636b65729a766973697465645f65787465726e616c5f646f6f725f6c6f636b9c766973697465645f6169725f76656869636c655f7465726d696e616c97766973697465645f67616c6178795f7465726d696e616c98766973697465645f696d706c616e745f7465726d696e616c99766973697465645f7365636f6e646172795f6361707475726590757365645f32356d6d5f63616e6e6f6e99757365645f6c6962657261746f725f626f6d6261726469657293766973697465645f7265706169725f73696c6f93766973697465645f76616e755f6d6f64756c6591757365645f666c61696c5f776561706f6e8b757365645f73637974686598766973697465645f7265737061776e5f7465726d696e616c8c757365645f62616c6c67756e92757365645f656e657267795f67756e5f747295757365645f616e6e69766572736172795f67756e6195757365645f616e6e69766572736172795f67756e6294757365645f616e6e69766572736172795f67756e90757365645f37356d6d5f63616e6e6f6e92757365645f6170635f6e635f776561706f6e92757365645f6170635f74725f776561706f6e92757365645f6170635f76735f776561706f6e90757365645f666c75785f63616e6e6f6e9f757365645f617068656c696f6e5f706c61736d615f726f636b65745f706f6491757365645f617068656c696f6e5f7070618c757365645f666c7578706f6494766973697465645f6266725f7465726d696e616c9e757365645f636f6c6f737375735f636c75737465725f626f6d625f706f64a0757365645f636f6c6f737375735f6475616c5f3130306d6d5f63616e6e6f6e7399757365645f636f6c6f737375735f74616e6b5f63616e6e6f6e96766973697465645f656e657267795f6372797374616c9b757365645f68656176795f6772656e6164655f6c61756e6368657298757365645f33356d6d5f726f74617279636861696e67756e8b757365645f6b6174616e6190757365645f33356d6d5f63616e6e6f6e93757365645f7265617665725f776561706f6e7396757365645f6c696768746e696e675f776561706f6e738c757365645f6d65645f61707090757365645f32306d6d5f63616e6e6f6e98766973697465645f6d6f6e6f6c6974685f616d657269736899766973697465645f6d6f6e6f6c6974685f636572797368656e97766973697465645f6d6f6e6f6c6974685f637973736f7297766973697465645f6d6f6e6f6c6974685f6573616d697299766973697465645f6d6f6e6f6c6974685f666f72736572616c99766973697465645f6d6f6e6f6c6974685f697368756e64617298766973697465645f6d6f6e6f6c6974685f7365617268757397766973697465645f6d6f6e6f6c6974685f736f6c73617292757365645f6e635f6865765f66616c636f6e99757365645f6e635f6865765f7363617474657263616e6e6f6e93757365645f6e635f6865765f73706172726f7791757365645f61726d6f725f736970686f6e9f757365645f706572656772696e655f6475616c5f6d616368696e655f67756e9f757365645f706572656772696e655f6475616c5f726f636b65745f706f647399757365645f706572656772696e655f6d65636868616d6d65729e757365645f706572656772696e655f7061727469636c655f63616e6e6f6e96757365645f706572656772696e655f73706172726f7791757365645f3130356d6d5f63616e6e6f6e92757365645f31356d6d5f636861696e67756ea0757365645f70756c7365645f7061727469636c655f616363656c657261746f7293757365645f726f74617279636861696e67756e9f766973697465645f6465636f6e737472756374696f6e5f7465726d696e616c95757365645f736b7967756172645f776561706f6e7391766973697465645f67656e657261746f7291757365645f67617573735f63616e6e6f6e89757365645f7472656b95757365645f76616e67756172645f776561706f6e73a4766973697465645f616e6369656e745f6169725f76656869636c655f7465726d696e616ca2766973697465645f616e6369656e745f65717569706d656e745f7465726d696e616c96766973697465645f6f726465725f7465726d696e616ca7766973697465645f616e6369656e745f67726f756e645f76656869636c655f7465726d696e616c9f766973697465645f67726f756e645f76656869636c655f7465726d696e616c97757365645f76756c747572655f626f6d6261726469657298757365645f76756c747572655f6e6f73655f63616e6e6f6e98757365645f76756c747572655f7461696c5f63616e6e6f6e97757365645f776173705f776561706f6e5f73797374656d91766973697465645f636861726c6965303191766973697465645f636861726c6965303291766973697465645f636861726c6965303391766973697465645f636861726c6965303491766973697465645f636861726c6965303591766973697465645f636861726c6965303691766973697465645f636861726c6965303791766973697465645f636861726c6965303891766973697465645f636861726c6965303996766973697465645f67696e6765726d616e5f6174617298766973697465645f67696e6765726d616e5f646168616b6196766973697465645f67696e6765726d616e5f6876617296766973697465645f67696e6765726d616e5f697a686199766973697465645f67696e6765726d616e5f6a616d7368696498766973697465645f67696e6765726d616e5f6d697468726198766973697465645f67696e6765726d616e5f726173686e7599766973697465645f67696e6765726d616e5f7372616f73686198766973697465645f67696e6765726d616e5f79617a61746195766973697465645f67696e6765726d616e5f7a616c8e766973697465645f736c656430318e766973697465645f736c656430328e766973697465645f736c656430348e766973697465645f736c656430358e766973697465645f736c656430368e766973697465645f736c656430378e766973697465645f736c6564303897766973697465645f736e6f776d616e5f616d657269736898766973697465645f736e6f776d616e5f636572797368656e96766973697465645f736e6f776d616e5f637973736f7296766973697465645f736e6f776d616e5f6573616d697298766973697465645f736e6f776d616e5f666f72736572616c96766973697465645f736e6f776d616e5f686f7373696e98766973697465645f736e6f776d616e5f697368756e64617297766973697465645f736e6f776d616e5f7365617268757396766973697465645f736e6f776d616e5f736f6c736172857567643036857567643035857567643034857567643033857567643032857567643031856d61703939856d61703938856d61703937856d61703936856d61703135856d61703134856d61703131856d61703038856d61703034856d61703035856d61703033856d61703031856d61703036856d61703032856d61703039856d61703037856d617031300300000091747261696e696e675f73746172745f6e638b747261696e696e675f75698c747261696e696e675f6d61700000000000000000000000000000000000000000800000003d0c04d350840240000010000602429660f80c80000c8004200c1b81480000020000c046f18a47019000019000ca4644304900000040001809e6bb052032000008001a84787211200000080003010714889c06400000100320ff0a42e4000001009e95a7342e03200000080003010408c914064000000001198990c4e4000001000060223b9b2180c800000a00081c20c92c800003600414ec172d900000040001808de1284a0320000320008ef1c336b20000078011d830e6f6400000600569c417e2c80000020000c04102502f019000008c00ce31027d99000000400018099e6146203200004b0015a7d44002f720000008000301040c18dc064000023000b1240800636400000100006020e0e92280c80000c800081650c00cfc800006400ce32a1801a59000000400018099e6fc3e03200004b00058b14680463200000080003010742610c064000043000b16c8880916400000100006020e0d01580c80000c8006714e24012cc80000020000c04cf25c190190000258001032e240307900000c8019c74470061b2000000800030133ced8fc0640000960012d9a8d00f0640000010025b9c1401e4c8000002004b6b23c03d1900000040098f585007b3200000080131a58c00f864000001002536f1c01f4c8000002004a64e2a03f190000004015e1b4580873200000080003010711f8a406400000100110a00c010ee400000100006020e2a51380c8000002002218d21021ec80000020000c041c40249019000000400af18a44043f90000004000180838b44760320000008015e38c80088320000008000301071490cc064000001002bc35890110e400000100006020e2052180c800000200221f90d0222c80000020000c041c5e447019000000400442e62e044790000004000180838af032032000000800886d08c089320000008000301071738740640000010011098898112e400000100006020e2361c80c8000002002212a1b0226c80000020000c041c512170190000004004420a32044f900000040001808389104a0320000008008874c8808a3200000080003010715907c06400000100110c0898114e400000100006020e2771a80c800000200578bd13022ac80000020000c041c424330190000004004423848045790000004000180838bfc32032000000801a86506008b320000008000301071030dc06400000100129f68a0117640000010026353110232c8000002004b69438046d90000004015e2887008eb200000080003010715909406400000100350fb8e011de400000100006020e2881980c8000002005786d0f023cc80000020000c041c4cc3b019000000400af1ba1c047b90000004000180838af872032000000800886344408fb20000008000301071620d406400000100110c10b011fe400000100006020e2870d80c800000200578f30c0240c80000020000c041c5863b019000000400442ee300483900000040001808388605e032000000801a86f03c090b200000080003010712a8fc064000001002bc0d858121e400000100006020e2521c80c800000200578b7230244c80000020000c041c49629019000000400d434026048b90000004000180838afc42032000000801a86d864091b200000080003010711989c064000001003508c8c8123e400000100006020e2a82280c8000002006a14f110248c80000020000c041c4be21019000000400af12640049390000004000180838a54720320000008015e33430092b20000008000301071228cc064000001003546e8d432400000100004f34a631139000004001b0834723120000008000204000c2ed0fa1c800000200a8432234a90000004000180952b248a0320000018004024c569d20000008000250a4d0ebc480000020000c04a24bc43019000000c00e0" "DetailedCharacterData" should { @@ -151,6 +159,142 @@ class DetailedCharacterDataTest extends Specification { } } + "decode (character, seated)" in { + PacketCoding.DecodePacket(string_testchar_seated).require match { + case ObjectCreateDetailedMessage(len, cls, guid, parent, data) => + len mustEqual 3103 + cls mustEqual ObjectClass.avatar + guid mustEqual PlanetSideGUID(75) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(43981) + parent.get.slot mustEqual 0 + data match { + case Some(DetailedPlayerData(None, basic, char, inv, hand)) => + basic.app.name mustEqual "IlllIIIlllIlIllIlllIllI" + basic.app.faction mustEqual PlanetSideEmpire.VS + basic.app.sex mustEqual CharacterGender.Female + basic.app.head mustEqual 41 + basic.app.voice mustEqual 1 //female 1 + basic.voice2 mustEqual 3 + basic.black_ops mustEqual false + basic.jammered mustEqual false + basic.exosuit mustEqual ExoSuitType.Standard + basic.outfit_name mustEqual "" + basic.outfit_logo mustEqual 0 + basic.backpack mustEqual false + basic.facingPitch mustEqual 2.8125f + basic.facingYawUpper mustEqual 210.9375f + basic.lfs mustEqual true + basic.grenade_state mustEqual GrenadeState.None + basic.is_cloaking mustEqual false + basic.charging_pose mustEqual false + basic.on_zipline mustEqual false + basic.ribbons.upper mustEqual MeritCommendation.None + basic.ribbons.middle mustEqual MeritCommendation.None + basic.ribbons.lower mustEqual MeritCommendation.None + basic.ribbons.tos mustEqual MeritCommendation.None + + char.bep mustEqual 0 + char.cep mustEqual 0 + char.healthMax mustEqual 100 + char.health mustEqual 100 + char.armor mustEqual 50 //standard exosuit value + char.unk1 mustEqual 1 + char.unk2 mustEqual 7 + char.unk3 mustEqual 7 + char.staminaMax mustEqual 100 + char.stamina mustEqual 100 + char.certs.length mustEqual 7 + char.certs.head mustEqual CertificationType.StandardAssault + char.certs(1) mustEqual CertificationType.MediumAssault + char.certs(2) mustEqual CertificationType.ATV + char.certs(3) mustEqual CertificationType.Harasser + char.certs(4) mustEqual CertificationType.StandardExoSuit + char.certs(5) mustEqual CertificationType.AgileExoSuit + char.certs(6) mustEqual CertificationType.ReinforcedExoSuit + char.implants.length mustEqual 0 + char.firstTimeEvents.size mustEqual 4 + char.firstTimeEvents.head mustEqual "xpe_sanctuary_help" + char.firstTimeEvents(1) mustEqual "xpe_th_firemodes" + char.firstTimeEvents(2) mustEqual "used_beamer" + char.firstTimeEvents(3) mustEqual "map13" + char.tutorials.size mustEqual 0 + char.cosmetics.isDefined mustEqual false + inv.isDefined mustEqual true + val inventory = inv.get.contents + inventory.size mustEqual 10 + //0 + inventory.head.objectClass mustEqual ObjectClass.beamer + inventory.head.guid mustEqual PlanetSideGUID(76) + inventory.head.parentSlot mustEqual 0 + var wep = inventory.head.obj.asInstanceOf[DetailedWeaponData] + wep.ammo.head.objectClass mustEqual ObjectClass.energy_cell + wep.ammo.head.guid mustEqual PlanetSideGUID(77) + wep.ammo.head.parentSlot mustEqual 0 + wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 16 + //1 + inventory(1).objectClass mustEqual ObjectClass.suppressor + inventory(1).guid mustEqual PlanetSideGUID(78) + inventory(1).parentSlot mustEqual 2 + wep = inventory(1).obj.asInstanceOf[DetailedWeaponData] + wep.ammo.head.objectClass mustEqual ObjectClass.bullet_9mm + wep.ammo.head.guid mustEqual PlanetSideGUID(79) + wep.ammo.head.parentSlot mustEqual 0 + wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 25 + //2 + inventory(2).objectClass mustEqual ObjectClass.forceblade + inventory(2).guid mustEqual PlanetSideGUID(80) + inventory(2).parentSlot mustEqual 4 + wep = inventory(2).obj.asInstanceOf[DetailedWeaponData] + wep.ammo.head.objectClass mustEqual ObjectClass.melee_ammo + wep.ammo.head.guid mustEqual PlanetSideGUID(81) + wep.ammo.head.parentSlot mustEqual 0 + wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 1 + //3 + inventory(3).objectClass mustEqual ObjectClass.locker_container + inventory(3).guid mustEqual PlanetSideGUID(82) + inventory(3).parentSlot mustEqual 5 + inventory(3).obj.isInstanceOf[DetailedLockerContainerData] mustEqual true + inventory(3).obj.asInstanceOf[DetailedLockerContainerData].inventory.isDefined mustEqual false + //4 + inventory(4).objectClass mustEqual ObjectClass.bullet_9mm + inventory(4).guid mustEqual PlanetSideGUID(83) + inventory(4).parentSlot mustEqual 6 + inventory(4).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //5 + inventory(5).objectClass mustEqual ObjectClass.bullet_9mm + inventory(5).guid mustEqual PlanetSideGUID(84) + inventory(5).parentSlot mustEqual 9 + inventory(5).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //6 + inventory(6).objectClass mustEqual ObjectClass.bullet_9mm + inventory(6).guid mustEqual PlanetSideGUID(85) + inventory(6).parentSlot mustEqual 12 + inventory(6).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //7 + inventory(7).objectClass mustEqual ObjectClass.bullet_9mm_AP + inventory(7).guid mustEqual PlanetSideGUID(86) + inventory(7).parentSlot mustEqual 33 + inventory(7).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //8 + inventory(8).objectClass mustEqual ObjectClass.energy_cell + inventory(8).guid mustEqual PlanetSideGUID(87) + inventory(8).parentSlot mustEqual 36 + inventory(8).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //9 + inventory(9).objectClass mustEqual ObjectClass.remote_electronics_kit + inventory(9).guid mustEqual PlanetSideGUID(88) + inventory(9).parentSlot mustEqual 39 + //the rek has data but none worth testing here + hand mustEqual DrawnSlot.Pistol1 + case _ => + ko + } + case _ => + ko + } + } + "decode (BR32)" in { PacketCoding.DecodePacket(string_testchar_br32).require match { case ObjectCreateDetailedMessage(len, cls, guid, parent, data) => @@ -225,7 +369,7 @@ class DetailedCharacterDataTest extends Specification { } } - "encode (character)" in { + "encode" in { val pos : PlacementData = PlacementData( 3674.8438f, 2726.789f, 91.15625f, 0, 0, 36.5625f @@ -300,6 +444,79 @@ class DetailedCharacterDataTest extends Specification { //TODO work on DetailedCharacterData to make this pass as a single stream } + "encode (character, seated)" in { + val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData( + BasicCharacterData( + "IlllIIIlllIlIllIlllIllI", + PlanetSideEmpire.VS, + CharacterGender.Female, + 41, + 1 + ), + 3, + false, + false, + ExoSuitType.Standard, + "", + 0, + false, + 2.8125f, 210.9375f, + true, + GrenadeState.None, + false, + false, + false, + RibbonBars() + ) + val char : (Option[Int])=>DetailedCharacterData = DetailedCharacterData( + 0, + 0, + 100, 100, + 50, + 1, 7, 7, + 100, 100, + List( + CertificationType.StandardAssault, + CertificationType.MediumAssault, + CertificationType.ATV, + CertificationType.Harasser, + CertificationType.StandardExoSuit, + CertificationType.AgileExoSuit, + CertificationType.ReinforcedExoSuit + ), + List(), + "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, + List.empty, + None + ) + val inv = InventoryData( + InventoryItemData(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(4, 8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) :: + InventoryItemData(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(4, 8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) :: + InventoryItemData(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(4, 8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) :: + InventoryItemData(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedLockerContainerData(8)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) :: + Nil + ) + val obj = DetailedPlayerData.apply(app, char, inv, DrawnSlot.Pistol1) + + val msg = ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), ObjectCreateMessageParent(PlanetSideGUID(43981), 0), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + val pkt_bitv = pkt.toBitVector + val ori_bitv = string_testchar_seated.toBitVector +// var test = pkt_bitv +// while(test.nonEmpty) { +// val (printHex, save) = test.splitAt(512) +// test = save +// println(printHex) +// } + pkt_bitv mustEqual ori_bitv + } + "encode (character, br32)" in { val pos : PlacementData = PlacementData( Vector3(5500.0f, 3800.0f, 71.484375f), diff --git a/common/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala b/common/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala index 930c7e3a..02f43faf 100644 --- a/common/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala +++ b/common/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala @@ -11,100 +11,111 @@ import scodec.bits._ class UtilityVehiclesTest extends Specification { val string_ant = hex"17 C2000000 9E0 7C01 6C2D7 65535 CA16 00 00 00 4400003FC000000" val string_ams = hex"17 B8010000 970 3D10 002D765535CA16000000 402285BB0037E4100749E1D03000000620D83A0A00000195798741C00000332E40D84800000" + val string_ams_seated = + hex"17ec060000970fe0f030898abda28127f007ff9c1f2f80c0001e18ff00001051e40786400000008c50004c0041006d0069006e006700790075006500540052007c00000304217c859e8080000000000000002503420022c02a002a002a002a0050004c0041002a002a002a002a00010027e300940000016c0400023c040002285a086c2f00c80000000000300210288740800000004046f17423018000002c4d6190400000001010704a86406000002bc770842000000004041c5f21d01800000e075821902000000623e84208000001950588c1800000332ea0f840000000" "Utility vehicles" should { - "decode (ant)" in { - PacketCoding.DecodePacket(string_ant).require match { +// "decode (ant)" in { +// PacketCoding.DecodePacket(string_ant).require match { +// case ObjectCreateMessage(len, cls, guid, parent, data) => +// len mustEqual 194L +// cls mustEqual ObjectClass.ant +// guid mustEqual PlanetSideGUID(380) +// parent.isDefined mustEqual false +// data.isDefined mustEqual true +// data.get.isInstanceOf[VehicleData] mustEqual true +// val ant = data.get.asInstanceOf[VehicleData] +// ant.basic.pos.coord.x mustEqual 3674.8438f +// ant.basic.pos.coord.y mustEqual 2726.789f +// ant.basic.pos.coord.z mustEqual 91.15625f +// ant.basic.pos.orient.x mustEqual 0f +// ant.basic.pos.orient.y mustEqual 0f +// ant.basic.pos.orient.z mustEqual 90.0f +// ant.basic.faction mustEqual PlanetSideEmpire.VS +// ant.basic.unk mustEqual 2 +// ant.basic.player_guid mustEqual PlanetSideGUID(0) +// ant.health mustEqual 255 +// ant.driveState mustEqual DriveState.Mobile +// case _ => +// ko +// } +// } +// +// "decode (ams)" in { +// PacketCoding.DecodePacket(string_ams).require match { +// case ObjectCreateMessage(len, cls, guid, parent, data) => +// len mustEqual 440L +// cls mustEqual ObjectClass.ams +// guid mustEqual PlanetSideGUID(4157) +// parent.isDefined mustEqual false +// data.isDefined mustEqual true +// data.get.isInstanceOf[VehicleData] mustEqual true +// val ams = data.get.asInstanceOf[VehicleData] +// ams.basic.pos.coord.x mustEqual 3674.0f +// ams.basic.pos.coord.y mustEqual 2726.789f +// ams.basic.pos.coord.z mustEqual 91.15625f +// ams.basic.pos.orient.x mustEqual 0f +// ams.basic.pos.orient.y mustEqual 0f +// ams.basic.pos.orient.z mustEqual 90.0f +// ams.basic.faction mustEqual PlanetSideEmpire.VS +// ams.basic.unk mustEqual 0 +// ams.basic.player_guid mustEqual PlanetSideGUID(34082) +// ams.unk1 mustEqual 2 +// ams.health mustEqual 236 +// ams.unk2 mustEqual false +// ams.driveState mustEqual DriveState.Deployed +// +// ams.inventory.isDefined mustEqual true +// val inv = ams.inventory.get.contents +// inv.head.objectClass mustEqual ObjectClass.matrix_terminalc +// inv.head.guid mustEqual PlanetSideGUID(3663) +// inv.head.parentSlot mustEqual 1 +// inv.head.obj.isInstanceOf[CommonTerminalData] mustEqual true +// inv(1).objectClass mustEqual ObjectClass.ams_respawn_tube +// inv(1).guid mustEqual PlanetSideGUID(3638) +// inv(1).parentSlot mustEqual 2 +// inv(1).obj.isInstanceOf[CommonTerminalData] mustEqual true +// inv(2).objectClass mustEqual ObjectClass.order_terminala +// inv(2).guid mustEqual PlanetSideGUID(3827) +// inv(2).parentSlot mustEqual 3 +// inv(2).obj.isInstanceOf[CommonTerminalData] mustEqual true +// inv(3).objectClass mustEqual ObjectClass.order_terminalb +// inv(3).guid mustEqual PlanetSideGUID(3556) +// inv(3).parentSlot mustEqual 4 +// inv(3).obj.isInstanceOf[CommonTerminalData] mustEqual true +// case _ => +// ko +// } +// } +// + "decode (ams, seated)" in { + PacketCoding.DecodePacket(string_ams_seated).require match { case ObjectCreateMessage(len, cls, guid, parent, data) => - len mustEqual 194L - cls mustEqual ObjectClass.ant - guid mustEqual PlanetSideGUID(380) - parent.isDefined mustEqual false data.isDefined mustEqual true - data.get.isInstanceOf[VehicleData] mustEqual true - val ant = data.get.asInstanceOf[VehicleData] - ant.basic.pos.coord.x mustEqual 3674.8438f - ant.basic.pos.coord.y mustEqual 2726.789f - ant.basic.pos.coord.z mustEqual 91.15625f - ant.basic.pos.orient.x mustEqual 0f - ant.basic.pos.orient.y mustEqual 0f - ant.basic.pos.orient.z mustEqual 90.0f - ant.basic.faction mustEqual PlanetSideEmpire.VS - ant.basic.unk mustEqual 2 - ant.basic.player_guid mustEqual PlanetSideGUID(0) - ant.health mustEqual 255 - ant.driveState mustEqual DriveState.Mobile case _ => ko } } - - "decode (ams)" in { - PacketCoding.DecodePacket(string_ams).require match { - case ObjectCreateMessage(len, cls, guid, parent, data) => - len mustEqual 440L - cls mustEqual ObjectClass.ams - guid mustEqual PlanetSideGUID(4157) - parent.isDefined mustEqual false - data.isDefined mustEqual true - data.get.isInstanceOf[VehicleData] mustEqual true - val ams = data.get.asInstanceOf[VehicleData] - ams.basic.pos.coord.x mustEqual 3674.0f - ams.basic.pos.coord.y mustEqual 2726.789f - ams.basic.pos.coord.z mustEqual 91.15625f - ams.basic.pos.orient.x mustEqual 0f - ams.basic.pos.orient.y mustEqual 0f - ams.basic.pos.orient.z mustEqual 90.0f - ams.basic.faction mustEqual PlanetSideEmpire.VS - ams.basic.unk mustEqual 0 - ams.basic.player_guid mustEqual PlanetSideGUID(34082) - ams.unk1 mustEqual 2 - ams.health mustEqual 236 - ams.unk2 mustEqual false - ams.driveState mustEqual DriveState.Deployed - - ams.inventory.isDefined mustEqual true - val inv = ams.inventory.get.contents - inv.head.objectClass mustEqual ObjectClass.matrix_terminalc - inv.head.guid mustEqual PlanetSideGUID(3663) - inv.head.parentSlot mustEqual 1 - inv.head.obj.isInstanceOf[CommonTerminalData] mustEqual true - inv(1).objectClass mustEqual ObjectClass.ams_respawn_tube - inv(1).guid mustEqual PlanetSideGUID(3638) - inv(1).parentSlot mustEqual 2 - inv(1).obj.isInstanceOf[CommonTerminalData] mustEqual true - inv(2).objectClass mustEqual ObjectClass.order_terminala - inv(2).guid mustEqual PlanetSideGUID(3827) - inv(2).parentSlot mustEqual 3 - inv(2).obj.isInstanceOf[CommonTerminalData] mustEqual true - inv(3).objectClass mustEqual ObjectClass.order_terminalb - inv(3).guid mustEqual PlanetSideGUID(3556) - inv(3).parentSlot mustEqual 4 - inv(3).obj.isInstanceOf[CommonTerminalData] mustEqual true - case _ => - ko - } - } - - "encode (ant)" in { - val obj = VehicleData( - CommonFieldData( - PlacementData(3674.8438f, 2726.789f, 91.15625f, 0f, 0f, 90.0f), - PlanetSideEmpire.VS, 2 - ), - 0, - 255, - false, false, - DriveState.Mobile, - false, false, false, - Some(UtilityVehicleData(0)), - None - )(VehicleFormat.Utility) - val msg = ObjectCreateMessage(ObjectClass.ant, PlanetSideGUID(380), obj) - val pkt = PacketCoding.EncodePacket(msg).require.toByteVector - - pkt mustEqual string_ant - } +// +// "encode (ant)" in { +// val obj = VehicleData( +// CommonFieldData( +// PlacementData(3674.8438f, 2726.789f, 91.15625f, 0f, 0f, 90.0f), +// PlanetSideEmpire.VS, 2 +// ), +// 0, +// 255, +// false, false, +// DriveState.Mobile, +// false, false, false, +// Some(UtilityVehicleData(0)), +// None +// )(VehicleFormat.Utility) +// val msg = ObjectCreateMessage(ObjectClass.ant, PlanetSideGUID(380), obj) +// val pkt = PacketCoding.EncodePacket(msg).require.toByteVector +// +// pkt mustEqual string_ant +// } "encode (ams)" in { val obj = VehicleData(