ClientMod (2023-03-21): opt-in Rec Royale night mode. RRO scenes never
enable the room mood engine (no data blob → SceneMoodSettings.isEnabled
stays false, CurrentMood null, the per-frame update returns at once), so
the mod replays the room-data loader's bring-up, registers a shipped
night MoodSetting (Night_Calm_Outdoor_Mood by default) as the scene's
default mood, supplies a skybox material on the Custom/SkyBox shader
(MoodConfig.SkyBoxMaterial is not loaded outside custom rooms), assigns
the scene's directional light as RenderSettings.sun, matches the key
light to the engine-driven sun, darkens via the mood's post-exposure and
sun/ambient intensities, forces post-processing on, keeps or disables
fog, hides the daytime cloud meshes, unlocks weapon flashlights, and
restores everything on hotkey-off / scene change. All knobs live in
dorknet-clientmod.json (RecRoyaleNight*), written by install-melon.ps1.
Server/admin: baked Rec Room Originals are seeded with an empty
DataBlobName, which the 2023 client reads as "no room data" — no Maker
Pen, no CV2 room-settings chips, no saves, and for Rec Royale no game at
all (its game-rule chip RecRoyaleSolos_GameRuleWrapperCircuit ships in
the blob, not the scene). New admin actions on the Sub-rooms tab:
POST rooms/{id}/subrooms/{sub}/blob/enable stamps the synthetic default
blob name (which CdnController already answers with the RRO-editable
blob) and blob/reset clears it back to scene-only. IsRRO is derived from
IsAGRoom, so the client keeps loading the baked map.
Docs: README section, docs/admin.md procedure, docs/recroom-2023-room-save.md,
new docs/recroom-2023-scene-moods.md with the ISIL offsets behind all of it.
Tests: RroRoomBlobTests.
8.8 KiB
2023 client room-save flow
Wire contract for the 2023.03.21 client's room save, reverse-engineered
from OGPDOMCNIFM.UploadRoomDataBlobAndSyncReload
(RecRoom.RoomLoading.Runtime) and verified against dev server traffic
on 2026-07-07. Diagnosed while fixing "Failed to save room" /
"An error occurred" save failures.
Sequence
-
Upload room metadata —
POST https://storage.<apex>/upload, multipart (File=file.bin,FileType=6, optionalReferences). FileType 6 = RoomMetadata is new in 2023 (2020 enum stopped at 5=Invention). Small payload (~4 bytes observed for a dorm). Response:{"filename": "roommeta_p<player>_<guid>.bin"}. -
Upload scene save — same endpoint,
FileType=1(RoomSave), payload is the scene blob. Response:{"filename": "<blob>.dat"}(e.g.dorm_p<id>_v<N>.dat). The upload response DTO is{Filename, Hash, OwnershipProof}— the client accepts bothFilename/filenamecasings;HashandOwnershipProofmay be null. -
Commit —
POST https://rooms.<apex>/rooms/{roomId}/subrooms/{subRoomId}/datawith JSON:{ "UnityAssetId": null, "RoomData": { "Filename": "roommeta_….bin", "Hash": null, "OwnershipProof": null }, "SubRoomData": { "Filename": "dorm_p…_vN.dat", "Hash": null, "OwnershipProof": null } }SubRoomData.Filenameis the scene save and must become the room'sCurrentDataBlobName(and the dorm-state row for dorms).RoomData.Filenameis the FileType=6 metadata blob.Response contract (deserializer
NEOPBOMGIOG, mapperKMBHAKAHNGH): legacy{success, value, error}envelope wherevalueMUST contain BOTH keys, non-null:{ "success": true, "value": { "Room": { …same DTO as GET rooms/{id} (BuildRoomServerDetails)… }, "SubRoomDataSave": { "SubRoomDataSaveId": 0, "SubRoomId": 0, "UnityAssetId": "", "DataBlob": "<blob>.dat", "SavedByAccountId": 1, "SavedOnPlatform": 7, "SavedOnDeviceClass": 2, "CreatedAt": "…", … } }, "error": "" }The client parses each field then runs a Dispose walk (
NEOPBOMGIOG.FKDDCNLJOLF, reached from the SaveRoom commit continuation) that dereferences the mapped children — a missing nested object or scalar leaves a null it NREs on, and the save persists server-side but the watch shows "Failed to save room". So theRoomDTO must carry every key theFGCPNAACHIKmapper (GLEGPFFPDBE) reads, including the ones the include-maskedGET rooms/{id}path gets away without: DataBlob, DataBlobHash, MaxPlayers, ToxmodEnabled, and a non-null RankingContext object, plus SubRooms/Roles/Tags/Stats.SubRoomDataSavekeys per theJKIFFPPAJNKmapper (PELEHJLMKJO): SubRoomDataSaveId, SubRoomId, UnityAssetId, DataBlob, DataBlobHash, SavedByAccountId, SavedOnPlatform, SavedOnDeviceClass, Description, CreatedAt.
Server handling: StorageController.Upload (FileType 6 →
UploadGenericAsync("roommeta"), CDN-servable) and
RoomsController.SubRoomData → ReadSaveRoomSceneRequestAsync (nested
2023 JSON via BlobRefDto) → SaveDataCore.
Failure modes seen
| Symptom (client) | Cause |
|---|---|
NDIKGKCFOCG: An error occurred at UploadRoomDataBlobAndSyncReload, upload frame (KPLOPGMJOLE) in stack |
storage.<apex> host not routed at the edge (Traefik 404 page not found) — the save dies before reaching DorkNet. Probe https://storage.<apex>/healthz. |
NDIKGKCFOCG: Failed to save room, no upload frame in stack |
Commit POST rejected — historically 400 missing_room_data_filename because the server parsed only the flat 2020 body and missed SubRoomData.Filename. |
NDIKGKCFOCG: Failed to save room preceded by a NullReferenceException at NEOPBOMGIOG.FKDDCNLJOLF |
Commit returned 200 but value wasn't {Room, SubRoomDataSave} — the save actually persisted; only the response parse failed. |
Room clone (2023)
The in-room "copy room" flow (RoomModel.CopyRoom →
RecNet.Runtime NLDBPDCNNCF.GDHIIAHCBMN) POSTs
roomserver/rooms/{id}/clone with an x-www-form-urlencoded name=…
body — the same roomserver/ prefix as every other room mutation from
that client. A bare-only rooms/{id}/clone route 404s with an empty
body, which the client's promise layer surfaces as a message-less
Failed to copy room: Exception of type 'CEMNLBKJABA' was thrown.
The response must be the FULL room-details object (FGCPNAACHIK,
same shape as GET roomserver/rooms/{id}), not a status wrapper.
Blob semantics on clone (RoomsModerationController.BareClone):
- First-party template source (IsAGRoom + system-owned, e.g. the
RecCenter seed): the clone keeps the copied scenes but gets a
fresh empty blob (
CurrentDataBlobName/sceneDataBlobNamecleared). Any blob on the template row is a MakerPen overlay against the shared baked scene and must not leak into clones; AG room details also require an emptyDataBlobNameon the wire. - User-owned source (including AG-flagged clones of RROs): blobs are copied so "copy room" carries the player's MakerPen edits.
Clones are created private (Accessibility=0) regardless of the
source's visibility; the owner publishes via
rooms/{id}/accessibility when ready.
Sub-room saves list is a PAGED OBJECT
GET rooms/{id}/subrooms/{sub}/saves (and …/saves/no_unity_assets)
must return {"Results":[…],"TotalResults":N} —
PagedResultsDTO<SubRoomDataSaveDTO> per the Studio decompile
(RRStudio-decomp/...RecNet/PagedResultsDTO.cs); the 2023 client's
GetSubRoomDataSaves sends skip/take and its strict reader throws
expected:'{', actual:'[' on a bare array. Because callers run inside
larger flows, the bare-array bug surfaced as "Failed to copy room:
Exception of type '…' was thrown" (clone) and a dead private-instance
button — not as a saves error.
Private rooms: one instance per sub-room
Private rooms (Accessibility=0) allow exactly ONE instance per
sub-room. GoToController.ApplyNewInstanceAsync funnels every join
mode (plain matchmake, JoinMode=1 new-public, JoinMode=2 new-private)
into the same deterministic invite-gated instance
(PrivateInstanceService.EnsureForPrivateRoomAsync, id marker
0xEE0000, keyed on room+subroom — dorms keep their own 0xDD0000
flow). Joins by players who are neither the creator, an accepted
role-holder (co-owner/mod/host), nor an instance invitee return
errorCode=4 (RoomDoesNotExist) so private rooms don't leak their
existence. Tests: PrivateRoomInstanceTests.cs.
Baked Rec Room Originals and data blobs (Rec Royale)
Baked RRO rooms are seeded with empty DataBlobName / CurrentDataBlobName,
which the details builders and matchmaking resolvers pass through as an
empty DataBlob. The 2023 client treats that as "no room data": it never
downloads a blob, so the room has no room roles (no Maker Pen even though
the CDN overlays permissive roles for AG rooms), no CV2 room-settings
chips, no mood engine and nothing to save into. IsRRO (= IsAGRoom) is
independent of the blob, so a baked room CAN carry one: the client loads
the baked map from its own assets and layers the blob's persistence views
on top — exactly how Rec Center behaves.
Rec Royale needs this to be a game at all: the Frontier scene has no baked
game rule (contrast PaintballTeamBattle_GameRuleWrapperCircuit, which is
baked into the paintball scene). Its rule is the Maker Pen chip
RecRoyaleSolos_GameRuleWrapperCircuit / RecRoyaleSquads_GameRuleWrapperCircuit
(BattleRoyaleGameRuleWrapper : GameRuleCircuitWrapperTool), which real
Rec Room delivered inside the room blob. Without it there is no
BattleRoyaleManager in the scene.
Admin switch: POST api/admin/v1/rooms/{id}/subrooms/{sub}/blob/enable
stamps RoomService.SyntheticDefaultRoomDataBlobName(id) on the scene and
room; .../blob/reset clears it. See docs/admin.md → "RRO room data
blob" for the full procedure (enable → spawn the game-rule chip → save).
Related 2023 quirks fixed alongside
- Play-menu search (
IBEOONPEELF.SearchRooms): callsrooms/search_rooms/{query}&skip={n}&take={n}—rooms/-prefixed AND paging embedded in the final path segment. Same segment style onhot_rooms,hot_roomsandplaylists,search_roomsandplaylists(parsed byRoomsController.ParsePagedPathSegment). - Room photo gallery (
KLJOGJHBONK, "Could not show images for room"):api/images/v{2-5}/room/{id}?sort=CheerCount_Desc&filter=PublicOnly—sort/filterarrive as enum NAMES; binding them asintmakes[ApiController]auto-400. Bound as strings, mapped inImagesController.ParseSort.
Tests: DorkNet.Server.Tests/RoomSave2023Tests.cs,
DorkNet.Server.Tests/RoomBrowse2023Tests.cs.