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 2fcb5c231..0de046cad 100644
--- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala
@@ -6,52 +6,161 @@ import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless._
-case class ObjectCreateMessageParent(guid : Int, slot : Int)
+/**
+ * The parent information of a created object.
+ *
+ * Rather than a created-parent with a created-child relationship, the whole of the packet still only creates the child.
+ * The parent is a pre-existing object into which the (created) child is attached.
+ *
+ * The slot is encoded as a string length integer commonly used by PlanetSide.
+ * It is either a 0-127 eight bit number (0 = 0x80), or a 128-32767 sixteen bit number (128 = 0x0080).
+ * @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)
-case class ObjectCreateMessage(streamLength : Long, // in bits
+/**
+ * Communicate with the client that a certain object with certain properties is to be created.
+ * The object may also have primitive assignment (attachment) properties.
+ *
+ * In normal packet data order, the parent object is specified before the actual object is specified.
+ * This is most likely a method of early correction.
+ * "Does this parent object exist?"
+ * "Is this new object something that can be attached to this parent?"
+ * "Does the parent have the appropriate attachment slot?"
+ * There is no fail-safe method for any of these circumstances being false, however, and the object will simply not be created.
+ * In instance where the parent data does not exist, the object-specific data is immediately encountered.
+ *
+ * The object's GUID is assigned by the server.
+ * The clients are required to adhere to this new GUID referring to the object.
+ * There is no fail-safe for a conflict between what the server thinks is a new GUID and what any client thinks is an already-assigned GUID.
+ * Likewise, there is no fail-safe between a client failing or refusing to create an object and the server thinking an object has been created.
+ * (The GM-level command `/sync` tests for objects that "do not match" between the server and the client.
+ * It's implementation and scope are undefined.)
+ *
+ * Knowing the object's class is essential for parsing the specific information passed by the `data` parameter.
+ *
+ * Exploration:
+ * Can we build a `case class` "foo" that can accept the `objectClass` and the `data` and construct any valid object automatically?
+ * @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding
+ * @param objectClass the code for the type of object being constructed
+ * @param guid the GUID this object will be assigned
+ * @param parentInfo if defined, the relationship between this object and another object (its parent)
+ * @param data the data used to construct this type of object;
+ * requires further object-specific processing
+ */
+case class ObjectCreateMessage(streamLength : Long,
objectClass : Int,
- guid : Int,
+ guid : PlanetSideGUID,
parentInfo : Option[ObjectCreateMessageParent],
- stream : BitVector
- )
+ data : BitVector)
extends PlanetSideGamePacket {
-
def opcode = GamePacketOpcode.ObjectCreateMessage
def encode = ObjectCreateMessage.encode(this)
}
+object ObjectCreateMessageParent extends Marshallable[ObjectCreateMessageParent] {
+ implicit val codec : Codec[ObjectCreateMessageParent] = (
+ ("guid" | PlanetSideGUID.codec) ::
+ ("slot" | PacketHelpers.encodedStringSize)
+ ).as[ObjectCreateMessageParent]
+}
+
+
object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
+ type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil
+ /**
+ * Codec for formatting around the lack of parent data in the stream.
+ */
+ val noParent : Codec[Pattern] = (
+ ("objectClass" | uintL(0xb)) :: //11u
+ ("guid" | PlanetSideGUID.codec) //16u
+ ).xmap[Pattern] (
+ {
+ case cls :: guid :: HNil =>
+ cls :: guid :: None :: HNil
+ },
+ {
+ case cls :: guid :: None :: HNil =>
+ cls :: guid :: HNil
+ }
+ )
- type Pattern = Int :: Int :: Option[ObjectCreateMessageParent] :: HNil
- type ChoicePattern = Either[Pattern, Pattern]
+ /**
+ * Codec for reading and formatting parent data from the stream.
+ */
+ val parent : Codec[Pattern] = (
+ ("parentGuid" | PlanetSideGUID.codec) :: //16u
+ ("objectClass" | uintL(0xb)) :: //11u
+ ("guid" | PlanetSideGUID.codec) :: //16u
+ ("parentSlotIndex" | PacketHelpers.encodedStringSize) //8u or 16u
+ ).xmap[Pattern] (
+ {
+ case pguid :: cls :: guid :: slot :: HNil =>
+ cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil
+ },
+ {
+ case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil =>
+ pguid :: cls :: guid :: slot :: HNil
+ }
+ )
- val noParent : Codec[Pattern] = (("object_class" | uintL(0xb)) ::
- ("guid" | uint16L)).xmap[Pattern]( {
- case cls :: guid :: HNil => cls :: guid :: None :: HNil
- }, {
- case cls :: guid :: None :: HNil => cls :: guid :: HNil
- })
- val parent : Codec[Pattern] = (("parent_guid" | uint16L) ::
- ("object_class" | uintL(0xb)) ::
- ("guid" | uint16L) ::
- ("parent_slot_index" | PacketHelpers.encodedStringSize)).xmap[Pattern]( {
- case pguid :: cls :: guid :: slot :: HNil =>
- cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil
- }, {
- case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil =>
- pguid :: cls :: guid :: slot :: HNil
- })
+ /**
+ * Calculate the stream length in number of bits by factoring in the two variable fields.
+ *
+ * Constant fields have already been factored into the results.
+ * That includes:
+ * the length of the stream length field (32u),
+ * the object's class (11u),
+ * the object's GUID (16u),
+ * and the bit to determine if there will be parent data.
+ * In total, these fields form a known fixed length of 60u.
+ * @param parentInfo if defined, the parentInfo adds either 24u or 32u
+ * @param data the data length is indeterminate until it is read
+ * @return the total length of the stream in bits
+ */
+ private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : BitVector) : Long = {
+ (if(parentInfo.isDefined) {
+ if(parentInfo.get.slot > 127) 92 else 84 //60u + 16u + (8u or 16u)
+ }
+ else {
+ 60
+ }
+ + data.size)
+ }
implicit val codec : Codec[ObjectCreateMessage] = (
- ("stream_length" | uint32L) :: (either(bool, parent, noParent).exmap[Pattern]( {
- case Left(a :: b :: Some(c) :: HNil) => Attempt.successful(a :: b :: Some(c) :: HNil)
- case Right(a :: b :: None :: HNil) => Attempt.successful(a :: b :: None :: HNil)
- // failure cases
- case Left(a :: b :: None :: HNil) => Attempt.failure(Err("expected parent structure"))
- case Right(a :: b :: Some(c) :: HNil) => Attempt.failure(Err("got unexpected parent structure"))
- }, {
- case a :: b :: Some(c) :: HNil => Attempt.successful(Left(a :: b :: Some(c) :: HNil))
- case a :: b :: None :: HNil => Attempt.successful(Right(a :: b :: None :: HNil))
- }) :+ ("rest" | bits) )
- ).as[ObjectCreateMessage]
+ ("streamLength" | uint32L) ::
+ (either(bool, parent, noParent).exmap[Pattern] (
+ {
+ case Left(a :: b :: Some(c) :: HNil) =>
+ Attempt.successful(a :: b :: Some(c) :: HNil) //true, _, _, Some(c)
+ case Right(a :: b :: None :: HNil) =>
+ Attempt.successful(a :: b :: None :: HNil) //false, _, _, None
+ // failure cases
+ case Left(a :: b :: None :: HNil) =>
+ Attempt.failure(Err("missing parent structure")) //true, _, _, None
+ case Right(a :: b :: Some(c) :: HNil) =>
+ Attempt.failure(Err("unexpected parent structure")) //false, _, _, Some(c)
+ },
+ {
+ case a :: b :: Some(c) :: HNil =>
+ Attempt.successful(Left(a :: b :: Some(c) :: HNil))
+ case a :: b :: None :: HNil =>
+ Attempt.successful(Right(a :: b :: None :: HNil))
+ }
+ ) :+
+ ("data" | bits) )
+ ).xmap[ObjectCreateMessage] (
+ {
+ case len :: cls :: guid :: info :: data :: HNil =>
+ ObjectCreateMessage(len, cls, guid, info, data)
+ },
+ {
+ //the user should not have to manually supply a proper stream length, that's a restrictive requirement
+ case ObjectCreateMessage(_, cls, guid, info, data) =>
+ streamLen(info, data) :: cls :: guid :: info :: data :: HNil
+ }
+ ).as[ObjectCreateMessage]
}
diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala
index 2f7e8a11d..2d25cb26b 100644
--- a/common/src/test/scala/GamePacketTest.scala
+++ b/common/src/test/scala/GamePacketTest.scala
@@ -146,24 +146,27 @@ class GamePacketTest extends Specification {
"ObjectCreateMessage" should {
val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 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 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 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 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 "
- val packet2 = hex"18 17 74 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45"
+ val packet2 = hex"18 F8 00 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" //faked data?
+ val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16)
- "decode" in {
+ "decode (2)" in {
PacketCoding.DecodePacket(packet2).require match {
case obj @ ObjectCreateMessage(len, cls, guid, parent, rest) =>
- val manualRest = packet2.bits.drop(32 + 1 + 0xb + 16)
- len === 29719
- cls === 121
- guid === 2497
- rest === manualRest
- parent === None
+ len mustEqual 248
+ cls mustEqual 121
+ guid mustEqual PlanetSideGUID(2497)
+ parent mustEqual None
+ rest mustEqual packet2Rest
case default =>
ko
}
}
- "encode" in {
- ok
+ "encode (2)" in {
+ val msg = ObjectCreateMessage(0, 121, PlanetSideGUID(2497), None, packet2Rest)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual packet2
}
}