fix: nested room-details objects carry only keys the client reads

Extends the parity check into the payload's nested objects — sub-room 0, the
first role, stats and the first tag each have their own generated reader, and a
key set that disagrees with one of those breaks the room just as hard as a
wrong top-level set.

It immediately found seven invented keys. Six on every sub-room (CurrentSave,
DefaultMatchmakingPolicy, LastModeratedSaveModerationState, ShouldAutoStageSaves,
StagedSubRoomDataSaveId, SubRoomUnityAssetId) and IsPrimaryGenre on every tag,
whose real shape is exactly {Tag, Type}. Six of the seven appear NOWHERE in the
decompile; SubRoomUnityAssetId exists but belongs to another DTO, not this one.

The extractor's minimum field count drops from 4 to 2, which is what let this
hide: a room tag has two fields, so a 4-field floor skipped its reader entirely
and the invented third key had nothing to be checked against. The alias rule in
fields_in() is what rejects false positives — the field count never was.

This does not explain the clone failure on its own: the captured response
carries all 41 top-level keys and is still rejected. But it removes the whole
class of "we send something the reader has no slot for" from the search.
This commit is contained in:
Alexa
2026-07-28 11:27:35 +03:00
parent 078d576383
commit 4a1ac47007
4 changed files with 1103 additions and 47 deletions
+5 -3
View File
@@ -35,9 +35,11 @@ import re
import sys
from pathlib import Path
# A type with only a couple of fields is usually a wrapper or a false positive
# (enum name tables register short literal runs too). Real wire DTOs are wider.
MIN_FIELDS = 4
# Two is deliberate, not lax. Several real wire DTOs are exactly that small —
# a room tag is {Tag, Type} — and a higher floor hides them, which is how an
# invented third key on the tag object went unnoticed. The alias rule in
# fields_in() is what rejects false positives, not the field count.
MIN_FIELDS = 2
LITERAL = re.compile(r'Move \w+, "([A-Za-z][A-Za-z0-9_]*)"')