feat(admin): IsAGRoom (IsRRO) toggle on room props, for the editable-Rec-Royale experiment

The Maker Pen permission check runs a room precondition that looks the
current room up in AGRoomRuntimeConfig — a table baked into the client's
resources.assets — and returns a per-original boolean. Rec Center and
Stunt Runner allow creation there; Rec Royale does not, so no blob, role or
server response can grant the pen while the client treats the room as
that original. The table is consulted only for rooms with IsRRO (our
IsAGRoom) set.

Expose IsAGRoom on rooms/{id}/props (+ a checkbox on the General tab) so a
CLONE of Rec Royale can be flipped to a non-original with a real save
attached; whether the client still loads the baked Frontier scene for a
non-original is what that experiment answers. docs/admin.md explains the
gate and the two possible outcomes.
This commit is contained in:
Alexa
2026-09-28 00:34:56 +03:00
parent d7b1722abd
commit 6605f61047
3 changed files with 45 additions and 1 deletions
@@ -975,7 +975,8 @@ public class AdminController(
double? HotScore,
int? State,
string? ImageName,
int? MaxCapacity);
int? MaxCapacity,
bool? IsAGRoom);
/// <summary>POST <c>api/admin/v1/rooms/{id}/props</c> — partial
/// update of room properties. Only the non-null fields in the
@@ -1005,6 +1006,20 @@ public class AdminController(
room.MaxCapacity = Math.Clamp(cap, 1, 80);
changes.Add($"maxCapacity={room.MaxCapacity}");
}
if (body.IsAGRoom is bool ag)
{
// IsAGRoom is what the client reads as IsRRO. True: load the
// baked scene from the client's own assets AND consult the
// client-baked AGRoomRuntimeConfig table (which, per original,
// decides e.g. whether creation is allowed — Rec Center yes, Rec
// Royale no). False: treat the room as a custom room (roles and
// blob decide everything). Flipping a CLONE of a baked original
// to false is the server-side experiment for "editable Rec
// Royale"; whether the client still loads the baked scene for a
// non-original is what that experiment answers.
room.IsAGRoom = ag;
changes.Add($"isAGRoom={ag}");
}
if (changes.Count == 0) return Ok(new { room.Id, unchanged = true });
@@ -165,6 +165,7 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
const [description, setDescription] = useState(room.description);
const [accessibility, setAccessibility] = useState(room.accessibility);
const [cloningAllowed, setCloningAllowed] = useState(room.cloningAllowed);
const [isAGRoom, setIsAGRoom] = useState(room.isAGRoom);
const [tagsCsv, setTagsCsv] = useState(room.tagsCsv);
const [hotScore, setHotScore] = useState(String(room.hotScore));
const [imageName, setImageName] = useState(room.imageName);
@@ -176,6 +177,7 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
setDescription(room.description);
setAccessibility(room.accessibility);
setCloningAllowed(room.cloningAllowed);
setIsAGRoom(room.isAGRoom);
setTagsCsv(room.tagsCsv);
setHotScore(String(room.hotScore));
setImageName(room.imageName);
@@ -193,6 +195,7 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
Description: description,
Accessibility: accessibility,
CloningAllowed: cloningAllowed,
IsAGRoom: isAGRoom,
TagsCsv: tagsCsv,
HotScore: Number.isFinite(hot) ? hot : undefined,
ImageName: imageName,
@@ -283,6 +286,14 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
/>
<span>Cloning allowed</span>
</label>
<label className="flex items-center gap-2 text-sm" title="What the client reads as IsRRO. On: load the baked scene from the client's assets and consult the client's built-in per-original table (which forbids creation in Rec Royale). Off: treat as a custom room — roles and blob decide. Flip a CLONE of an original off to test editable Rec Royale.">
<input
type="checkbox"
checked={isAGRoom}
onChange={e => setIsAGRoom(e.target.checked)}
/>
<span>Rec Room Original (IsRRO) — baked scene + client creation table</span>
</label>
<div className="flex gap-2 pt-2 border-t border-ink-800">
<button onClick={save} disabled={busy} className="btn-primary text-xs">
{busy ? 'Saving…' : 'Save changes'}