Skip to content

Codec declarations

rebaserHEAD edited this page Oct 18, 2026 · 1 revision

reference, committed verified 2026-10-18 bootstrap c3409ba run 2026-10-18

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.

Declaring a message

A declaration

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.

The attributes

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

How they combine

  • 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 (0x0260 carries a list of 0x025F bodies), 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.

Irregular shapes

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

What the generator writes

Per declaration

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.

Diagnostics

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.

Known edges

  • 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 Write and requires zero allocated bytes.

Where the code lives

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/

Clone this wiki locally