Repository navigation
Codec declarations
The declaration language: the attributes a message is declared in, how they combine, how the irregular shapes are declared, and what the source generator writes or refuses. How the pieces hook together is on Codec.
0x0360 is a client-to-server message of two fixed fields, 16 bits in all, not yet checked for what it means, so it carries neutral names:
[WireMessage(GameMessageOpcode.Client0360, Direction.ClientToServer)]
public sealed partial class Client0360
{
[UInt(15)] public uint Unknown0 { get; private set; }
[UInt(1)] public bool Unknown1 { get; private set; }
}- Wire order is declaration order.
- Width and signedness are always explicit, never inferred from the C# type.
-
A checked message takes the client's names, a summary on the type and each field, and a
[Verified]rung, as Message audit design describes.
| Attribute | Goes on | Means on the wire |
|---|---|---|
[WireMessage(opcode, direction)] |
A partial class | A message root. Direction is client to server, server to client, or both |
[WireStruct] |
A partial class | A nested layout with no opcode, inlined where it's used |
[UInt(n)], [SInt(n)]
|
Integers, bool, enums |
Zero-extended or sign-extended, 1 to 64 bits; [SInt(n)] is two's complement in n bits |
[Float32], [Float64], [PackedFloat]
|
float, double, 2D and 3D vectors, quaternions |
IEEE bits per component, or the client's 16-bit packed float; a quaternion is four floats in X, Y, Z, W order |
[WideString], [NarrowString], [TerminatedWideString]
|
string |
The client's three string encodings |
[Array(CountBits = n)], [Array(CountFrom = ...)]
|
Lists and arrays | A count of n bits just before the elements, or elements counted by an earlier field. Capacity = n adds the most elements the client holds, which a write may not pass |
[Count] |
An integer field | A count shared by every array that names it |
[FixedArray(n)] |
Arrays | Exactly n elements, no count on the wire |
[Union(Selector = ...)], [UnionCase(value)]
|
An interface property, and each arm | A payload picked by an earlier field |
[Blob(...)] |
Bytes | Optional alignment, a length, raw bytes |
[Pad(n)] |
A Pad field |
Zeros on write, skipped on read |
[NotOnWire] |
Any member | Left out of the layout |
[Verified(rung)] |
A message | Nothing on the wire: how far the message has been checked against the client |
[LayoutConfirmed] |
A message | Nothing on the wire: the client takes the message's layout as declared, every member's width and order, whatever rung its meaning has reached; a test holds it to the client facts with no pinned disagreement |
[Dormant] |
A message | Nothing on the wire: the 16042 client never acts on the message, so the server never sends or handles it. The message keeps its rung, and below Fields it counts apart from the messages still to be read |
[VerifiedMember] |
A member | Nothing on the wire: its meaning is established twice and written in its summary, so the server may set it |
[WireSecret] |
A member | Nothing on the wire: the member carries a secret, a session key, a login token or a password, so a tool that records messages leaves out the body of any message that has one |
-
On a collection, the scalar attribute describes the element:
[Array(CountBits = 3), UInt(18)] List<uint>. -
A member whose type is a
[WireStruct]needs no attribute. -
A layout may derive from another
[WireStruct], and a message from another message; either way, the base's fields come first. -
A message can sit inside another layout, as a member or a list element, since the client nests whole message bodies in lists (
0x0260carries a list of0x025Fbodies), unless it holds an aligned blob, which needs the byte boundary only a message's start guarantees. -
A union's selector comes from its arm. The writer takes the selector value from the arm's
[UnionCase]and sets the selector to match, so the two can't disagree. A[UInt(n)]selector carries its cases' unsigned bits even on a signed type; an[SInt(n)]selector compares its cases as signed numbers. One selector serves exactly one union. -
A value or count that doesn't fit its field throws a
CodecException, and so does a read past the end of a message's body or an unknown selector, and a write of a list longer than itsCapacity. Reading isn't held to the capacity, since the frame already bounds what the client sends. A capacity larger than its count's bits can carry is a build error,NTC014(Codec diagnostics).
A message at Purpose may be sent once every member the server sets is marked [VerifiedMember]. A test fails a mark on a member with no summary. The generator judges a message member by member:
| Member | Passes when |
|---|---|
| Unmarked | It holds its whole value at the default: zero, false, an empty string or list, a struct or union at its defaults |
| Marked, holding a nested layout | Every member of that layout passes in turn, element by element in an array |
| Marked, a value | Always |
| A count or a union's selector | Judged through the array or union it describes, since the writer derives it |
| A marked union | Its chosen arm passes by that arm's own marks; while its selector is unmarked, only its first arm may be chosen |
| An unmarked union | It holds its first arm at its defaults |
The send gate runs the check before a message is serialized, and names the first member that fails.
| Shape | Declared as |
|---|---|
| A union family | A wire interface, each arm with its [UnionCase]
|
| A family whose arms write the shared fields after their own | The shared fields declared in each arm, after its own, in wire order; where the selector is written per element, the arms sit in a holder that keeps it |
| A byte-aligned envelope | A blob whose 32-bit length counts itself; the packed one aligns after its first 5 bits |
| The network guid | A 128-bit struct of four fields |
| A byte buffer | A byte array, fixed where its size is fixed, counted where it isn't |
| Records written column by column | Parallel lists sharing one count |
| A fixed-count list | An array, padded to its count by its initializer |
| A list with no count before it | The count the client reads there |
For each declared type, the generator writes:
- a reader and a writer,
ref structs over spans, least significant bit first, as the client reads and writes; - its size in bits, and a constant for a fixed layout;
- the widths on its empty path, and its full layout tree as
WireLayout.TreeText(Codec layout tree); -
[WireLayoutData], so a struct declared in another assembly nests into its holder's tree; - its send check,
FindUnverifiedValue, which names the first set member that isn't verified, andIsDefault.
One registry per assembly, WireMessages, holds every message's layout and a factory per direction keyed by opcode. The write path allocates nothing: the size picks a pooled buffer, and the writer fills a span. An aligned blob's alignment counts as a width on the empty path, since the writer emits it.
The generator refuses to emit code for a type that breaks a rule, and the build fails. Every diagnostic, how severe it is and when it fires, is on Codec diagnostics. Each sits on its declaration's syntax tree, so .editorconfig severities and #pragma reach it exactly as they reach the compiler's own. No project suppresses either warning.
- A union with no arm for selector 0 marks its layout incomplete, and the facts check reports that opcode.
- The build's banned-API check can't see generated code, so a test measures a generated
Writeand requires zero allocated bytes.
| Piece | Path |
|---|---|
| The wire attributes | Source/NexusTomorrow.Network.Codec/Attributes/ |
[WireMessage] and [Verified]
|
Source/NexusTomorrow.Network/Message/ |
CodecException |
Source/NexusTomorrow.Network.Codec/CodecException.cs |
| The source generator and its diagnostics | Source/NexusTomorrow.Network.Codec.Generator/ |
| The generator's tests | Tests/NexusTomorrow.Network.Codec.Generator.Tests/ |
Start here
How the server works
The protocol
Working on it