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.
0x009A is a client-to-server message of four fixed fields, 81 bits in all, not yet checked for what it means, so it carries neutral names:
[WireMessage(GameMessageOpcode.Client009A, Direction.ClientToServer)]
public sealed partial class Client009A
{
[UInt(32)] public uint Unknown0 { get; private set; }
[UInt(16)] public ushort Unknown1 { get; private set; }
[UInt(32)] public uint Unknown2 { get; private set; }
[UInt(1)] public bool Unknown3 { 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 |
[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 |
-
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.
| 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.
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.
Note
This table is kept by hand until the diagnostics get a generated reference page.
The generator refuses to emit code for a type that breaks a rule, and the build fails. A type with any error gets no generated code, and neither does a type that nests or derives from it.
| Id | Severity | When |
|---|---|---|
NTC001 |
error | A wire member has no encoding, isn't a [WireStruct] type and isn't [NotOnWire]
|
NTC002 |
error | A width the member's type can't hold, or outside its range: 1 to 64 bits, a bool only [UInt(1)], a count or a blob length 1 to 32 |
NTC003 |
warning, an error in our build unless suppressed with its reason | An enum member needs more bits than its field declares. The member stays, so a sentinel value that's never sent can be suppressed; writing it throws |
NTC004 |
error |
CountFrom or a union's selector names a member that's missing, declared later, or of the wrong kind, or two unions share one selector |
NTC005 |
error | Two messages share an opcode and direction, or two arms share a union value |
NTC006 |
warning, an error in our build |
[UInt(n)] on a signed integer narrower than its type, so a negative value can't be written. Enums are exempt; NTC003 covers them |
NTC007 |
error | Wire members spread across the parts of a partial class, so their order is undefined |
NTC008 |
error | The type, or a type containing it, isn't partial
|
NTC009 |
error | An attribute misused: both or neither of CountBits and CountFrom, a float attribute on an integer, a union type that isn't a wire interface or abstract [WireStruct], an abstract [WireStruct] held as a plain member, a member with no setter, a [Count] no array names |
NTC010 |
error | A union type with no arm in the assembly |
NTC011 |
error | An aligned blob outside a [WireMessage], the only kind of type that starts on a byte boundary |
NTC012 |
error | The type itself is misdeclared: a record, generic or nested in a generic type, both [WireStruct] and [WireMessage], an abstract message, no parameterless constructor, a bad [UnionCase] value, opcode or direction, or a union arm holding an aligned blob |
NTC013 |
error | A [UnionCase] value its selector can't carry, so that arm could never be read back |
Each diagnostic 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
- Server core design
- Host
- Gateway
- Owners
- Persistence
- Command layer
- Harness
- Map scheduler design
- Timer wheel design
The protocol
Working on it