Serialization layer
Many people suggested the implementation of a serialization layer. This layer needs to be on top of the network layer. We don't want the AgentServer to re-serialize all packets!
The goal if the serialization layer is to turn the data stream of a message with a given layout...
1 byte Content.ID
2 ushort Username.Length
* string Username
2 ushort Password.Length
* string Password
2 ushort Shard.ID
...in an object representation...
public class LoginPacket
{
public byte ContentID { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public short ShardID { get; set; }
}
that gets automatically filled when received, and written into a message when sending. But the conditionality of fields inside a message makes this no easy task!
I've parsed ~500 SR_Client packets in vSRO 1.188 (~900 total), and I can say a lot of them use types inside the packet instead of different opcodes per action.
- There are around 40 inventory operation types in a single packet.
- There are around 50 guild update types in a single packet.
- There are around 50 fortress update types while only using 3 operations (2Req/Ack, 1NoDir).
The per opcode class representing that packet would still need to contain all fields for all branches, even if they're not taken. If you come up with something clever to tackle this, your help is greatly appreciated, but try to avoid dynamic and reflection as much as possible in the serialization and handling part.
The worst edge cases come from the Server->Client packets, if we implement this layer only for receiving from client it's only half as bad, I'll admit. But here it is anyway the edge case collection packet.
1 byte result
// internal condition
if(result == 1)
{
4 uint UniqueID // Get the RefObjID from spawn data to get the correct RefObj (from textfiles or database)
32 string String0 // hardcoded string length of 8, 16, ..., 2048
// external condition
if(refObj.TypeID1 == 1)
{
1 byte valueFlag
// bit flags
if(flag & 1)
{
4 uint field0
}
if(flag & 2)
{
2 ushort field1
}
}
// Lists
1 byte itemCount // this could be sbyte, byte, short, ushort, int, uint, long, ulong
foreach(item)
{
2 ushort item.Value0
4 uint item.Value1;
2 ushort item.StringLength
* string item.String
}
// Arrays
1 byte arraySize // this could be sbyte, byte, short, ushort, int, uint, long, ulong
* byte[] arrayValues // this could be byte[], sbyte[], short[], ushort[], int[], uint[], long[], ulong[], string[]
if(arrayValues.Contains(5)) // if the array has an entry with the value of MyEnum.EnumValue = 5 for example.
{
2 ushort field2
}
// Enumeration
1 byte moveNext
while(moveNext == 1)
{
4 uint enumeratorValue
1 byte moveNext
}
// This is not a joke. There are packets, where you have to check if the player has a job suit item equipped at EQUIP_SLOT_EXTRA = 8.
if(player.IsWearingJobSuit()) //(TID1 == 3 && TID2 == 7 && (TID3 == 1 || TID3 == 2 || TID3 == 3)
{
4 uint field3
}
}
else
{
2 ushort errorCode
}
Finally I'd like to add my opinion (I know I'm repeating myself). We have the de-serializer going over message data reading everything into the class properties, checking the conditions (probably multiple times unless you can define blocks) and we also need to check the conditions again manually when we actually handle the de-serialized message (the class) because we are not supposed to access fields that we've not read. We end up writing the same (or similar code) twice. First as annotation (attributes) and a second time in the handler by accessing the serialized class. Reading directly from the message allows us to use the stack for temporary control bytes while maintaining control over the branching and access to contextual information.
I'll also present my "sort of in the middle" solution, message objects.
interface IMessageObject : IMessageReadable, IMessageWriteable
{
}
interface IMessageReadable
{
MessageResult TryRead(MessagReader reader);
}
interface IMessageWriteable
{
MessageResult TryWrite(MessageWriter writer);
}
Message objects can also be the data model itself, here it can also be created from database.

Defining read and write is not necessary depending on whether or not the message will only be received or sent. In fact, we could utilize the serialization on a small scale with these objects while keeping the branching manually in the handler.