Regression from adding the room-level DataBlob key: it was filled from the
per-ROOM name while sub-room 0 resolves its own, override included. A dorm's
data is per-PLAYER (dorm_p{id}_v*.dat) and reaches the builder as that
override, so the room level advertised a room_{id}_* file that was never
written. The client fetched it and rejected the whole room with "the data in
the room you are trying to load is corrupt" — which reads like a broken save
rather than a payload that disagrees with itself.
Both now come from one resolved value, so whichever key the client reads
first it gets the same file. The no-scenes fallback sub-room honours the
override too, instead of falling back to the per-room name.
Adds the invariant as a test: room-level DataBlob == SubRooms[0].DataBlob.
The DTO parity tests could not have caught this — the key was present and
correctly named, it just pointed somewhere else.
Both bugs were the same shape: we answered HTTP 200 with a key set the client's
generated reader did not agree with, the reader threw mid-deserialise, and it
surfaced later as a bare "Failed to copy room" / a chat that never opened, with
no URL in the client log and no payload in the server log.
Rather than guess at fields, tools/extract-client-dto-keys.py reads the readers
out of the ISIL dump. Cpp2IL emits each generated reader's property names as
string literals in registration order (three spellings per field: Pascal,
camel, lower), so the extracted catalogue is the client's own contract. 156
reader types land in DorkNet.Server.Tests/data/client-dto-keys-2023.json, and
ClientDtoParityTests asserts our real responses against them — nothing missing,
nothing extra.
Chat (EKEFFNBIOHJ) was missing exactly one key: LatestMessage. It was only
emitted when a thread HAD messages, so it vanished from the one response where
it was needed — the thread that thread/withmembers has just created, which by
definition has none. It is now always present, null when there is nothing to
report; a registered slot wants a value and an absent key is not one.
Room details (GLEGPFFPDBE) was missing DataBlob, DataBlobHash, MaxPlayers,
ToxmodEnabled and RankingContext, and sent 15 keys the reader has no slot for.
The 15 are gone: UgcVersion, UgcSubVersion, MinUgcSubVersion, PublishState,
PublishStateAvailability, BoostCount, ExcludeFromLists, IsJuniorCreated,
IsRecRoomApproved, IsRoomLinkedToRecRoomStudio, IsStudioRoom, StudioSessionId,
RestrictedCircuitsAllowListNames, AllowNewUsers, AllowsNewUsers.
That deletion also removes a comment asserting the 2023 client reads UgcVersion
to build its asset-bundle URL. It does not — the literal appears nowhere in the
entire decompile, and neither do PublishState or BoostCount.