using Google.Protobuf; using RecRoom.Protobuf2020; namespace DorkNet.Server.Services; /// /// Round-trips an uploaded .room protobuf blob through the /// 2020.03.06 client's full PersistedRoomData schema (generated /// at build time from Protos/recroom_2020.proto, which /// tools/gen-proto-from-decompiled.py produces by scanning /// Cpp2IL_CS/.../RecRoom/Protobuf/*.cs). /// /// Why: newer Rec Room clients sometimes serialize PersistedRoomData /// with redundant default-zero fields, packed/unpacked variants the /// 2020 reader doesn't tolerate, or other non-canonical encodings. /// The 2020 watch's stricter Google.Protobuf.MessageParser /// then throws "Error attempting to parse room data stream" on /// download. Re-serialising via the 2020 schema strips redundant /// encodings and emits canonical proto3 wire format that the watch /// accepts. /// /// On parse failure we hand the original bytes back unchanged — better /// to upload a blob that MAY work than to drop the user's content. /// Callers (the admin import endpoint) log the outcome. /// public sealed class RoomBlobNormalizerService(ILogger logger) { public sealed record Result(byte[] Bytes, bool Normalized, int InputBytes, int OutputBytes, string? Error); public Result Normalize(byte[] input) { if (input is null || input.Length == 0) return new Result(Array.Empty(), false, 0, 0, "empty_input"); try { // Parse with the FULL 2020 schema. proto3 silently keeps // unknown fields, so even if the input has fields the // generated 2020 schema doesn't know about, they survive // the round-trip. var msg = PersistedRoomData.Parser.ParseFrom(input); var output = msg.ToByteArray(); logger.LogInformation( "[normalize] parse OK, in={In:N0} bytes, out={Out:N0} bytes ({Pct:F2}%)", input.Length, output.Length, (double)output.Length / input.Length * 100); return new Result(output, true, input.Length, output.Length, null); } catch (InvalidProtocolBufferException ex) { logger.LogWarning( "[normalize] parse FAIL ({Bytes:N0} bytes): {Message} — passing original bytes through", input.Length, ex.Message); return new Result(input, false, input.Length, input.Length, ex.Message); } catch (Exception ex) { logger.LogWarning(ex, "[normalize] unexpected exception ({Bytes:N0} bytes) — passing original bytes through", input.Length); return new Result(input, false, input.Length, input.Length, ex.Message); } } }