mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
feat: lift the CircuitsV2 limits, Rec Royale night mode, RRO room blobs
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.
This commit is contained in:
+157
@@ -139,6 +139,45 @@ in S3 is modified. Toggle endpoint:
|
||||
`{"Disabled": bool}`. Clamped responses are served with a 60s edge TTL so
|
||||
flipping the toggle lands within a minute.
|
||||
|
||||
## CircuitsV2 limits (`/settings`)
|
||||
|
||||
`ServerSettingsEntity.CircuitsV2LimitsJson` backs the
|
||||
`UGC.CircuitsV2.Limit.*` gameconfig keys the client reads into
|
||||
`RecRoom.CircuitsV2.Config.LimitConfig`. The panel's headline setting is
|
||||
the network budget: retail allows a circuit **5 RPCs per frame** and
|
||||
**1024 bytes per 60 frames**, and silently **drops** every send past
|
||||
that, which is the usual cause of a busy CV2 room appearing to desync.
|
||||
DorkNet ships both budgets at 1,000,000,000.
|
||||
|
||||
Two things about this are counter-intuitive enough to be worth stating:
|
||||
|
||||
- The budgets are spent over a rolling window measured in **frames**, so
|
||||
widening a window makes the limit *stricter*, not looser. Raise the
|
||||
budget, not the window.
|
||||
- The **byte** budget is checked before the **RPC** budget, so raising
|
||||
`Rpcs` on its own leaves circuits blocked. Both have to move together.
|
||||
- `InkPerChip` is a **cost** charged against a 30000-ink room cap that is
|
||||
hard-coded in the client, so lowering it is what gives circuits more
|
||||
room. DorkNet ships it at 0.
|
||||
|
||||
The room-size limits ship at about 10x retail: Compute 500000, Chips
|
||||
30000, CloudValues 1000, and UnculledChips/VisibleChips 3000.
|
||||
CloudValuesAutosaveSeconds stays at 300.
|
||||
|
||||
Values are clamped on save, and most of the clamps are crash guards
|
||||
rather than taste. `Rpcs` of 0 divides by zero in the client's CV2 debug
|
||||
subscreen. A time window allocates one array slot per frame, so an
|
||||
unbounded window is an out-of-memory kill at circuit init. Chips sets
|
||||
the size of four list preallocations. InkPerChip × CloudValues is an
|
||||
unchecked int multiply. Compute is the client's only runaway-loop
|
||||
guard, so it has to stay finite. Full
|
||||
reasoning and the disassembly it came from are in
|
||||
`docs/recroom-2023-circuitsv2-limits.md`.
|
||||
|
||||
Endpoints: `GET`/`POST api/admin/v1/settings/circuits-limits`. The client
|
||||
builds its rate limiter once when circuits start, so **players must
|
||||
rejoin their room** before a change takes effect.
|
||||
|
||||
## Per-sub-room max players (`/rooms` → room detail)
|
||||
|
||||
Player caps are two separate settings, and conflating them is the easy
|
||||
@@ -179,6 +218,124 @@ 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. Seeded originals such as Rec Royale have **no
|
||||
`RoomScenes` row at all** ("Scenes 0"); the list synthesises a virtual
|
||||
"Home" entry (`isVirtual: true`, max players = room capacity) and enable
|
||||
creates the real row with the game-side defaults
|
||||
(`RoomsController.GetOrCreateSceneForMutationAsync`).
|
||||
What the CDN hands back for that name while nothing is saved is a
|
||||
**per-room stub** built by `RoomDataBlobService.BuildRroEditableBlob`:
|
||||
the skeleton of a real save (Rec Center's `room_100_v1.dat` was the
|
||||
reference) — `version` 16 (the client's ceiling; the old roles-only stub
|
||||
was stamped 19 and the client fetched it and silently loaded the room
|
||||
with no roles, i.e. no Maker Pen), `activity_id` = the room's
|
||||
`LocationReplicationId`, `last_save_time`, empty connectable graph /
|
||||
object model, scene settings and the four `room_mood_*` blocks, plus the
|
||||
permissive role collection. The miss path now runs the same 2023
|
||||
version clamp as the S3-hit path. It also carries the room's own three
|
||||
**game-role objects** (a default role named after the room, plus the
|
||||
built-in "In-Game" and "Eliminated" roles, well-known GUIDs shared by
|
||||
every room save — Rec Center and Stunt Runner both have exactly these).
|
||||
The 2023 client grants Maker Pen off the player's *game* role
|
||||
(`ICreatorRole.CreatorRoleCanUseMakerPen` on `GameRole`), so a blob with
|
||||
no role objects leaves even a co-owner with nothing to hold — that was
|
||||
the second reason "enable + co-owner" still showed no Maker Pen. The
|
||||
default role sets `can_use_maker_pen` to true, matching the permissive
|
||||
room-role overlay; the built-in roles mirror the reference saves.
|
||||
For the two Rec Royale rooms the stub also carries the room's
|
||||
**game-rule chip** (`RecRoyaleSolos_GameRuleWrapperCircuit` prefab id
|
||||
`983db81a…`, `RecRoyaleSquads_…` `857f7500…`, read from the client's
|
||||
spawnable-tool table; a saved Squads chip is header + prefab id + tag +
|
||||
node id + an empty wrapper payload), so the match logic exists on first
|
||||
join without anyone spawning it by hand.
|
||||
- **Upload save** — `POST api/admin/v1/rooms/{id}/subrooms/{subRoomId}/blob/upload?mode=full|basics`,
|
||||
multipart `file` = a real `PersistedRoomData` save from ANY client
|
||||
version (`.binpb` / `.room` / `.dat`, e.g. a RecNet export of a custom
|
||||
Rec Royale room). The server rewrites it for this room
|
||||
(`RoomDataBlobService.PrepareUploadedRoomBlob`): `activity_id` → the
|
||||
room's `LocationReplicationId`, drops `output_log`, `sub_room_id` and
|
||||
every field the 2023 schema doesn't know, clamps the header versions to
|
||||
the 2023 maxima, and in `basics` mode keeps only the game-role (field 50)
|
||||
and game-rule (fields 40/41) objects while stripping the circuit payloads
|
||||
(`circuit_data`, connectable graph, `circuit_v2_data` — a newer
|
||||
CircuitsV2 graph is rejected by the client's own CV2 version gate no
|
||||
matter what). Stored under a fresh `room_{id}_upload_*.dat`, recorded in
|
||||
`RoomDataBlobs`, and the sub-room (and room, for sub-room 0) is pointed
|
||||
at it. On serve the CDN applies the RRO role overlay and clamp exactly
|
||||
as for Rec Center's saved blob. Response carries a `summary`
|
||||
(`viewsIn/viewsKept/roleViews/gameRuleViews/versionIn/versionOut`).
|
||||
- **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`.
|
||||
|
||||
### Why Rec Royale still has no Maker Pen after all of the above
|
||||
|
||||
The Maker Pen permission check (`AGMBHDNGOEH.IJEIEBGJJLB`) first runs a
|
||||
room precondition (`LALJEMHEIBH`) that, via `MLKAFHHHEGA.GHPIBJPNOFF` →
|
||||
`PNPLLCBBMJP.JGPBNEHFLJA`, 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. No blob, role or
|
||||
server response changes that table, and it is consulted only for rooms the
|
||||
client considers originals (`IsRRO` = our `IsAGRoom`).
|
||||
|
||||
Server-side experiment: clone Rec Royale, flip the clone's **Rec Room
|
||||
Original (IsRRO)** checkbox off on the General tab
|
||||
(`rooms/{id}/props` `IsAGRoom: false`), upload a real save to it, join. If
|
||||
the client still loads the Frontier scene for a non-original, Rec Royale
|
||||
becomes an editable template; if the scene fails to load, the table gate
|
||||
has to be bypassed client-side (ClientMod postfix on `GHPIBJPNOFF`).
|
||||
|
||||
## Test cases ↔ GitHub issues
|
||||
|
||||
QA test cases can file and track GitHub issues. Configure with:
|
||||
|
||||
@@ -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`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
# 2023 client CircuitsV2 limits (`UGC.CircuitsV2.Limit.*`)
|
||||
|
||||
How the March-2023 client budgets CircuitsV2, and how DorkNet lifts the
|
||||
network budget so circuits stop dropping sends. Reverse-engineered from
|
||||
`RecRoom.CircuitsV2.Config.LimitConfig` in the 2023-03-21 ISIL dump
|
||||
(`IsilDump/Assembly-CSharp/RecRoom/CircuitsV2/Config/LimitConfig.txt`)
|
||||
and the limiter chain under `IsilDump/Circuits.V2/`.
|
||||
|
||||
## Why it matters
|
||||
|
||||
Retail budgets a circuit **5 RPCs per frame** and **1024 bytes per 60
|
||||
frames**. A circuit that exceeds either budget does not queue or slow
|
||||
down — the send is **dropped on the floor** and an execution error of
|
||||
kind 2 (bytes) or 3 (RPCs) is raised, and the room save gets flagged
|
||||
`cv2_rpc_limited` / `cv2_net_limited`. This is what makes busy CV2 rooms
|
||||
feel like they're desyncing at random.
|
||||
|
||||
DorkNet serves both budgets at 1,000,000,000, which is more than a
|
||||
circuit can spend in a frame. It also raises the room-size limits
|
||||
(compute, chips, cloud variables and culling) to about 10x retail.
|
||||
[The scalar limits](#the-scalar-limits) covers what each one does and
|
||||
why its value stops where it does.
|
||||
|
||||
## The keys
|
||||
|
||||
The client reads these through its gameconfig provider, i.e. the array
|
||||
served by `GET api/gameconfigs/v1/all`. Any key the server omits — or
|
||||
sends unparseably — silently falls back to a value baked into the
|
||||
client, with no error logged.
|
||||
|
||||
| Key | Type | Client default | DorkNet default |
|
||||
| --- | --- | --- | --- |
|
||||
| `UGC.CircuitsV2.Limit.Network` | JSON array | one entry, below | budgets lifted |
|
||||
| `UGC.CircuitsV2.Limit.Compute` | int | 50000 | 500000 |
|
||||
| `UGC.CircuitsV2.Limit.Chips` | int | 3000 | 30000 |
|
||||
| `UGC.CircuitsV2.Limit.CloudValues` | int | 100 | 1000 |
|
||||
| `UGC.CircuitsV2.Limit.CloudValuesAutosaveSeconds` | float | 300 | 300 |
|
||||
| `UGC.CircuitsV2.Limit.UnculledChips` | int | 500 | 3000 |
|
||||
| `UGC.CircuitsV2.Limit.VisibleChips` | int | 500 | 3000 |
|
||||
| `UGC.CircuitsV2.Limit.InkPerChip` | int | 15 | 0 |
|
||||
|
||||
The client defaults are the values `LimitConfig..ctor` bakes in (the
|
||||
`mov dword ptr [rbx+28h..40h]` run in `LimitConfig.txt`). For comparison,
|
||||
the 2023-06-21 build raised its `COMPUTE_DEFAULT` to 100000 and left the
|
||||
others unchanged.
|
||||
|
||||
Scalars are parsed with `int.TryParse` / `float.TryParse` under
|
||||
`NumberStyles.Integer` / `NumberStyles.Float` and
|
||||
`CultureInfo.InvariantCulture` — so no thousands separators, and a dot
|
||||
decimal separator only.
|
||||
|
||||
## The network array
|
||||
|
||||
`UGC.CircuitsV2.Limit.Network` is a **top-level JSON array**, parsed by
|
||||
Newtonsoft (`JsonConvert.DeserializeObject`), not Unity's `JsonUtility`.
|
||||
Newtonsoft binds public fields and falls back to case-insensitive
|
||||
matching, but DorkNet emits the client's own field names verbatim:
|
||||
|
||||
```json
|
||||
[{"NumPlayersForActivation":0,"Bytes":1000000000,"BytesTimeWindowFrame60s":60,"Rpcs":1000000000,"RpcTimeWindowFrame60s":1}]
|
||||
```
|
||||
|
||||
The retail value is the same shape with `Bytes` 1024, `Rpcs` 5.
|
||||
|
||||
**Entry selection.** The client sorts entries ascending by
|
||||
`NumPlayersForActivation` and activates the highest one whose threshold
|
||||
is at or below the room's **max player capacity** (not the live player
|
||||
count). One entry at threshold 0 therefore applies everywhere.
|
||||
|
||||
**Validation.** The client rejects the whole array — falling back to the
|
||||
retail budget — if it is not valid JSON, is empty, or has no entry with
|
||||
`NumPlayersForActivation` of 0. `ServerSettingsService` normalizes
|
||||
around all three: it sorts, de-duplicates thresholds, and rewrites the
|
||||
lowest entry's threshold to 0.
|
||||
|
||||
## The scalar limits
|
||||
|
||||
Every getter is consumed in exactly the places listed here. A sweep of
|
||||
the whole ISIL dump for `LimitConfig.get_` finds only these files.
|
||||
|
||||
### Compute: per-frame CPU budget
|
||||
|
||||
`CircuitsV2Manager`'s per-frame tick (`OCGDEIJJBAG(float)`, ISIL 240 at
|
||||
`CircuitsV2Manager.txt:21241`) builds a fresh compute context every
|
||||
frame through `DIILFAPPLOI` (ISIL 018 at `:12637`), which wraps the value
|
||||
in `Circuits.V2/BENFNKBKHEA`. That object keeps four int counters. Each
|
||||
charge adds to one of them and checks the sum against the budget with
|
||||
`<=`. The default costs come from the `MPKGOFPJCOP` interface: 100, 100,
|
||||
50 or 500 depending on the kind of operation. When the budget runs out,
|
||||
circuit work stops for the rest of the frame and the save is flagged
|
||||
`cv2_cpu_limited`.
|
||||
|
||||
Nothing is allocated from this value. **It is the only guard against
|
||||
runaway loops**, though. Raising it lets a broken loop burn more frame
|
||||
time before it is stopped. A budget near `int.MaxValue` would let the
|
||||
summed counters wrap negative so the check never fires. DorkNet serves
|
||||
500,000, which is 10x retail and 5x the June 2023 build. That allows
|
||||
1,000 to 10,000 operations per frame, depending on their kind.
|
||||
|
||||
### Chips: room chip cap
|
||||
|
||||
`CreationManager.OnCircuitsV2LimitsRefreshed` (`CreationManager.txt:3179`)
|
||||
rebuilds the room's max sandbox resources as
|
||||
`CostInfo(ink 30000, lights 64, ai 101, numChips = Chips, numCloudVariables = CloudValues)`
|
||||
(ISIL 050-068). Adding a chip checks `CanAffordResourceCost(OneChip)`
|
||||
against it. A failed check shows the "Too many chips." toast
|
||||
(`CircuitsV2Manager_NestedType_AMEJFMPHNEO.txt` ISIL 164-187, 592).
|
||||
|
||||
Hazard: the chip-culling manager's constructor preallocates **four**
|
||||
`List<Chip>(Chips)` (`PAFDMBHNMBP.txt` `.ctor` ISIL 053-108). That costs
|
||||
32 bytes per unit of the limit whether or not the room uses it, and a
|
||||
value of 0 or less throws. The clamp is 1..1,000,000, which is 32MB at
|
||||
the top. DorkNet serves 30,000, which preallocates under 1MB.
|
||||
|
||||
### CloudValues: room cloud-variable cap
|
||||
|
||||
This is the `numCloudVariables` field of the same max-resources
|
||||
`CostInfo`. `CreationManager.CalculateActiveResourceCost` also reads it
|
||||
(ISIL 389-415 at `CreationManager.txt:6136-6162`):
|
||||
|
||||
```
|
||||
inkCost += InkPerChip * Math.Min(numChips, CloudValues)
|
||||
```
|
||||
|
||||
That is an unchecked int multiply. It also means that raising
|
||||
CloudValues while InkPerChip stays at retail's 15 makes chips use
|
||||
**more** room ink. Serving InkPerChip 0 removes the term. The clamp was
|
||||
lowered from 100,000 to 0..10,000 so the product stays at or below 3e8
|
||||
even with InkPerChip at its own ceiling. DorkNet serves 1,000.
|
||||
|
||||
### InkPerChip: ink cost per chip (lower is more)
|
||||
|
||||
This is a **cost**, not an allowance. It feeds the
|
||||
`CalculateActiveResourceCost` term above. The add-chip path also charges
|
||||
it directly through `CanAffordResourceCost` ("Not enough ink.",
|
||||
`AMEJFMPHNEO` ISIL 247-265). That direct charge only runs when
|
||||
`ShowGadgetsToEveryone` is set and `Chips < VisibleChips` (ISIL 221-234),
|
||||
so with the defaults below it never runs.
|
||||
|
||||
The room's ink cap is a **hard-coded 30000** (`0x7530`,
|
||||
`CreationManager.txt:3374`), not a gameconfig key. Lowering this cost is
|
||||
therefore the only way to leave circuits more room under that cap. No
|
||||
client code divides by it. DorkNet serves **0**, so chips use no room
|
||||
ink. The clamp is 0..30,000, because past 30000 no single chip can be
|
||||
afforded.
|
||||
|
||||
### UnculledChips and VisibleChips: chip culling
|
||||
|
||||
The chip-culling manager reads both together (`PAFDMBHNMBP.DJLEFJCOLCF`,
|
||||
ISIL 079-087 at `PAFDMBHNMBP.txt:2520-2525`). It subtracts
|
||||
`UnculledChips` from the live chip count and culls that many chips.
|
||||
`VisibleChips` is used in the same arithmetic further down. Both values
|
||||
only appear in counts and subtractions: they allocate nothing and are
|
||||
never divisors. Raising them costs rendering time, not stability.
|
||||
DorkNet serves 3,000 for both, so every chip in a retail-sized room stays
|
||||
drawn. Both stay at or below Chips, which keeps the `Chips < VisibleChips`
|
||||
ink charge above switched off.
|
||||
|
||||
### CloudValuesAutosaveSeconds: save cadence
|
||||
|
||||
A `CircuitsV2Manager` coroutine reads this value
|
||||
(`CircuitsV2Manager_NestedType_CDPKCHNIHFG.txt` ISIL 059 at `:193`),
|
||||
waits that many seconds, then flushes the CircuitsV2 player save-data
|
||||
manager. It sets how often saves happen, not how much a room can hold. A
|
||||
shorter interval sends more save traffic to a shared server. A longer
|
||||
one loses more progress if the client crashes. DorkNet leaves it at
|
||||
retail's 300 seconds. The clamp stays at 1..3600 and rejects non-finite
|
||||
values.
|
||||
|
||||
### Limits these keys don't raise
|
||||
|
||||
- **Room ink 30000, lights 64 and AI controllers 101** are literals in
|
||||
the max-resources `CostInfo` (`CreationManager.txt:3374`, and again in
|
||||
the static constructor at `:22761`).
|
||||
- **Event recursion depth is fixed at 16.**
|
||||
`Circuits.V2/BCNIMLFCMMD.MENMKOOAJPF` (ISIL 041,
|
||||
`Compare [rbx+240], 16`) raises "Max recursion depth reached. Do you
|
||||
have events in a loop?"
|
||||
- **Room save uploads are capped at 100,000,000 bytes on the server.**
|
||||
`StorageController.Upload` sets `[RequestSizeLimit]` and
|
||||
`MultipartBodyLengthLimit` to 100 MB, up from Kestrel's 30 MB default
|
||||
that a room with tens of thousands of chips could reach. 100 MB is
|
||||
also Cloudflare's per-request ceiling, so raising it further does
|
||||
nothing behind the tunnel.
|
||||
|
||||
## How the budget is enforced
|
||||
|
||||
Each budget is a sliding window with **one bucket per rendered frame**.
|
||||
`RpcTimeWindowFrame60s` is that window's length in frames, so 1 means
|
||||
per frame and 60 means roughly per second at 60fps. On each send the
|
||||
client adds the cost to the current bucket and compares the window's
|
||||
running sum against the budget.
|
||||
|
||||
Two consequences worth keeping in mind:
|
||||
|
||||
- **Widening a window makes the limit stricter, not looser** — the same
|
||||
budget now has to cover more frames. Raise the budget instead.
|
||||
- The **byte budget is checked before the RPC budget**, so lifting
|
||||
`Rpcs` alone leaves circuits blocked on `Bytes`. Both have to move.
|
||||
|
||||
Enforcement is entirely client-local: each client reads its own
|
||||
gameconfig and polices its own outgoing sends, and nothing validates
|
||||
budgets on receive. Photon's own server-side message-rate and bandwidth
|
||||
caps still apply underneath this and are not affected by these keys.
|
||||
|
||||
## Clamps, and why they exist
|
||||
|
||||
`NormalizeCircuitsV2Limits` clamps admin input. Most of the clamps are
|
||||
crash guards read straight off the disassembly:
|
||||
|
||||
- **`Rpcs` is floored at 1.** The CircuitsV2 debug subscreen renders
|
||||
`RpcRecentMax / Rpcs` with a hardware integer divide, so a budget of 0
|
||||
is a divide-by-zero the moment that panel opens.
|
||||
- **Time windows are capped at 3600 frames.** A window allocates
|
||||
`new int[N]` — one bucket per frame — when circuits start, so an
|
||||
unbounded window is an out-of-memory kill rather than a bigger budget.
|
||||
- **`Chips` is capped at 1,000,000 and floored at 1**, because it sets
|
||||
the size of four list preallocations in the chip-culling manager.
|
||||
- **`InkPerChip` is capped at 30,000 and `CloudValues` at 10,000**,
|
||||
because the ink calculation multiplies them as unchecked ints.
|
||||
- **`Compute` is capped at 10,000,000.** It allocates nothing, but it is
|
||||
the runaway-loop guard, and a value near `int.MaxValue` lets its
|
||||
counters wrap past the check.
|
||||
|
||||
Budgets themselves are capped at 1,000,000,000: the client only ever
|
||||
compares a counter against them, and this leaves 2x headroom under
|
||||
`int.MaxValue`.
|
||||
|
||||
## Operational notes
|
||||
|
||||
- The client builds its rate limiter **once, when circuits start**.
|
||||
Changing these settings does not affect anyone already in a room —
|
||||
players have to rejoin.
|
||||
- Raising `Rpcs` also changes the Maker Pen top bar's network readout,
|
||||
which renders `<RpcRecentMax> / <Rpcs>`; the ratio collapses toward 0
|
||||
and the bar effectively tracks byte usage only.
|
||||
- The keys are additive to the gameconfig array, so older clients that
|
||||
know nothing about CircuitsV2 just ignore them.
|
||||
- Defaults are applied when the settings are read. While the
|
||||
`CircuitsV2LimitsJson` column is empty (no admin has saved the panel),
|
||||
the server always serves the current defaults. Saving the panel
|
||||
stores the full record, so every value sticks from then on, including
|
||||
the ones the admin didn't touch. **Reset** in the panel restores the
|
||||
current defaults.
|
||||
|
||||
## Where this lives
|
||||
|
||||
| Concern | File |
|
||||
| --- | --- |
|
||||
| Settings record, defaults, clamps, key emission | `DorkNet.Server/Services/ServerSettingsService.cs` |
|
||||
| Persisted column | `ServerSettingsEntity.CircuitsV2LimitsJson` |
|
||||
| Admin API | `api/admin/v1/settings/circuits-limits` (GET/POST) |
|
||||
| Admin SPA | `admin-ui/src/pages/Settings.tsx` → CircuitsV2 limits panel |
|
||||
| Served to the client | `api/gameconfigs/v1/all` |
|
||||
| Tests | `DorkNet.Server.Tests/CircuitsV2LimitsTests.cs` |
|
||||
@@ -132,6 +132,74 @@ 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
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user