Skip to content

fix(networking): 0x2E group-box class caps are Warrior, Wizard, Rogue, Priest, Monk - #22

Merged
baughj merged 1 commit into
mainfrom
fix/htoo-64-groupbox-class-cap-order
Aug 2, 2026
Merged

fix(networking): 0x2E group-box class caps are Warrior, Wizard, Rogue, Priest, Monk#22
baughj merged 1 commit into
mainfrom
fix/htoo-64-groupbox-class-cap-order

Conversation

@baughj

@baughj baughj commented Aug 2, 2026

Copy link
Copy Markdown
Member

Fixes HTOO-64.

GroupRequestPacket (C→S 0x2E, stage 4 — the recruiting group box) read and wrote the five class-cap bytes as Warrior, Wizard, Monk, Priest, Rogue. The retail client's order is Warrior, Wizard, Rogue, Priest, Monk. Positions 3 and 5 were transposed, in both directions.

Why no test caught it

Reader and writer were swapped consistently, so DALib round-tripped with itself perfectly. The two byte-pinning tests did assert wire order — but they asserted the wrong order, and their comments carried the false proof inline:

// Distinct caps Warrior 1 / Wizard 2 / Monk 3 / Priest 4 / Rogue 5 appear on the wire as
// 01 02 03 04 05, proving the order W, Wiz, Monk, Priest, Rogue.

That experiment cannot answer the question it was used to answer. Filling the dialog's five rows top-to-bottom with 1,2,3,4,5 produces 01 02 03 04 05 whichever class each row belongs to. It establishes that the wire follows UI row order and is silent on class identity. The names were assumed — and published as a correction to Chaos.Networking, which had it right all along.

Please don't pin this with a round-trip test. A round-trip agrees with itself under either ordering and passed throughout. The assertion has to be against absolute byte positions.

Impact

The caps gate who may join an advertised group, so this is behavioural, not cosmetic:

  • DALib as server misreads a real client's recruit box — a rogue cap lands in monk and vice versa.
  • DALib as client (Brigid) publishes a box whose caps a real server misreads.

Grounding

Rung 1, from Darkages.exe (md5 3244dc0e68cd26f4fb1626da3673fda8) plus that install's shipped UI assets. The executable alone cannot answer this — there are no class-name strings anywhere in the group module.

  • net_send_group_recruit_start @ 0x00513B30 — a bare five-iteration loop copying contiguous model bytes to the wire. Encodes no class ordering at all.

  • ui_group_ad_dialog_read_model @ 0x005120A0 — fills wire byte k from dialog control 2k+7 (verified against raw disassembly: 0x0051211b: LEA EDX,[ECX + ECX*0x1 + 0x7]). A monotonic stride-2 walk over the per-row control pairs, so wire byte k is screen row k, top to bottom.

  • Class identity comes from the artwork: setoa/_ngcdlg1.txt places N_CLASS0 at y=206 and N_CLASS1 at y=233 (Δy=+27). Overlaying those rects on _ngcdlg1.spf reads, top to bottom:

    control y label
    N_CLASS0_O/W 206 Warrior
    N_CLASS1_O 233 Wizard
    (+27) 260 Rogue
    (+27) 287 Priest
    (+27) 314 Monk

    Note the client's own control naming independently binds CLASS0→Warrior and CLASS1→Wizard, without reference to the disassembly.

Corroborated at rung 2 by ewrogers/darkages-741-re @ f12a147 (docs/network/client/046-0x2e-group.md), and by Hybrasyl's GroupRecruit, both already correct.

The file already contradicted itself

Only three places were wrong. The layout comment and the property declarations were correct and are unchanged:

  • layout comment: [u8 MaxWarrior][u8 MaxWizard][u8 MaxRogue][u8 MaxPriest][u8 MaxMonk]was already right
  • property order (MaxWarriorMaxMonk) — was already right
  • prose sentence, WriteBody, Parsefixed here

DALib's S→C GroupRecruitInfo has used Warrior, Wizard, Rogue, Priest, Monk all along, so the library was internally inconsistent and the C→S packet was the outlier.

Verification

Corrected the two byte-pinning tests first and confirmed them red against the unfixed code — exactly 2 failed / 14 passed, differing at index 22 (the third cap byte). Both round-trip tests stayed green throughout, as expected.

After the fix: 947/947.

Downstream

  • hybrasyl-server is correct today and has deliberately left 0x2E stage 4 on its positional read rather than import the swap. It unblocks once this ships in a published package.
  • Brigid is the emitting side and has not been checked. If it publishes group boxes, it is putting the caps on the wire transposed.

…, Priest, Monk

GroupRequestPacket stage 4 read and wrote the five class-cap bytes as
Warrior, Wizard, Monk, Priest, Rogue. The retail client's order is
Warrior, Wizard, Rogue, Priest, Monk — positions 3 and 5 transposed, in
both directions.

Because reader and writer were swapped consistently, DALib round-tripped
with itself perfectly and every test passed. The bug only appears against
a real client or a correct server: DALib-as-server misreads a real
client's recruit box, and DALib-as-client (Brigid) publishes one a real
server misreads. The caps gate who may join an advertised group, so this
is behavioural, not cosmetic.

The file already contradicted itself — the layout comment and the
property declarations both had the correct order; only the prose
sentence, WriteBody and Parse were wrong. DALib's own S->C
GroupRecruitInfo has used the correct order all along.

Grounding (rung 1, Darkages.exe md5 3244dc0e68cd26f4fb1626da3673fda8 plus
that install's shipped UI assets):

  - net_send_group_recruit_start @ 0x00513B30 is a bare five-iteration
    loop over contiguous model bytes — it encodes no class ordering.
  - ui_group_ad_dialog_read_model @ 0x005120A0 fills wire byte k from
    dialog control 2k+7, a monotonic stride-2 walk over the per-row
    control pairs. So wire byte k is screen row k, top to bottom.
  - Class identity is in the artwork, not the code: setoa/_ngcdlg1.txt
    places N_CLASS0 at y=206 and N_CLASS1 at y=233 (dy=+27); overlaying
    those rects on _ngcdlg1.spf reads, top to bottom, Warrior, Wizard,
    Rogue, Priest, Monk.

Corroborated at rung 2 by ewrogers/darkages-741-re @ f12a147
(docs/network/client/046-0x2e-group.md) and by Hybrasyl's GroupRecruit,
both of which already had it right.

Tests: the two byte-pinning cases carried the wrong expectation and its
false proof in a comment ("proving the order W, Wiz, Monk, Priest,
Rogue"). Corrected first and confirmed red against the unfixed code —
exactly two failures, differing at index 22, the third cap byte. Both
round-trip tests stayed green throughout and always would have; they are
symmetric in the defect and cannot pin this. Do not add a round-trip for
it. 947/947.

Provenance: the error entered from a distinct-value capture — fill the
recruit form 1/2/3/4/5, observe 01 02 03 04 05 on the wire. That
experiment cannot answer the question it was used to answer. Filling five
rows top-to-bottom with 1..5 yields that wire whichever class each row
belongs to; it establishes that the wire follows UI row order and is
silent on class identity. The names were assumed, and the assumption was
published as a correction to Chaos.Networking, which had it right.

Refs: HTOO-64
@baughj
baughj merged commit a59728e into main Aug 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant