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.
This commit is contained in:
Alexa
2026-09-27 22:45:40 +03:00
parent cf3aa29ba2
commit 258183fb94
12 changed files with 2300 additions and 7 deletions
+55
View File
@@ -179,6 +179,61 @@ Enforcement caveat carried over from `RoomEntity.MaxCapacity`: this is the
advertised cap. The client never sets Photon `RoomOptions.MaxPlayers`, so
hard enforcement still needs a ClientMod Photon patch.
## RRO room data blob — Maker Pen and chips in a Rec Room Original (`/rooms` → room detail → Sub-rooms)
Baked Rec Room Originals (Rec Royale, Paintball, Laser Tag, …) are seeded
with an empty `RoomSceneEntity.DataBlobName` and empty
`RoomEntity.CurrentDataBlobName`. Every details builder and matchmaking
resolver (`RoomsController.CurrentOrSyntheticDataBlobName`,
`GoToController.ResolveInitialDataBlobAsync`,
`MatchPlayerController`) turns that into an empty `DataBlob`, and the 2023
client reads an empty blob name as "this room has no room data": it skips
the persistence download entirely, so there are no room roles (no Maker
Pen despite the CDN's RRO role overlay), no CV2 room-settings chips, no
room mood engine and no saves. Real Rec Room served these rooms a blob —
Rec Royale's game-rule chip (`RecRoyaleSolos_GameRuleWrapperCircuit` /
`RecRoyaleSquads_GameRuleWrapperCircuit`, prefabs in `resources.assets`)
is delivered that way; the Frontier scene has no baked game rule.
The Sub-rooms tab shows, per sub-room, the blob the client will actually
be told to download (`effectiveBlobName`) and, for baked originals, two
actions:
- **Enable Maker Pen blob** — `POST api/admin/v1/rooms/{id}/subrooms/{subRoomId}/blob/enable`.
Stamps the synthetic default name (`room_{id}_dorknet_v8.dat` for the
entry scene, `room_{id}_dorknet_v8_sub{n}.dat` otherwise) on the scene
and, for sub-room 0, on the room. No resolver changes are involved: they
already return a non-empty name verbatim, and `CdnController` already
answers that name for AG rooms with the RRO-editable blob (permissive
room roles, no objects) while nothing is saved. `IsRRO` is derived from
`IsAGRoom`, so the client keeps loading the baked map and only
additionally fetches the blob. Idempotent; a no-op on a sub-room that
already has a blob.
- **Reset to baked scene** — `POST api/admin/v1/rooms/{id}/subrooms/{subRoomId}/blob/reset`.
Clears both pointers. This is the escape hatch if a save left the room
unplayable. Saved blobs stay in `RoomDataBlobs`, so the in-game restore
list can bring one back later.
Both are audited (`enable_subroom_blob`, `reset_subroom_blob`). Who can
then save the room in game is the normal rule in `SaveDataCore`: owner,
any admin, or an accepted co-owner (`RoomRoles.Role == 0`). Grant a
co-owner role from the Roles tab if a non-admin should be able to save.
Procedure that makes Rec Royale playable AND editable:
1. Enable the blob on the Solos room's sub-room 0 (and the Squads room's).
2. Join as an admin; the Maker Pen is available. Spawn the room's game-rule
chip (`RecRoyaleSolos_GameRuleWrapperCircuit` in Solos,
`RecRoyaleSquads_GameRuleWrapperCircuit` in Squads) — via the palette's
Game Rules section, or the debug console's `DebugSpawnTool` if it is not
listed — then save. Without that chip there is no `BattleRoyaleManager`
in the scene and the match never starts.
3. From then on the saved blob (with the game-rule chip and whatever
room-settings chips were placed) is what everyone loads. If a save
breaks the room, Reset to baked scene.
Tests: `RroRoomBlobTests`.
## Test cases ↔ GitHub issues
QA test cases can file and track GitHub issues. Configure with:
+3 -1
View File
@@ -55,7 +55,9 @@ DorkNet.sln
watch at a DorkNet server: URI rewrite (`.rec.net` → configured apex),
Photon AppId override, and TLS trust bypass. It loads under
MelonLoader 0.6.x and reads its settings from a JSON config under
`MelonLoader/UserData/`.
`MelonLoader/UserData/`. Opt-in extras (all off by default) live in the
same config: Desktop Screen-Share FPS, RRO quest party size, and Rec
Royale night mode (see the README and `recroom-2023-scene-moods.md`).
---
+25
View File
@@ -132,6 +132,31 @@ 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`): calls
+223
View File
@@ -0,0 +1,223 @@
# Rec Room 2023-03-21 — scene moods (sky dome / sun / fog) and Rec Royale night mode
Client-side reference for the room "mood" engine the March-2023 client uses
for Maker Pen sky/sun/fog settings, and how `DorkNet.ClientMod`
(`RecRoyaleNight.cs`) reuses it to give Rec Royale a night variant the game
never shipped. Everything below was read from the Cpp2IL ISIL dump
(`C:\tmp\recroom-2023-03-21-isil`), the interop stubs
(`C:\tmp\recroom-2023-03-21-decompiled`) and the built game data
(`RecRoom_Data\resources.assets`, `globalgamemanagers`).
## Ownership chain
| Object | Where | Notes |
|---|---|---|
| `RecRoomSceneManager` | `MonoBehaviourPun`, one per room scene | static `get_Instance` (interop property name obfuscated: `PDCOCJBICGC`); name-preserved field `sceneMoodSettings` |
| `RecRoom.Core.Scene.SceneMoodSettings` | plain class held by the scene manager | initialised by `PCJGOACJEDL(RecRoomSceneManager)` at scene start |
| `RecRoom.Core.Creation.SceneMoods.MoodSetting` | `ScriptableObject` asset | `FriendlyName`, `SkyBoxSettings`, `SunSettings`, `FogSettings`, `BloomSettings`, `ColorGradingSettings`, `BackgroundObjectsSettings`, `VolumeProfile`, `BakedReflection`, `guid` |
`SceneMoodSettings` name-preserved members: `availableMoods`
(`MoodSetting[]`), `defaultMood`, `CurrentMood`, `PrevMood`,
`CurrentRoomMoodIndex`, `SupportsMoodCircuits`, `HasRecRoomSkyboxShader`,
`HasSunSource`, `Volume`, `DefaultVolumeProfile`, `ReflectionIntensity`,
`AmbientLightIntensity`, plus one `PFFMDAJPHJO<T>` blender per settings
block (`SkyBoxMoodSettingsBlender`, `SunMoodSettingsBlender`, …).
## Apply path (what actually changes the picture)
`SceneMoodSettings.MLLBALMKEGJ(MoodSetting)` — ISIL
`RecRoom/Core/Scene/SceneMoodSettings.txt:3025-3581`:
1. `ALCMNGKOEBK()` — pushes the mood's six settings blocks into the blenders
as the "top" data (6× generic blender set call).
2. `OEKNGEMDMDB(VolumeProfile)` — swaps the URP post-processing profile.
3. `RenderSettings.set_skybox` — the shared Rec Room skybox material; the
`SkyBoxMoodSettings` blender then writes `_SkyTopColor`-style shader
props into it (`SkyBoxMoodSettings.FPKCBLPBOFN(Material)`).
4. `RenderSettings.get_sun` → re-aims/recolours the sun (`SunMoodSettings`,
only when `HasSunSource`).
5. `ReflectionProbe.set_customBakedTexture` ← `MoodSetting.BakedReflection`.
6. 6× blender force-update.
It does **not** write `CurrentRoomMoodIndex`, so calling it is local only.
### RRO scenes: the engine starts disabled
`SceneMoodSettings.isEnabled` (+0x38) gates everything: `get_CurrentMood`
returns null while it is false, `get_RoomMoodsEnabled` is
`isEnabled && defaultMood != null`, and nothing ticks. The flag is only
switched on by the room data-blob loader (`EIMFCPIHEPF.JPKEMACDNCF`, ISIL
`EIMFCPIHEPF.txt:2136-2310`), which custom rooms run and RRO scenes (no
data blob) never do. Its bring-up sequence:
| Call | Meaning |
|---|---|
| `LKIKMLNPCFK(false)` | SetEnabled(false): applies a null mood (blenders reset, default VolumeProfile back) and toggles `BackgroundObjectsRoot` |
| `LIEDFOLBGHL()` | Capture: builds the blenders' "previous" data from `RenderSettings.skybox` / `sun` / fog; sets `hasRecRoomSkyboxShader` = skybox shader name `== "Custom/SkyBox"` |
| `LKIKMLNPCFK(true)` | SetEnabled(true) |
The apply step then swaps `RenderSettings.skybox` to the global
`MoodConfig` skybox material (`LNMDIIFOFPD.NHAEHKJINFH`, +0x18).
The ClientMod replays this exact sequence when it finds `isEnabled` false.
**The apply step only sets blend targets.** The per-frame update
`CEKLEBHDAHA` (ISIL :3895-4031) is what actually writes sky colours,
rotates/recolours the sun, blends fog and background objects and (via
`CPJBHLMCBBC`) pushes bloom/colour grading into the Volume — and it
returns immediately while `get_CurrentMood()` is null. `get_CurrentMood`
(ISIL :297-457) returns null when `isEnabled` is false, else
`_studioRoomUnityMood` if set, else `availableMoods[index]` when the
synced index is set, else `defaultMood`. RRO scenes have `defaultMood`
null and `availableMoods` empty, so even a fully enabled engine with a
successful apply shows only the ungated background objects (moon, stars).
The ClientMod therefore writes the night mood into `defaultMood` and a
one-element `availableMoods` before enabling the engine.
Two flags captured by `LIEDFOLBGHL` gate what the apply step and the
per-frame update are allowed to touch:
| Flag | Set by capture from | Gates |
|---|---|---|
| `hasRecRoomSkyboxShader` (+0xA8) | `RenderSettings.skybox.shader.name == "Custom/SkyBox"` | sky colour blender apply |
| `hasSunSource` (+0xA9) | `RenderSettings.sun != null` | sun direction / colour / intensity |
Note the sun flag is nested inside the sky check: `hasSunSource` can only
become true when the skybox shader test already passed (ISIL :2629-2705).
`RecRoyale_Frontier` fails both at capture time: its skybox is not the
mood shader. The observable result of a naive apply is exactly "moon and
stars appear, sky and sun unchanged" — the mood's
`BackgroundObjectsSettings` are not gated.
Worse, the apply step's own skybox swap does not help in RRO scenes: it
assigns `MoodConfig.SkyBoxMaterial`, and that material
(`Skybox_Moods_Sunny_Default_Mat`, sharedassets77.assets — a custom-room
scene's shared assets) is not loaded there, so `RenderSettings.skybox`
becomes null (plain black sky) and both flags stay false forever. The
`Custom/SkyBox` shader itself is in resources.assets, so
`new Material(Shader.Find("Custom/SkyBox"))` works anywhere.
Workarounds used by the ClientMod: assign the scene's directional `Light`
to `RenderSettings.sun` if unset; apply once; then put a material on the
`Custom/SkyBox` shader into `RenderSettings.skybox` and
`MoodConfig.SkyBoxMaterial`; capture again so both flags flip true; then
apply for real. Post-exposure additionally needs the scene `Volume`
enabled with weight 1 and `UniversalAdditionalCameraData.renderPostProcessing`
on (`RecRoyaleNightForcePostProcessing`). `LILOMACBFPI()` (:2789-2817) force-sets
both flags to true (writes 0x0101 at +0xA8) and clears
`_studioRoomUnityMood`; it is the Studio path and not used here because
a true `hasSunSource` with a null sun would NRE in the update loop.
Fog: `FogMoodSettings.NLDDIDEIEFH()` reads `RenderSettings.fog`,
`fogStartDistance`, `fogEndDistance`, `fogColor` into the block;
`KGBEBONMIII()` writes them back. Swapping `MoodSetting.FogSettings` for
a freshly captured block before the first apply keeps the scene's fog
(`RecRoyaleNightFog: false`). The block exposes only `color`,
`startDistance`, `fadeDistance` (no enabled flag), so "fog off"
(`RecRoyaleNightDisableFog`) pushes both distances to 1e6 and forces
`RenderSettings.fog = false` after each apply.
Frontier's daytime clouds are scene meshes, not mood background objects:
`CloudPlane` ×19 under `Clouds`, static-batched as
`StaticObjectsStaticBatch_Cloud_RecRoyale_Frontier_Mat` (level106).
`RecRoyaleNightHideObjects` disables renderers whose object or
`sharedMaterial` name contains a pattern (default `Cloud`).
Brightness: the mood blocks are plain data the ClientMod edits in place
before the apply (originals restored on scene exit / toggle-off):
| Block · field | Applied as | ClientMod knob |
|---|---|---|
| `ColorGradingMoodSettings.exposure` | `ColorAdjustments.postExposure` via `VolumeParameter<float>.Override` in `KGBEBONMIII` | `RecRoyaleNightExposure` (EV offset) — the only knob that dims baked lightmaps |
| `SunMoodSettings.sunIntensity` | `RenderSettings.sun.intensity` (needs `hasSunSource`) | `RecRoyaleNightSunScale` (×) |
| — | other directional `Light`s (never touched by the engine; Frontier's `RenderSettings.sun` is `SkyboxLight`, the sun-disc light, not the key light) | `RecRoyaleNightMatchSunLights`: copy the driven sun's rotation/colour, intensity × `RecRoyaleNightSunScale` |
| `SkyBoxMoodSettings.ambientLightIntensity` | `RenderSettings.ambientIntensity` | `RecRoyaleNightAmbientScale` (×) |
Other `ColorGradingMoodSettings` fields: `color`, `temperature`, `tint`,
`hueShift`, `saturation`, `contrast`, `colorFilterIntensity`.
`BloomMoodSettings`: `color`, `diffusion`, `intensity`, `threshold`.
### Networked path (do not use for a client-side effect)
`SceneMoodSettings.IPAMPMBDPNB(Guid)` scans `availableMoods` for the GUID
and calls `set_CurrentRoomMoodIndex`. `_currentRoomMoodIndex` is a
`DFAOKLDJPEL<int>` from `RecRoom.Networking.SynchronizedFields.Runtime`,
i.e. a synchronized field that replicates to everyone in the instance.
CircuitsV2 room-settings chips (`RecRoom.CircuitsV2.RoomSettings.RoomSkydome`,
`RoomSun`, `RoomFog`, all `RoomMoodBase<T,U,V>`) drive the same engine
through `ModifyMood(...)` and are also networked.
## Shipped mood assets
All live in `resources.assets` and are registered in the `globalgamemanagers`
ResourceManager table under their lower-cased asset name (no folder), so
`Resources.Load("<name>", typeof(MoodSetting))` works.
| Asset | `FriendlyName` |
|---|---|
| `Night_Calm_Outdoor_Mood` | Calm Night |
| `Night_Spooky_Outdoor_Mood` | Spooky Night |
| `Night_Wild_Outdoor_Mood` | Wild Night |
| `Night_StuntRunner_Outdoor_Mood` | (Stunt Runner night) |
| `OuterSpace_Mood` | Outer Space |
| `Sunset_Cozy_Outdoor_Mood` | Cozy Sunset |
| `Sunset_Burning_Mood` | Burning Sunset |
| `Sunset_Tropical_Outdoor_Mood` | Tropical Sunset |
| `Sunrise_Misty_Outdoor_Mood` | (misty sunrise) |
Reflection cubemaps ship alongside (`Moods_WildNight_ReflectionProbe`,
`Moods_SunnyDay_Skybox_Reflection_Baked`, …) and are referenced from each
`MoodSetting.BakedReflection`.
## Rec Royale specifics
* Only one Rec Royale map in this build:
`Assets/Activities/RecRoyale/Maps/RecRoyale_Frontier/RecRoyale_Frontier.unity`
(Unity scene name `RecRoyale_Frontier`). The `"recroyale"` literal in
`BattleRoyaleManager` is only `get_RuleTypeAutoTag`, not a scene name.
* `BattleRoyaleManager` has no static instance accessor; scene-name
matching via MelonLoader `OnSceneWasLoaded` is the detection choke point.
* Weapon flashlights already exist: `WeaponFlashlightController` (fields
`weapon`, `flashlightPrefab`, `flashlightRoot`) spawns
`WeaponFlashlight` when `get_FlashlightAllowed` is true. That getter
(ISIL `WeaponFlashlightController.txt:3-75`) is
`Matchmaking.FBELDNLEKAC() != null && [obj+0x28] == 15` — a game-mode
gate for a different activity. Interop property name: `KIMMMCCKMJL`.
## Real CV2 mood chips in an RRO scene
The Maker Pen chips ("Skydome Constant", "Room Skydome Modify/Reset", and
the Sun / Fog / Background Objects trios) drive `RoomSkydome`, `RoomSun`,
`RoomFog`, `RoomBackgroundObjects` (`RecRoom.CircuitsV2.RoomSettings`,
all `RoomMoodBase<T,U,V>`), which hang off `RoomElementSettingsManager` (a
`NetworkedSingletonMonoBehaviour` that `CircuitsV2Manager` brings up in
`MPDIMHOMIDP`). Their state persists in the room blob
(`PersistedRoomData` fields 32-35: `room_fog_data`, `room_mood_sun_data`,
`room_mood_background_objects`, `room_mood_skydome`). None of that runs in
a room the client believes has no data blob, so the prerequisite for chips
in Rec Royale is server-side: give the room a blob (`docs/admin.md` → "RRO
room data blob"). The ClientMod probe line
`[royale-night] CV2 probe in 'RecRoyale_Frontier': …` logs instance counts
for `CircuitsV2Manager`, `RoomElementSettingsManager`, `RoomSkydome`,
`RoomSun`, `RoomFog`, `RoomBackgroundObjects` and `MakerPen` so the effect
of the blob can be verified in one join.
## How the ClientMod uses this
`DorkNet.ClientMod/RecRoyaleNight.cs`:
1. `OnSceneWasLoaded` → scene name contains any `RecRoyaleNightScenes`
entry (default `RecRoyale`) ⇒ arm.
2. `OnUpdate` every 30 frames: `RecRoomSceneManager.Instance.sceneMoodSettings`
present and `CurrentMood` non-null (engine initialised) ⇒
`Resources.Load(RecRoyaleNightMood, MoodSetting)` ⇒
`MLLBALMKEGJ(mood)`. Re-asserted every ~10 s (the engine re-applies its
own mood on quality-level changes).
3. Hotkey (`RecRoyaleNightToggleKey`, a `UnityEngine.KeyCode` name) toggles;
off re-applies `CurrentMood`, i.e. the scene's own default.
4. Harmony postfix on `get_FlashlightAllowed` returns `true` while active
and in a Rec Royale scene (`RecRoyaleNightFlashlights`).
Limits: baked lightmaps are untouched (interiors keep their daytime bake);
darkening comes from the mood's colour grading, sun and ambient settings.
Purely per-client — other players see whatever their own client applies.