Okay, I actually came up with this idea like a week ago, but I’ll post about it anyway.
I’ve been thinking lately that I’m tired of the conventional ways of parsing packets, because:
- Manually indenting the output sucks.
- Manually typing out an unknown value counter sucks.
- The code gets way too long.
Optimally, you’d automatically serialize the packets 100% transparently, but that’s just not an option with WoW, due to its obnoxious packet structures. I mean, it’s possible, but the overhead from doing it automatically is just not worth the syntactic sugar. Plus, I’m after short code here, and writing support for such complex packet structures (and then actually writing the structures themselves) would not be worth it. At all.
So, what I want is something that feels like I’m actually writing a structure (or a class if you will), while still supporting conditions, switches, loops, so on. And so I present:
[WorldParser(WorldServerOpCodes.SMSG_CREATURE_QUERY_RESPONSE)]
public static void HandleCreatureQueryResponse(WorldServerOpCodes opCode, PacketProcessor packet)
{
var entry = packet.UInt32("Entry ID");
packet.If("(Entry ID & 0x80000000) == 0", (entry & 0x80000000) == 0, () =>
{
packet.For(0, 4, i =>
packet.String("Name"));
packet.String("Sub Name");
packet.String("Icon Name");
packet.Enum<CreatureTypeFlags>("Type Flags");
packet.Enum<CreatureTypes>("Creature Type");
packet.Enum<CreatureFamily>("Creature Family");
packet.Enum<CreatureRank>("Creature Rank");
packet.For(0, 2, i =>
packet.UInt32("Kill Credit"));
packet.For(0, 4, i =>
packet.UInt32("Display ID"));
packet.Single("Health Modifier");
packet.Single("Mana Modifier");
packet.Boolean("Racial Leader");
packet.For(0, 6, i =>
packet.UInt32("Quest Item ID"));
packet.UInt32("Movement ID");
});
}
This here packet handler would normally have taken 3 times the lines without the PacketProcessor class I’m using (I won’t post the code, since it’s part of a private project – but writing it is not that hard anyway).
There are several nice things about it:
- Every data type is read with a very short method call, in fact, shorter than a line you would type out in a struct.
- If a name is not given to the data type methods, it assumes the purpose of the variable is unknown, and thus uses and increments an internal unknown counter for that specific data type.
- Conditions, loops, and switches are done through lambdas, meaning you’ll almost never have to use any C# keywords. The PacketProcessor class also has IfMore (executes if the packet has more data) and IfDirection (executes if the packet’s direction matches the given direction), which do not output anything, as you don’t need to see those conditions in the dump.
- It reads enums 100% correctly. Yes, flags too. It uses the underlying type of the enum to do so. It was brought to my attention by Phazerz, though, that some enums are written as different types in various packets, so I’ll have to come up with a way to handle that. No biggie, though.
- I get a nice, universal, and automatically formatted output.
And now for the output of the above code:
ServerToClient: SMSG_CREATURE_QUERY_RESPONSE (0x0061) Length: 117 Time: 15-07-2010 14:26:51
Entry ID: 352
If ((Entry & 0x80000000) == 0) => True
{
For (0 -> 4)
{
0 =>
[
Name: "Dungar Longdrink"
]
1 =>
[
Name: ""
]
2 =>
[
Name: ""
]
3 =>
[
Name: ""
]
}
Sub Name: "Gryphon Master"
Icon Name: ""
Type Flags: None
Creature Type: Humanoid
Creature Family: None
Creature Rank: Elite
For (0 -> 2)
{
0 =>
[
Kill Credit: 0
]
1 =>
[
Kill Credit: 0
]
}
For (0 -> 4)
{
0 =>
[
Display ID: 5128
]
1 =>
[
Display ID: 0
]
2 =>
[
Display ID: 0
]
3 =>
[
Display ID: 0
]
}
Health Modifier: 3
Mana Modifier: 3
Racial Leader: False
For (0 -> 6)
{
0 =>
[
Quest Item ID: 0
]
1 =>
[
Quest Item ID: 0
]
2 =>
[
Quest Item ID: 0
]
3 =>
[
Quest Item ID: 0
]
4 =>
[
Quest Item ID: 0
]
5 =>
[
Quest Item ID: 0
]
}
Movement ID: 0
}
As seen, indentation is nicely handled by PacketProcessor, due to the If/For/Switch methods, which all internally take care of indenting and dedenting when necessary.
Some code demonstrating switching (I want to improve this a bit eventually):
[WorldParser(WorldServerOpCodes.SMSG_AUTH_RESPONSE)]
public static void HandleAuthResponse(WorldServerOpCodes opCode, PacketProcessor packet)
{
var authCode = packet.Enum<ResponseCodes>("Response Code");
packet.Switch("Code", authCode, new Switch
{
{ ResponseCodes.AUTH_OK, () =>
ReadAuthResponse(packet)
},
{ ResponseCodes.AUTH_WAIT_QUEUE, () =>
{
if (packet.Length <= 6)
{
ReadQueuePosition(packet);
return;
}
ReadAuthResponse(packet);
ReadQueuePosition(packet);
}}
});
}
And the output:
ServerToClient: SMSG_AUTH_RESPONSE (0x01EE) Length: 11 Time: 15-07-2010 14:26:38
Response Code: AUTH_OK
Switch (Code): AUTH_OK
{
Case AUTH_OK:
[
Remaining Game Time: 8541
Billing Flags: Trial1, Restricted
Rested Game Time: 0
Client Type: WorldOfWarcraft
]
Case AUTH_WAIT_QUEUE:
[
<Unmatched>
]
}
And some code demonstrating unknown value counting:
[WorldParser(WorldServerOpCodes.SMSG_ACCOUNT_DATA_TIMES)]
public static void HandleAccountDataTimes(WorldServerOpCodes opCode, PacketProcessor packet)
{
packet.UInt32();
packet.Byte();
var mask = packet.UInt32("Time Mask");
packet.For(0, 8, i =>
packet.If("(Time Mask & (1 << i)) != 0", (mask & (1 << (int)i)) != 0, () =>
packet.Int32()));
}
Output:
ServerToClient: SMSG_ACCOUNT_DATA_TIMES (0x0209) Length: 21 Time: 15-07-2010 14:26:39
Unknown UInt32 0: 1279196803
Unknown Byte 0: 1
Time Mask: 21
For (0 -> 8)
{
0 =>
[
If ((Time Mask & (1 << i)) != 0) => True
{
Unknown Int32 1: 1278872250
}
]
1 =>
[
If ((Time Mask & (1 << i)) != 0) => False
{
<Unmatched>
}
]
2 =>
[
If ((Time Mask & (1 << i)) != 0) => True
{
Unknown Int32 1: 0
}
]
3 =>
[
If ((Time Mask & (1 << i)) != 0) => False
{
<Unmatched>
}
]
4 =>
[
If ((Time Mask & (1 << i)) != 0) => True
{
Unknown Int32 1: 0
}
]
5 =>
[
If ((Time Mask & (1 << i)) != 0) => False
{
<Unmatched>
}
]
6 =>
[
If ((Time Mask & (1 << i)) != 0) => False
{
<Unmatched>
}
]
7 =>
[
If ((Time Mask & (1 << i)) != 0) => False
{
<Unmatched>
}
]
}
Take note of how the unknown counter is kept intact across loops (admittedly the backing code for that is a bit hacky, but hey – it works).
Overall, I’m pretty happy with this. I achieved what I wanted; to be able to write packet parsing in a functional fashion, while preserving the feeling of writing a structure. Only the switching came out a bit bad. As mentioned, I’ll have to look at that again; it most likely could be nicer.