mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
36e64e99ba
CircuitsV2 limits are served as UGC.CircuitsV2.Limit.* gameconfig keys and are editable from admin Settings. Retail drops any send over 5 RPCs/frame or 1024 bytes/60 frames; both network budgets now default to 1,000,000,000. The creation limits are raised too, each against the hazard the client binary shows for it: - Chips 3000 -> 30000: the chip-culling manager preallocates four List<Chip>(Chips), so the 1M clamp stays. - Compute 50000 -> 500000: a per-frame budget with no allocation; the 10M cap keeps its counters from wrapping. - CloudValues 100 -> 1000, InkPerChip 15 -> 0: InkPerChip is an ink COST, multiplied by min(chips, CloudValues), so lowering it is the increase. The clamps tighten to 10000 / 30000 to keep that product from overflowing (the room ink cap is a hard-coded 30000). - UnculledChips / VisibleChips 500 -> 3000: render-cost counts only. Room save uploads may now be up to 100 MB (Kestrel's 30 MB default would 413 a big circuits room). Contract and evidence: docs/recroom-2023-circuitsv2-limits.md. Also includes the Rec Royale night mode client feature (docs/recroom-2023-scene-moods.md) and the admin blob enable/reset for baked RRO sub-rooms.
218 lines
11 KiB
Markdown
218 lines
11 KiB
Markdown
# 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:
|
|
|
|
```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:
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
Stub shape matters. Until something is saved the CDN synthesises the blob,
|
|
and the client fetches it through `RecNet.Rooms.GetRoomData` and rejects
|
|
one it deems unusable — a clone surfaces that as the bare "Failed to copy
|
|
room", a baked room as a silent load with no room roles (no Maker Pen).
|
|
Compared against Rec Center's working `room_100_v1.dat`, the old stub was
|
|
stamped `PersistedRoomVersion` 19 (client ceiling 16; only the S3-hit path
|
|
clamped) and lacked the save skeleton (`last_save_time`, `activity_id` =
|
|
LocationReplicationId, connectable graph, scene settings, object model,
|
|
`room_mood_*`). `RoomDataBlobService.BuildRroEditableBlob` now emits that
|
|
skeleton per room at version 16.
|
|
|
|
Second requirement — game-role objects. Both reference saves carry three
|
|
`PersistenceViewData` entries whose `game_role_node_data` (field 50) holds a
|
|
`PlayerGameRoleData`: the room's default role (Rec Center: "Rec Center",
|
|
Stunt Runner: "StuntRunner"; GUID `c48bac02-…` in both), "In-Game"
|
|
(`role_id` 2097152, GUID `2c721342-…`) and "Eliminated" (`role_id`
|
|
4194304, rank 1015, `can_move` false, GUID `0b9ec39e-…`). Their
|
|
`spawnable_tool_data.prefab_id` is the GameRoleNode chip prefab
|
|
(`c46e1535…` raw). Since 2023 permissions hang off the player's GameRole,
|
|
a blob without these leaves a co-owner with no Maker Pen. The stub now
|
|
generates Rec Royale's own set (view ids derived from the activity id).
|
|
|
|
Game-rule chip. A newer-client export of a custom Rec Royale room showed
|
|
the saved `RecRoyaleSquads_GameRuleWrapperCircuit` chip is minimal:
|
|
`spawnable_tool_data.prefab_id` `857f7500…`, `tagged_tool_data` tag
|
|
`recroyalesquadsgameruleci` (prefab name lower-cased, 25 chars),
|
|
`circuit_node_data.id`, and an EMPTY `game_rule_wrapper_data` (field 41).
|
|
Prefab ids live in the client's spawnable-tool table in
|
|
`resources.assets`: after `SpawnableTool_Runtime_<ENUM>` comes a 20-byte
|
|
hash (len-prefixed), two u32s, then the 16-byte prefab id (len-prefixed),
|
|
then the enum again, display name and tag. Extracted:
|
|
Solos `983db81a68927f419edb0daf1243e93e`, Squads
|
|
`857f750016ca77408ae9f72f9877748a` (matches the save), StuntRunner
|
|
`28276d92470ff34ab05f72b2707a04d1`. Stunt Runner's other rule-ish object
|
|
(`84eb8434…`, `game_configuration_data` field 40) is the generic Game
|
|
Configuration chip, optional — `GameConfigurationTool` falls back to its
|
|
baked asset. The stub emits the wrapper chip for the two Rec Royale rooms.
|
|
|
|
Attaching a real save: `POST api/admin/v1/rooms/{id}/subrooms/{sub}/blob/upload`
|
|
(multipart `file`, `?mode=basics|full`) rewrites any-version
|
|
`PersistedRoomData` for the room — see `docs/admin.md` → "Upload save".
|
|
Used to A/B a generated stub against a genuine custom-Rec-Royale save.
|
|
|
|
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`): 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`.
|