From 674a3fc257f59ba3d9609c6921482168a75ed096 Mon Sep 17 00:00:00 2001
From: Alexa <53471801+Alexa-RR@users.noreply.github.com>
Date: Fri, 17 Jul 2026 00:16:54 +0300
Subject: [PATCH] feat: Update room version clamping logic; adjust maximum
persisted room version for March-2023 client compatibility
---
.../RoomBlobVersionClampTests.cs | 12 +++++---
.../Services/RoomDataBlobService.cs | 28 +++++++++++++------
docs/admin.md | 15 ++++++++--
3 files changed, 40 insertions(+), 15 deletions(-)
diff --git a/DorkNet.Server.Tests/RoomBlobVersionClampTests.cs b/DorkNet.Server.Tests/RoomBlobVersionClampTests.cs
index c6ab0ee..51e594a 100644
--- a/DorkNet.Server.Tests/RoomBlobVersionClampTests.cs
+++ b/DorkNet.Server.Tests/RoomBlobVersionClampTests.cs
@@ -49,7 +49,7 @@ public sealed class RoomBlobVersionClampTests
Assert.True(changed);
var msg = PersistedRoomData.Parser.ParseFrom(bytes);
- Assert.Equal(PersistedRoomVersion.LatestVersion, msg.Version);
+ Assert.Equal(RoomDataBlobService.Client2023MaxPersistedRoomVersion, (int)msg.Version);
Assert.Equal(DEPRECATED_RoomPersistenceVersion.LatestRoomPersistenceVersion, msg.DEPRECATEDVersion);
}
@@ -60,17 +60,21 @@ public sealed class RoomBlobVersionClampTests
var (bytes, _) = RoomDataBlobService.ClampVersionsFor2023(input);
- // version=131 is a two-byte varint, 19 is one byte — the output
+ // version=131 is a two-byte varint, 16 is one byte — the output
// shrinks by exactly that and matches the input everywhere else.
Assert.Equal(input.Length - 1, bytes.Length);
- var expected = BuildModernBlob(deprecatedVersion: 38, version: 19);
+ var expected = BuildModernBlob(
+ deprecatedVersion: 38,
+ version: RoomDataBlobService.Client2023MaxPersistedRoomVersion);
Assert.Equal(expected, bytes);
}
[Fact]
public void Blob_already_at_2023_versions_passes_through_unchanged()
{
- var input = BuildModernBlob(deprecatedVersion: 38, version: 19);
+ var input = BuildModernBlob(
+ deprecatedVersion: 38,
+ version: RoomDataBlobService.Client2023MaxPersistedRoomVersion);
var (bytes, changed) = RoomDataBlobService.ClampVersionsFor2023(input);
diff --git a/DorkNet.Server/Services/RoomDataBlobService.cs b/DorkNet.Server/Services/RoomDataBlobService.cs
index 0c00a38..b4e6f6a 100644
--- a/DorkNet.Server/Services/RoomDataBlobService.cs
+++ b/DorkNet.Server/Services/RoomDataBlobService.cs
@@ -126,16 +126,28 @@ public class RoomDataBlobService
if (pos > input.Length) throw new EndOfStreamException("Unexpected end of protobuf field.");
}
+ /// Highest PersistedRoomVersion (field 30) the running
+ /// March-2023 client (build 2023-03-21) actually accepts. NOT the same
+ /// as our proto's (19):
+ /// that enum came from a runtime dump of a NEWER build, and this client
+ /// rejects 19 — its real ceiling is 16 (observed on the .room blobs of
+ /// rooms this client loads without complaint). Clamp to this, not the
+ /// proto's Latest.
+ public const int Client2023MaxPersistedRoomVersion = 16;
+
/// Clamp the two top-level version varints of a
/// PersistedRoomData blob to the newest values the March-2023
/// client knows: field 1 (DEPRECATED_RoomPersistenceVersion,
- /// max 38) and field 30 (PersistedRoomVersion, max 19 =
- /// V19February23BetaRelease). Modern RecNet exports stamp much higher
- /// values (a Sep-2025 save carries version=131) and the client refuses
- /// the whole room with its "update Rec Room to visit this room" gate.
- /// Pure wire-format rewrite — every other byte is preserved verbatim,
- /// so blobs already at or below the 2023 versions round-trip
- /// unchanged. On malformed input the original bytes are returned.
+ /// max 38) and field 30 (PersistedRoomVersion, max
+ /// = 16). Modern RecNet
+ /// exports stamp much higher values (a Sep-2025 save carries
+ /// version=131) and the client refuses the whole room with its
+ /// "update Rec Room to visit this room" gate. Pure wire-format rewrite —
+ /// every other byte is preserved verbatim, so blobs already at or below
+ /// the 2023 versions round-trip unchanged. On malformed input the
+ /// original bytes are returned. NOTE: this only addresses the room-HEADER
+ /// version gate; a room whose CircuitsV2 graph (field 18) is too new is
+ /// rejected regardless and this clamp cannot help it.
public static (byte[] Bytes, bool Changed) ClampVersionsFor2023(byte[] input)
{
try
@@ -154,7 +166,7 @@ public class RoomDataBlobService
var value = ReadVarint(input, ref pos);
var max = fieldNumber == 1
? (ulong)LatestDeprecatedPersistenceVersion
- : (ulong)LatestPersistenceVersion;
+ : (ulong)Client2023MaxPersistedRoomVersion;
if (value > max)
{
value = max;
diff --git a/docs/admin.md b/docs/admin.md
index dbe5c0c..96cadbb 100644
--- a/docs/admin.md
+++ b/docs/admin.md
@@ -117,9 +117,18 @@ immediately.
RecNet zip exports carry room-data blobs whose top-level version varints
(field 1 `DEPRECATED_RoomPersistenceVersion`, field 30
`PersistedRoomVersion`) are far past what the March-2023 client knows —
-a Sep-2025 save stamps `version=131` against the client's maximum of 19
-(`V19February23BetaRelease`) — and the client rejects the whole room with
-its "update Rec Room to visit this room" gate before spawning anything.
+a Sep-2025 save stamps `version=131` against the client's maximum of 16
+(`RoomDataBlobService.Client2023MaxPersistedRoomVersion`; the proto's
+`LatestVersion=19` came from a newer build's dump and this client rejects
+it) — and the client rejects the whole room with its "update Rec Room to
+visit this room" gate before spawning anything.
+
+This clamp only addresses the room-**header** version gate. A room whose
+CircuitsV2 chip graph (`circuit_data`, field 18) was saved in a newer Rec
+Room is rejected by a separate CircuitsV2 version gate regardless — check
+the client's `Player.log`, where the stack frame under "Booting player to
+dorm" names the failing subsystem (`CircuitsV2Manager` = the circuit
+graph, not the header). That case is not fixable by this clamp.
While the clamp is active (the default), the CDN serve path rewrites the
two varints down to the 2023 maxima on the way out for `.room` / `.meta` /