Files
DorkNet/docs/recroom-2023-scene-moods.md
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

12 KiB
Raw Permalink Blame History

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 Lights (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.