Distributed Object Protocol
Distributed Object Protocol
This section describes how the distributed object protocol creates, updates and deletes views on the client. The information is not 100% complete, as it was derived from reverse engineering, testing and observation.
MXO server uses "views" to create objects on the client. The views can create objects that are new to the world - like players, npcs, or views can point to objects that already exist in the world. For example a door may already exist in the world. But how does the server tell the client if the door is opened or closed? The server spawns a "view" based on the static object (door in this case) and by updating the view to open or closed position, the door becomes either opened or closed. For more information about the distributed object system design choices, see: http://www.gamasutra.com/view/feature/2948/distributing_object_state_for_.php
The distributed object protocol has an MPM id of 03. Here's how the general layout of it goes:
... 03 [viewId] [viewData] [viewId] [viewData] ...
viewId is of type uint16
viewData is a stream of bytes, its length and content depends on the view id, the viewData will be processed by the view update handler for that view id in the current context
The client parses the 03 protocol using the following steps:
- Read the view id
- If viewId is 0, break (end of distributed object protocol, return control to MPM)
- hand off the packet to the view id handler, which will consume the bytes after it properly
- Repeat Step 1
So viewId 0 is reserved for the "no more views" tag, ok. But how do we manipulate the context to get views inside of it anyway ? That's why the client's context ALWAYS has a view registered with viewId 1. This is the AdminView.
AdminView parses it's updates as directives that will change the client's context. Here's an overview of how it handles it's data (this is after the viewId, the data that actually gets passed to the view handler):
... [flags] [additional operation data] ...
flags - is of type uint8, it depending on which bit is set, the AdminView does a certain operation, the flags are of the following format [???? ndcc]
cc bits indicate the command id Other 2 bits are only used for the first command (object creation) d bit (specific for create command), isFullyDynamic flag (true = new object, false = based on existing object), n bit (specific for create command), isNormalObject (always set to true)
Table of possible values for cc bits:
|3||RelevanceNotices (S2S only)|
View Create Stream (command = 0)
Create view stream is used by the server to spawn objects on the client. It specifies exactly which object should be spawned and which properties should be populated.
Structure of the packet
To create a view id on the client, the server might send something like this (sample taken from the logs):
... 0c 7b 95 [optional world object id] 48 cd ab 02 09 00 00 00 00 93 ba d1 40 00 00 00 80 00 be a9 40 00 00 00 00 e3 c2 a6 40 00 00 00 80 f3 04 35 bf 00 00 00 80 f3 04 35 3f 69 00 00 ...
This specific packet creates a vendor on the roof of a building beside Mara Central Hardline. We are creating an object with goobj id 0x957b = 38267. This particular goobj is "DowntownMartialArtsMidHigh" according to the GObj database.
|0c||Because the flags byte contans data relevant to the creation of the object, it's included here. isFullyDynamic and isNormalObject is set in this byte.|
|7b 95||This is the game object id 0x957b = 38267 = "DowntownMartialArtsMidHigh"|
|[optional world object id]||uint32. For objects that are spawned based on an existing object, this will be populated with a world id. isFullyDynamic must be false for this.|
|48||This is just a spawn sequence, ideally every object that is being created, should increment this sequence.|
|cd ab||??? This is not known, but it is present in all the creation streams. Refered to as AB CD magic word.|
|02 09 00 00 ... 35 3f||This is the actual create view stream. In this stream all the properties are serialized, so that the client can create the object.|
|69 00||This is the view id that is being assigned to the new created object.|
How the client parses the view create stream
Through the use of a debugger and looking at client.dll, it was possible to dump out which property is being read, and what is the size of the property.
The output looks something like this:
-  0 - 24 : 00 00 00 00 93 ba d1 40 00 00 00 80 00 be a9 40 00 00 00 00 e3 c2 a6 40 00 00 00 80
-  3 - 16 : f3 04 35 bf 00 00 00 80 f3 04 35 3f
The first byte out of the stream indicates how many properties are being serialized in the stream. So in our case 02 means two properties.
09 is actually a bit vector, which looks like this 0000 1001. The bitvector has the following structure [xbbb bbbb]. if x is set, then it means there are more properties available, if any of the b are set, then it means that that specific property (position of the bit + 7 * number of iterations) is present in the stream.
So the order of the operations is as follows:
- Read first byte - number of properties present.
- Read byte - bitvector
- For each "b" bit in bitvector that is set, determine property number (7 * iteration + 1 << bit offset)
- Read the number of bytes the property takes up (deserialize the property)
- If the "x" bit is not set, no more properties are present.
- Increment number of iterations and goto step 2.
Putting it all together
How do we know what exact property we are reading and its size? There is a list available (researched by Rajko), that defines which goobjects can be created, what is the property order, and what is the type of the property (and its size). The list can be found here goobj creation table.
Vendor, in the example above has the following creation properties.
- LTVector3d (24 bytes) Position
- LTVector3f (12 bytes) HalfExtents
- REZ_ID (4 bytes) DescriptionID
- LTQuaternion (16 bytes) Orientation
Therefore when the server is sending property 0 and 3, we know that first property (index 0) will take up 24 bytes and it represents the position of the object. We also know that the next property (index 03) is the orientation property which is of type LTQuaternion and takes up 16 bytes.
By knowing the creation properties and their order, we can take any GO object and create it in the world!
Note that if you spawn views based on static objects, the attribute list will be parsed according to the goid in the packet, but what will be displayed in the world is the goid from the world (you cannot change goid of world objects). However, if the world object isnt loaded at the time the spawn packet arrives, nothing will be inherited from the world object (including its goid), so all that you will see is the goid the server created and the attributes it sent. So it's dangerous to use the same goid for say, all doors, even though it saves you from getting the attribute list of all the different door types (you can use the same attribute list/"packet format" for all doors), because if they arent loaded when the spawn packet comes, you will see the door type you sent, not the one that's supposed to be there in the world.
As for the attribute list format, the format in the packets is really quite simple, however you need to know the attribute list of the specific object you are updating/creating (different lists for update/create, and different list for PlayerCharacter update autoview vs view), which requires you to hook the update/create methods and parse some trees/make some knowledgeable observations, so i will leave this for you to find out, or post it later
View Delete Stream (command = 1)
Delete view stream is used to delete views on the client. It is pretty straightforward and has the following format:
... 01 [number of views] [view1] [view2] ... [view last]
|01||Flag 01 = means the command is "delete views"|
|[number of views]||uint16 numberOfViewsToDelete|
|[view1] [view2] ... [view last]||The list of views, uint16 per view.|
Pending Create Update Stream (command = 2)
This command is used when the server sent a Create command to the client, but didn't yet receive an ack for it, yet the newly created view needs to be updated. To prevent the client just discarding the update, the update is wrapped in this header, and all of these updates will be applied to the object once it's created on the client. The length word is required, because as the client might not have created the object yet, it wouldn't know how to parse the update data, so when it adds it to the queue, it needs to know how many bytes to add to the queue.
... 02 [update length] [updateData]...
|02||Flag 02 = means the command is "delayed update"|
|[update length]||uint16 the length of the update data and this number, so +2|
|[updateData]||Consists of uint16 viewId and byte stream of update data.|