Well, I guess we have different approaches in mind when working with packets. I probably use something similar to your option A, but I don't parse packets into structs or classes. Imho, defining all this message structs just adds a lot of unneeded boilerplate and sometimes complexity. I work directly on a byte array (and soon pooled Memory/Span<byte>) with some convenience extension methods. So basically, I extract the data I need into local variables and pass them to my game action functions. When sending packets I also use extension methods to set the bytes.
Imagine you have to initialize a class of hundreds of variables. Using this method you will write a function to initialize this object from a memory stream once and pray for the gods you don't need ever more to touch this function.
Packets coming from clients are usually very small (under 5 fields), so this is not an issue here. For outgoing packets it's similar - most are relatively small. The big ones are a bit of an issue, but it's a trade-off I made here. The original code doesn't make things much better in this cases
If for some reason you need to modify this code you will spend a LOT of time trying to check if the memory offsets are right with your new modifications.
Sure, but the same applies for structs and marshalling. You'll have to correctly define them as well and hope marshalling works as expected. The original game server/client also uses different byte order (little/big-endian) for same types of fields, sometimes structs are packed, sometimes not - big PITA. I wonder how marshalling behaves when it runs under different CPU architectures (possible with .net core at linux), e.g. ARM.
I don't know which option you are using to do this in your project but I strongly disagree when you say that "marshaling is not faster and adds some unneeded complexity to the code".
I wrote a small test which compares my way of reading packets to using struct marshalling. My way is about 100 times faster in this test (if I leave the validation away it's more like 800 times). I know this is more like a theoretical test and that's no bottleneck in the real world, but still. You can find the test code here:
You must be registered to see links
Well, when you say that there are complex mu packet structures that can not be represented using Marshal I have no clue what you are talking about. The marshal class is made to fit every possible data type in memory. This is the method used in every pinvoke (platform invoke) code. Could you show an example of such a complex structure that can not be represented in a classe using Marshal?
I encountered problems when I tried to use arrays of structs. Some packets also have some dynamically sized blocks, e.g. the effects in player meet packets. Simplified example:
[Header]
[Player Count], for each:
[[Fixed block of Player Data] [Effects counter] [Dynamic block of effects, one byte each]]
I don't know if I were too stupid some years ago, but I didn't manage to use Marshal in a simple way. My first problem was, that only the first player got parsed/serialized. Even if I could get that to work, the next problem was, that I couldn't define a variable length of the array of effects. Maybe my approach was flawed and it required more manual work, like multiple marshal calls for one packet.
The problem is more about aspects of code quality (like maintainability).
I hope I got your point now... So, you're telling me that maintaining C++ code is easier than a C# and code quality in this specific aspect will be better in C++? Well, you can write horrible code in any language - you saw this IGCN sources, right?
My experience so far is, that packet handling is one of the easier and smaller aspects during development of a server. The other benefits of C# are way more valuable for me.
And my current approach might not be the final one... I'm thinking about defining packet structures as data which feeds some dynamic code generator - would be just as fast as writing my manual code but highly flexible.