rebuilt from the feature branches, so anything living there alone is effectively unsaved. This puts
the work on the golden source where it belongs.
What lands here:
1. THE 94-RECORD CATALOG WRITER. The writer parsed only the first 9 product records; it now decodes
the whole message - 94 products, 116 deliverables, 21 groups, 97 shop entries, 0 bytes remainder -
with a byte-exact round trip verified over 12 captures across 3 client builds, plus a mutation
test (fields rewritten to DIFFERENT lengths) and a splice test. The old ceiling was
FRAME_AFTER_BLOCKA = 18 standing in for DisplayInfo presence bits; product 9 (Argi, id 108) is the
first with hasFileDataID = 0, which shifts everything by 4 bytes.
It also corrects three field mappings that were reading the wrong data entirely: ProductID at
record+81 is really DisplayInfo.fileDataID (card artwork), Flags at record+85 is
DisplayInfo.modelSceneID, and the header is 24 bytes / 6 u32, not 28 / 7.
2. THE PURCHASE FIX. SMSG_BATTLE_PAY_PURCHASE_UPDATE has no leading Result field; we were writing
one, so the client read our always-zero Result as the record count and parsed ZERO purchase
records. That silently broke every purchase: with confirmation on, the status-9 record never
reached the client so no prompt appeared and nothing was charged; with it off, the goods arrived
but the UI hung on "Connecting". Its sibling GET_PURCHASE_LIST_RESPONSE genuinely does have a
leading Result - the two share a record type but not a header.
3. THE OWNERSHIP FIX. The catalog blob is a capture from a real retail account and carried that
account's ownership state, which we shipped back verbatim: Product.Eligibility == 2 (Owned) and
Deliverable.AlreadyOwns == 1 on 21 deliverables, affecting 12 products. Both are now cleared -
clearing only Eligibility restores the Buy button but PurchaseProduct still never sends the CMSG.
4. NOT overwriting Product.Flags with our admin DisplayFlags. Product.Flags is a separate unreflected
enum whose bits 1/3 drive buyableHere; writing our value (0 for all 66 rows) over it would have
made every slot-pinned product unpurchasable. Display flags go to DisplayInfo.Flags, which is what
sharedData.flags actually reads.
Enum.BattlepayDisplayFlags is now fully recovered (12 values, registrar RVA 0x13EF840).
5. VAS status queries - CMSG_UPDATE_VAS_PURCHASE_STATES and CMSG_VAS_GET_SERVICE_STATUS, which every
client sends at character select and which were dropped by STATUS_IGNORED without even a log line.