feat: Implement room blob version clamp toggle; add settings endpoint and UI support for managing version clamping of imported rooms

This commit is contained in:
Alexa
2026-07-16 23:34:55 +03:00
parent 618bcd9203
commit e93bbb14cb
9 changed files with 353 additions and 3 deletions
+69 -1
View File
@@ -109,7 +109,12 @@ public class RoomDataBlobService
pos += 8;
break;
case 2:
pos += checked((int)ReadVarint(input, ref pos));
// NOTE: read the length into a local first. `pos += ReadVarint(ref pos)`
// adds the payload length to the PRE-length-varint position (C# evaluates
// the left operand of += before the right), landing the skip short by
// the size of the length varint and desyncing the whole walk.
var length = checked((int)ReadVarint(input, ref pos));
pos += length;
break;
case 5:
pos += 4;
@@ -121,6 +126,69 @@ public class RoomDataBlobService
if (pos > input.Length) throw new EndOfStreamException("Unexpected end of protobuf field.");
}
/// <summary>Clamp the two top-level version varints of a
/// <c>PersistedRoomData</c> blob to the newest values the March-2023
/// client knows: field 1 (<c>DEPRECATED_RoomPersistenceVersion</c>,
/// max 38) and field 30 (<c>PersistedRoomVersion</c>, 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.</summary>
public static (byte[] Bytes, bool Changed) ClampVersionsFor2023(byte[] input)
{
try
{
using var output = new MemoryStream(input.Length);
var pos = 0;
var changed = false;
while (pos < input.Length)
{
var fieldStart = pos;
var tag = ReadVarint(input, ref pos);
var fieldNumber = (int)(tag >> 3);
var wireType = (int)(tag & 0x07);
if (wireType == 0 && fieldNumber is 1 or 30)
{
var value = ReadVarint(input, ref pos);
var max = fieldNumber == 1
? (ulong)LatestDeprecatedPersistenceVersion
: (ulong)LatestPersistenceVersion;
if (value > max)
{
value = max;
changed = true;
}
WriteVarint(output, tag);
WriteVarint(output, value);
}
else
{
SkipField(input, ref pos, wireType);
output.Write(input, fieldStart, pos - fieldStart);
}
}
return changed ? (output.ToArray(), true) : (input, false);
}
catch
{
// Not parseable as a protobuf message — serve the original
// bytes rather than dropping the download.
return (input, false);
}
}
private static void WriteVarint(Stream output, ulong value)
{
while (value >= 0x80)
{
output.WriteByte((byte)(value | 0x80));
value >>= 7;
}
output.WriteByte((byte)value);
}
public byte[] OverlayAllPermsRoleData(byte[] existingBlob)
{
var msg = PersistedRoomData.Parser.ParseFrom(existingBlob);
@@ -247,6 +247,26 @@ public class ServerSettingsService(DorkNetDbContext db)
return existing;
}
/// <summary>Whether the CDN's PersistedRoomData version clamp for the
/// March-2023 client is bypassed. Off by default (clamp active) — see
/// <see cref="ServerSettingsEntity.RoomBlobVersionClampDisabled"/> and
/// <see cref="RoomDataBlobService.ClampVersionsFor2023"/>.</summary>
public async Task<bool> IsRoomBlobVersionClampDisabledAsync()
{
var row = await db.ServerSettings.AsNoTracking()
.FirstOrDefaultAsync(s => s.Id == RowId);
return row?.RoomBlobVersionClampDisabled ?? false;
}
public async Task<ServerSettingsEntity> SetRoomBlobVersionClampDisabledAsync(bool disabled)
{
var existing = await GetTrackedRowAsync();
existing.RoomBlobVersionClampDisabled = disabled;
existing.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return existing;
}
/// <summary>Live charades slot→list bindings. Empty/unparseable JSON
/// resolves to <see cref="CharadesSlotBindings.Empty"/> (every slot
/// falls back to its built-in seed list).</summary>