feat: Update room version clamping logic; adjust maximum persisted room version for March-2023 client compatibility

This commit is contained in:
Alexa
2026-07-17 00:16:54 +03:00
parent e93bbb14cb
commit 674a3fc257
3 changed files with 40 additions and 15 deletions
@@ -49,7 +49,7 @@ public sealed class RoomBlobVersionClampTests
Assert.True(changed); Assert.True(changed);
var msg = PersistedRoomData.Parser.ParseFrom(bytes); 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); Assert.Equal(DEPRECATED_RoomPersistenceVersion.LatestRoomPersistenceVersion, msg.DEPRECATEDVersion);
} }
@@ -60,17 +60,21 @@ public sealed class RoomBlobVersionClampTests
var (bytes, _) = RoomDataBlobService.ClampVersionsFor2023(input); 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. // shrinks by exactly that and matches the input everywhere else.
Assert.Equal(input.Length - 1, bytes.Length); 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); Assert.Equal(expected, bytes);
} }
[Fact] [Fact]
public void Blob_already_at_2023_versions_passes_through_unchanged() 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); var (bytes, changed) = RoomDataBlobService.ClampVersionsFor2023(input);
+20 -8
View File
@@ -126,16 +126,28 @@ public class RoomDataBlobService
if (pos > input.Length) throw new EndOfStreamException("Unexpected end of protobuf field."); if (pos > input.Length) throw new EndOfStreamException("Unexpected end of protobuf field.");
} }
/// <summary>Highest <c>PersistedRoomVersion</c> (field 30) the running
/// March-2023 client (build 2023-03-21) actually accepts. NOT the same
/// as our proto's <see cref="PersistedRoomVersion.LatestVersion"/> (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.</summary>
public const int Client2023MaxPersistedRoomVersion = 16;
/// <summary>Clamp the two top-level version varints of a /// <summary>Clamp the two top-level version varints of a
/// <c>PersistedRoomData</c> blob to the newest values the March-2023 /// <c>PersistedRoomData</c> blob to the newest values the March-2023
/// client knows: field 1 (<c>DEPRECATED_RoomPersistenceVersion</c>, /// client knows: field 1 (<c>DEPRECATED_RoomPersistenceVersion</c>,
/// max 38) and field 30 (<c>PersistedRoomVersion</c>, max 19 = /// max 38) and field 30 (<c>PersistedRoomVersion</c>, max
/// V19February23BetaRelease). Modern RecNet exports stamp much higher /// <see cref="Client2023MaxPersistedRoomVersion"/> = 16). Modern RecNet
/// values (a Sep-2025 save carries version=131) and the client refuses /// exports stamp much higher values (a Sep-2025 save carries
/// the whole room with its "update Rec Room to visit this room" gate. /// version=131) and the client refuses the whole room with its
/// Pure wire-format rewrite — every other byte is preserved verbatim, /// "update Rec Room to visit this room" gate. Pure wire-format rewrite —
/// so blobs already at or below the 2023 versions round-trip /// every other byte is preserved verbatim, so blobs already at or below
/// unchanged. On malformed input the original bytes are returned.</summary> /// 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.</summary>
public static (byte[] Bytes, bool Changed) ClampVersionsFor2023(byte[] input) public static (byte[] Bytes, bool Changed) ClampVersionsFor2023(byte[] input)
{ {
try try
@@ -154,7 +166,7 @@ public class RoomDataBlobService
var value = ReadVarint(input, ref pos); var value = ReadVarint(input, ref pos);
var max = fieldNumber == 1 var max = fieldNumber == 1
? (ulong)LatestDeprecatedPersistenceVersion ? (ulong)LatestDeprecatedPersistenceVersion
: (ulong)LatestPersistenceVersion; : (ulong)Client2023MaxPersistedRoomVersion;
if (value > max) if (value > max)
{ {
value = max; value = max;
+12 -3
View File
@@ -117,9 +117,18 @@ immediately.
RecNet zip exports carry room-data blobs whose top-level version varints RecNet zip exports carry room-data blobs whose top-level version varints
(field 1 `DEPRECATED_RoomPersistenceVersion`, field 30 (field 1 `DEPRECATED_RoomPersistenceVersion`, field 30
`PersistedRoomVersion`) are far past what the March-2023 client knows — `PersistedRoomVersion`) are far past what the March-2023 client knows —
a Sep-2025 save stamps `version=131` against the client's maximum of 19 a Sep-2025 save stamps `version=131` against the client's maximum of 16
(`V19February23BetaRelease`) — and the client rejects the whole room with (`RoomDataBlobService.Client2023MaxPersistedRoomVersion`; the proto's
its "update Rec Room to visit this room" gate before spawning anything. `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 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` / two varints down to the 2023 maxima on the way out for `.room` / `.meta` /