Skip to content

Codec declarations

rebaserHEAD edited this page Oct 24, 2026 · 10 revisions

reference, committed verified 2026-10-24 dev f2ad749 run 2026-10-24

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

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.

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. 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

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, and a write of a list longer than its Capacity. 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).

Verified members

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.

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;
  • its send check, FindUnverifiedValue, which names the first set member that isn't verified, and IsDefault.

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

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.

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