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
+73 -2
View File
@@ -563,6 +563,11 @@ interface SubRoom {
canMatchmakeInto: boolean;
isSandbox: boolean;
dataBlobName: string;
/** What the client is actually told to download: scene blob, else room
* blob, else synthetic default — or '' for a baked Rec Room Original
* with no blob enabled (scene geometry only, no Maker Pen / chips). */
effectiveBlobName: string;
isBakedOriginal: boolean;
}
// Per-sub-room player caps. Distinct from the room's own MaxCapacity, which is
@@ -576,6 +581,34 @@ function SubRoomsTab({ roomId, roomCapacity }: { roomId: number; roomCapacity: n
const { data, loading, error, refresh } = useApi<SubRoom[]>(`/rooms/${roomId}/subrooms`);
const [edits, setEdits] = useState<Record<number, string>>({});
const [saving, setSaving] = useState<number | null>(null);
const [blobBusy, setBlobBusy] = useState<number | null>(null);
const [resetArmed, setResetArmed] = useState<number | null>(null);
// Baked Rec Room Originals ship with no data blob, so the client never
// runs its room-data path there (no Maker Pen, no CV2 chips, no saves).
// "Enable" stamps the synthetic default name the CDN already answers with
// the RRO-editable blob; "Reset" clears the pointers back to scene-only.
const setBlob = async (sub: SubRoom, action: 'enable' | 'reset') => {
setBlobBusy(sub.subRoomId);
try {
const res = await api<{ effectiveBlobName: string; changed: boolean }>(
`/rooms/${roomId}/subrooms/${sub.subRoomId}/blob/${action}`,
{ method: 'POST' },
);
toast.push(
action === 'enable'
? `${sub.name || `Sub-room ${sub.subRoomId}`} now loads ${res.effectiveBlobName}`
: `${sub.name || `Sub-room ${sub.subRoomId}`} reset to the baked scene (no blob)`,
'success',
);
setResetArmed(null);
refresh();
} catch (e) {
toast.push((e as Error).message, 'error');
} finally {
setBlobBusy(null);
}
};
const save = async (sub: SubRoom) => {
const raw = edits[sub.subRoomId];
@@ -631,8 +664,46 @@ function SubRoomsTab({ roomId, roomCapacity }: { roomId: number; roomCapacity: n
{sub.isSandbox && <span className="badge-neutral">Sandbox</span>}
{!sub.canMatchmakeInto && <span className="badge-banned">No matchmaking</span>}
</div>
{sub.dataBlobName && (
<div className="text-xs text-ink-500 font-mono truncate mt-0.5">{sub.dataBlobName}</div>
<div className="text-xs text-ink-500 font-mono truncate mt-0.5">
{sub.effectiveBlobName
? sub.effectiveBlobName
: sub.isBakedOriginal
? 'no data blob — baked scene only (no Maker Pen / chips)'
: '(synthetic default blob)'}
</div>
{sub.isBakedOriginal && (
<div className="flex items-center gap-2 mt-1.5">
{!sub.dataBlobName ? (
<button
onClick={() => setBlob(sub, 'enable')}
disabled={blobBusy === sub.subRoomId}
className="btn-secondary text-xs"
title="Serve a room data blob for this baked sub-room so Maker Pen, CV2 chips and saves work. The first in-game save replaces it with a real upload."
>
{blobBusy === sub.subRoomId ? 'Enabling…' : 'Enable Maker Pen blob'}
</button>
) : resetArmed === sub.subRoomId ? (
<>
<span className="text-xs text-danger">Drops the blob pointer; saved blobs stay restorable.</span>
<button
onClick={() => setBlob(sub, 'reset')}
disabled={blobBusy === sub.subRoomId}
className="btn-danger text-xs"
>
{blobBusy === sub.subRoomId ? 'Resetting…' : 'Confirm reset'}
</button>
<button onClick={() => setResetArmed(null)} className="btn-secondary text-xs">Cancel</button>
</>
) : (
<button
onClick={() => setResetArmed(sub.subRoomId)}
className="btn-secondary text-xs"
title="Go back to the baked scene with no data blob (escape hatch if a save broke the room)."
>
Reset to baked scene
</button>
)}
</div>
)}
</div>
<div className="flex items-center gap-2">