Files
DorkNet/docs/recroom-2023-room-save.md
T
Alexa 258183fb94 feat: Rec Royale night mode (ClientMod) + admin switch to give baked RRO rooms a data blob
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.
2026-09-27 22:45:40 +03:00

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

  1. Upload room metadata — POST https://storage.<apex>/upload, multipart (File=file.bin, FileType=6, optional References). 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"}.

  2. 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 both Filename/filename casings; Hash and OwnershipProof may be null.

  3. Commit — POST https://rooms.<apex>/rooms/{roomId}/subrooms/{subRoomId}/data with JSON:

    {
      "UnityAssetId": null,
      "RoomData":    { "Filename": "roommeta_….bin", "Hash": null, "OwnershipProof": null },
      "SubRoomData": { "Filename": "dorm_p…_vN.dat", "Hash": null, "OwnershipProof": null }
    }
    

    SubRoomData.Filename is the scene save and must become the room's CurrentDataBlobName (and the dorm-state row for dorms). RoomData.Filename is the FileType=6 metadata blob.

    Response contract (deserializer NEOPBOMGIOG, mapper KMBHAKAHNGH): legacy {success, value, error} envelope where value MUST 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 the Room DTO must carry every key the FGCPNAACHIK mapper (GLEGPFFPDBE) reads, including the ones the include-masked GET rooms/{id} path gets away without: DataBlob, DataBlobHash, MaxPlayers, ToxmodEnabled, and a non-null RankingContext object, plus SubRooms/Roles/Tags/Stats. SubRoomDataSave keys per the JKIFFPPAJNK mapper (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/scene DataBlobName cleared). 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 empty DataBlobName on 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).

  • Play-menu search (IBEOONPEELF.SearchRooms): calls rooms/search_rooms/{query}&skip={n}&take={n} — rooms/-prefixed AND paging embedded in the final path segment. Same segment style on hot_rooms, hot_roomsandplaylists, search_roomsandplaylists (parsed by RoomsController.ParsePagedPathSegment).
  • Room photo gallery (KLJOGJHBONK, "Could not show images for room"): api/images/v{2-5}/room/{id}?sort=CheerCount_Desc&filter=PublicOnly — sort/filter arrive as enum NAMES; binding them as int makes [ApiController] auto-400. Bound as strings, mapped in ImagesController.ParseSort.

Tests: DorkNet.Server.Tests/RoomSave2023Tests.cs, DorkNet.Server.Tests/RoomBrowse2023Tests.cs.