Repository navigation
Message audit design
How we establish exactly how to talk to the 16042 client, for every message it registers, before any feature code depends on one. It runs alongside the core skeleton on the Roadmap, milestone one's messages first, and builds on the declarations and facts check described on Codec design. What the server does with each message is out of scope: that starts with the core skeleton.
We assume every message, opcode and field we inherited is wrong until the client says otherwise, and check every one of them before anything uses it, milestone one's first. Each declaration records how far it has been verified, a test holds it there, and nothing below "fields verified" may be used. Until then a message carries a neutral name, and the results are published here as a protocol reference generated from the declarations.
A misread is silent. When the server sends a message the client reads differently, the client takes the wrong bits or the wrong meaning and carries on, and the symptom shows up far from the cause. The only defence is to know each message before anything depends on it.
The old code is wrong at every level. Measured against the client, it has a mistyped opcode (0x776E for 0x076E), one that isn't a client opcode at all (0x0511), a swapped pair (0x0851 and 0x0852), three messages declared in the wrong direction, 116 disagreements on the empty path alone, two reordered field pairs, nine command fields typed as integers where the client uses floats, and a vector named Rotation that's a start position. The layout check catches only some of that, and none of the meaning.
A name is a claim. The old code called 0x0111 ServerItemAdd, and a name like that tells everyone after what the message is for, whether or not anyone checked. Every name we inherited was a guess of that kind, so a name has to be earned the same way a width is.
Each message climbs five rungs, and reaches one only with every rung below it.
| Rung | Name | Established when we know |
|---|---|---|
| R0 | Identity | The opcode exists, and which way it travels |
| R1 | Layout | Every field, array element, union arm and string, in order, at its width |
| R2 | Purpose | What the message is for, and so its name |
| R3 | Fields | Each field's meaning: its unit, range or set of values, the table an id indexes, integer or float, signed or not |
| R4 | Conversation | When it may be sent, what has to come before it, and what the client does or sends in reply |
R0 and R1 are checked mechanically, for every message at once. R2 and R3 are established for every message, milestone one's first and each one before the core uses it, and each one also records the message's own side of the conversation: what state the client checks, and what it sends back. R4 needs the whole flow, so it's established flow by flow, milestone one's first; a message reaches it when every flow it takes part in is done.
Only what's established reaches a declaration. A fact reaches a declaration only when two independent readings of the client agree on it, and a claim that isn't settled holds its message below the rung it would complete. A message whose values the client names only in its interface stays at Purpose until those names are checked too.
A verified declaration carries the client's own names for the message, its nested types and its fields, and its rung in a [Verified] attribute beside [WireMessage]:
| Where | Says |
|---|---|
The type's <summary> |
What the message is for |
Each member's <summary> |
The field's meaning, unit and range |
<remarks> |
The conversation, where it's known |
[Verified(Rung.Fields)] |
The highest rung reached: Purpose, Fields or Conversation. No attribute means R1 at most, since R0 and R1 come from the facts check |
An enum-typed field gets its enum back only once its set of values is confirmed, and the enum's member names are checked against the values the client tells apart. A value may be named with one plain English word the client also uses, such as offline or low, but never with a compound identifier or the interface's own text; otherwise values are described in our own words.
RegistryVerificationTests, beside the facts check and in the same shape, built at fadf745. Each run reports how far the audit has got; at ee2b70d, with pregame, world entry and the tagged wrapper merged, it reads "1119 declared messages: 48 read to fields; 1071 unverified."
-
Every declared message is at
Fieldsor above, or listed in a pinnedUnverifiedset. A listed message that reachesFieldsfails as stale, so the list only ever shrinks. It starts as every declaration and ends empty. -
A message at
Purposeor above has a name of its own and a<summary>. Its neutralClient,ServerorMessagenumber gives way to a name for what it does. AtFieldsor above, every wire member of every layout it reaches has a summary, whatever the member's accessibility and whichever model project holds the layout, and a member still namedUnknown<n>begins its summary with "The client gives this value no meaning." One sentence covers both directions: a value the client ignores when it reads, and one it writes with no meaning. -
Once the core skeleton adds dispatch, nothing below
Fieldscan be used: registering a handler for one, or sending one, fails a test over the dispatch table. Until then the shrinking list is the gate.
The build writes XML documentation for Network, Network.Auth and Network.World, and the test reads it. CS1591 is off for the wire declarations, because the test enforces documentation by rung instead, and a message nobody has checked shouldn't be made to describe itself.
Everything not yet verified gets a name built from its position, so nothing in the code claims a meaning nobody has checked, and anyone can check a name without a lookup table.
| Thing | Neutral name | Example |
|---|---|---|
| Opcode, one way |
Client or Server and four hex digits |
Client0094, Server01C2
|
| Opcode, both ways |
Message and four hex digits |
Message0635 |
| Message type | Its opcode's name | Server01C2 |
| Nested struct | Its first holder (the lowest opcode), Part, and its order of first appearance |
Server01C2Part0 |
| Union interface | Its holder, Union, and its order |
Client01C3Union0 |
| Union arm | Its union, Arm, and the selector value in hex |
Client01C3Union0Arm05 |
| Field |
Unknown and its position in the declaration, from 0 |
Unknown0, Unknown1
|
| Enum-typed field | The smallest unsigned integer that holds its width | [UInt(5)] public byte Unknown3 |
| Pad | A field like any other, since a pad claims the client ignores those bits | [UInt(32)] public uint Unknown4 |
The old code's comments go too. A comment on a model or a member is a claim of meaning just as a name is, so a declaration stays bare until it's verified.
What stays:
-
Encodings:
[Float32],[PackedFloat],[SInt(n)], the string kinds, arrays, unions and blobs. They change how our code reads the bits, not which bits travel, and each one is confirmed or corrected at R3. -
The codec's value types:
NetworkGuid,Vector2,Vector3andQuaternion, which are shapes in the codec, not models. -
The feature folders under
Message/Model/, which group and claim nothing. A message moves folder when it turns out to belong elsewhere. -
Game.Static's enums, which serve the game side and the tables. The wire stops using them until a field's values are confirmed.
The opcode list is exactly the client's: 1,119 wire opcodes, the 181 the old list lacked among them under neutral names. Members the client doesn't register, State and State2, are gone with their declarations, and the 13 registrations at 0x10000 and up aren't wire ids and stay out.
Today's facts table holds each message's widths on the empty path: every count at zero, every union at its first arm. R1 needs the whole layout.
-
The facts table grows into a tree: fields, arrays with their element layout, and unions with an arm for each selector value. It's still our own numbers, and still holds for both the 64-bit and the 32-bit client. Built at
bd2ec2c: every side carries its tree except three client envelopes, which keep their widths and are checked on the empty path. -
The generator's layout grows into the same tree, emitted for every declared message. Built at
9f13a55, in the notation on Codec design, and held to the empty path the facts check already proves. -
The facts check compares trees, field by field, element by element and arm by arm. A declared arm the client lacks, or a client arm the declaration lacks, is a disagreement, and every disagreement is pinned by opcode, side and path with its reason, as today. Built at
d3aa2dd, with the rules on Codec design. -
Every registration gets declared. Each one with no declaration gets a neutral one generated from its tree, so the audit starts with every message declared and checked at R1. Built at
7072554: 192 new declarations, all agreeing with the client, filed in the world model folders by opcode range.
What R1 can't see: fields of equal width swapped, a float against an integer, signed against unsigned, and meaning. Those are R3.
Messages are checked one feature area at a time (chat, guilds, housing and so on), because a feature's messages share code, tables and behaviour, and what's learned about one informs the rest. Milestone one's go first: pregame and realm, character select, world entry, entity commands and movement, and chat. The rest follow by size, largest first.
Areas come from the client. A message belongs to the client feature it serves, and the declarations will sit in one folder per area. Neither the opcode numbers nor the old code's folders draw the lines: the client didn't allocate opcodes in feature blocks, and only 7 of the old code's 40 folders span a contiguous run of opcodes. A message whose feature isn't settled yet keeps a provisional folder until its area's check settles it. The area names are ours, plain feature words, and a name groups messages without claiming anything about what they do.
Each area is reviewed on its own before its page of the protocol reference is generated again.
A tool in the repository, Tools/NexusTomorrow.ProtocolReference, loads the built Network, Network.Auth and Network.World assemblies, walks each one's generated WireMessages registry, reads the XML documentation the build writes, and generates this wiki's message pages:
-
Messages: an index of every message by opcode. -
Messages-<area>: one reference page per feature area, with a section per message giving its opcode, direction, rung and purpose, a table of its fields with width, encoding and meaning, union arms and array elements nested under their holders, and the conversation in prose where it's known.
A message below Purpose shows its opcode, direction and layout only, marked unverified, and an area with no message at Purpose says its name is only a grouping. The pages are generated, so they follow Documentation standards exactly and are never edited by hand. The tool is in at 58cb3ba, and its pages are generated again at each commit that moves a declaration they show.
-
Delete the old code. The proof against the old models paired declarations with them by name, so a rename would break it, and agreement with the old code is exactly what the audit stops trusting. Done at
c5b0774. -
The neutral rename: the names, then each file renamed for its type, then the enums the wire no longer uses. Every layout is bit for bit what it was. Done at
86b7f28, and the[Verified]attribute and its test followed atfadf745. -
Bring
Network.WorldandNetwork.Authunder the analysers. The rename had already cleared every naming finding, and the rest were 388 members with no initializer and four public fields, so every declaration the audit touches from here lands under the analysers and nullable reference types. Every layout is bit for bit what it was. Done at9c93c58. -
R1 for everything: the full-layout facts, the layout tree in the generator, and the tree check. The facts check's remaining pins fold into it. The tree notation and the generator's trees are in at
9f13a55, the client's trees in the facts table atbd2ec2c, the tree check atd3aa2dd, and every registration declared at7072554. Done at64dba89, after a review of the whole step. -
R2 and R3 for every message, area by area, milestone one's areas first, and R4 flow by flow. Under way: pregame (login, realm select and character select) is merged, at
cd601a0, at25218fdonce the values the client's interface names were checked, at585b32awith the character list, atb9b7eeawith character creation, and atf1e5c15withLoginRequest(0x0592): 41 of its 42 messages are atFields.0x03EFstays unverified, because the client gives it no purpose. World entry followed at7c68ecc, with six messages atFields:PlayerUnit(0x019B),WorldSettings(0x00F1),MovementControl(0x0636),PathState(0x06BC),WorldReady(0x00F2) andWorldEntryComplete(0x0061). The player's own state (0x025E) stays belowFields, since six of its fields have a meaning from only one reading and three aren't read yet. The unit create,UnitCreate(0x0262), reachedPurposeatc136cc7: what it's for is settled, and its fields aren't yet.TaggedMessage(0x038C), which carries a message under its opcode's tag, reachedFieldsatee2b70d; what each tag value stands for isn't established yet. What these messages do, and the conversation they make, is on Client protocol. -
Close: a review of the whole audit, and the protocol reference generated from the result. The codec's third stage closed after its own review at
c6b56e9, and step 3 didn't wait for the audit to close: the core skeleton is built on the messages already verified, as decision 2 below now reads.
- Every inherited opcode and model is assumed wrong until the client says otherwise.
- Every message is checked before anything uses it, for its purpose and its fields. Milestone one's messages go first, the core skeleton is built on them as they're verified, and the remaining areas are checked alongside the build. This changed on 2026-10-16 from "every message before any more feature work", so the project can stop depending on the old server sooner; the gate in decision 3 is what keeps the core off anything unchecked.
- The verification level lives in each declaration, and a test enforces it.
- Everything unverified gets a neutral name: messages, nested structs and fields. Enum-typed fields become plain integers until their values are confirmed.
-
The codec's third stage paused for the audit, and closed after its review at
c6b56e9. - The results become a developer reference on this wiki: Messages.
- Nothing in the design. The work is the open part, and the
Unverifiedcount shows how much is left at any point.
| Idea | What killed it |
|---|---|
| Checking only the messages the core uses, when it first uses them, with the mechanical layout check for the rest | Every message is still checked, not only the ones the core uses, and no feature is built on a message nobody has checked. Since 2026-10-16 milestone one's go first and the rest follow alongside the build |
| Recording each message's verification level in a separate list, held by review | The level lives in each declaration with a test, so the build holds it, and anyone reading the code can see what's trusted |
| Keeping the old code's names as labels until a message is checked | Neutral names instead, so nothing public asserts a meaning nobody has checked, and no check starts out anchored on the old code's guess |
| Renaming only the messages, or only the messages and their fields | Everything goes neutral, nested structs and enum-typed fields included, for the same reason |
| Finishing the codec's third stage first, then auditing | Once the proof against the old code was deleted, the rest of the third stage would have rested on the empty-path check alone |
| Claim | Basis |
|---|---|
| 1,132 registrations, the same in the 64-bit and the 32-bit client: 366 client to server, 718 server to client, 35 both ways, 13 that aren't wire ids | Established from the client |
| The full layout of 1,030 of the 1,119 declared messages agrees with the client: every field, array element, union arm and string. 104 disagreements are pinned, to be settled area by area | The tree check, in the CI run in the header badge |
| The client's full layouts hold for both the 64-bit and the 32-bit client on 1,145 of 1,154 sides | Established from the client. Of the 9 others, 6 are envelopes, 3 differ in two selector values far out of range, and 0x098B also in one arm, left for its area |
| Names, field meanings, integer or float, value sets | Not checked yet. The old code's guesses are out of the code since the rename at 86b7f28. R2 and R3 |
| Every message is held to its rung |
RegistryVerificationTests in the CI run in the header badge: 1,119 declared, 48 read to fields, 1,071 pinned as unverified |
| The reference shows each message's full layout as declared | The protocol reference's walk, in the CI run in the header badge: all 1,119 declared messages walked into page rows, none differing from its generated layout tree |
The purposes of the 49 named messages, and the field meanings of the 48 at Fields |
Two independent readings of the client agreeing on every stated fact, and a review of the area. Their order in the game's own flow is R4, still to be seen in game |
| Order and preconditions for milestone one's world entry and its other three flows | Established from the client; not yet seen in game, which happens once a server speaks them |
| The old code's errors listed under Why | Each found against the client; the opcode errors and swapped pairs are already corrected |
Each new test is shown to fail the way it should before it's trusted: a run with one rung lowered, one summary removed, and one arm dropped from a facts tree.
- Whether the full-layout facts reach every path, such as a union selector wider than 8 bits or an array gated by a flag. A path found later is added to them, and to the tree check.
| Piece | Path |
|---|---|
| The opcode list | Source/NexusTomorrow.Network/Message/GameMessageOpcode.cs |
| The declarations |
Source/NexusTomorrow.Network/Message/Model/, Source/NexusTomorrow.Network.Auth/, Source/NexusTomorrow.Network.World/
|
| The client facts table | Source/NexusTomorrow.Network.Codec/Facts/wire-facts-16042.jsonl |
| The facts check | Tests/NexusTomorrow.Network.Codec.Tests/Facts/ |
[Verified] and Rung
|
Source/NexusTomorrow.Network/Message/VerifiedAttribute.cs |
| The verification check, its pinned list and its rules | Tests/NexusTomorrow.Network.Codec.Tests/Verification/ |
| The generator's layouts | Source/NexusTomorrow.Network.Codec.Generator/ |
| The protocol reference generator and its walk |
Tools/NexusTomorrow.ProtocolReference/, Tests/NexusTomorrow.ProtocolReference.Tests/
|
Guides
Explanation
Reference
Design
- Server core design
- Roadmap
- Codec design
- Message audit design
- Host design
- Gateway design
- Owners design
- Persistence design
- Command layer design
- Harness design
About this wiki