mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
fix: stop sending a room-level DataBlob — it broke the dorm
Adding DataBlob/DataBlobHash at room level for reader parity was wrong, and the
dorm is what proved it: rooms came back "the data in the room you are trying to
load is corrupt", and every room loaded fine before the key existed.
Which blob a room loads is decided per INSTANCE and per SUB-ROOM, not per room.
The matchmake response carries it (a dorm's is per-player,
dorm_p{id}_v*.dat) and each sub-room carries its own. A room-scoped answer to
the same question is one the client preferred, and for a dorm it named a file
that was never written.
A registered slot does not oblige us to fill it: Newtonsoft leaves an absent
key at its default, which is the state every room loaded in. The parity test
records both keys as deliberately omitted with the reason, so restoring them
takes a decision rather than a tidy-up.
Also drops the room-level/sub-room DataBlob invariant test added an hour ago —
there is no room-level DataBlob left for it to check.
Note for the clone bug: the captured response now shows the clone carries all
41 keys the reader registers and the client still rejects it, so key parity is
NOT that failure. It is a value the reader refuses, not a missing name.
This commit is contained in:
@@ -38,6 +38,25 @@ public sealed class ClientDtoParityTests : IClassFixture<DorkNetServerFactory>
|
||||
/// <c>POST thread/withmembers</c> and <c>GET thread/{id}</c>.</summary>
|
||||
private const string ChatThreadReader = "EKEFFNBIOHJ";
|
||||
|
||||
/// <summary>Keys the reader registers that we deliberately do NOT send.
|
||||
///
|
||||
/// A registered slot does not oblige us to fill it — Newtonsoft leaves an
|
||||
/// absent key at its default. These two were added for parity and broke
|
||||
/// the dorm: which blob a room loads is decided per INSTANCE (the matchmake
|
||||
/// response carries it, and a dorm's is per-player, dorm_p{id}_v*.dat) and
|
||||
/// per SUB-ROOM. A room-scoped answer to the same question is one the
|
||||
/// client preferred, and for a dorm it named a file that was never written,
|
||||
/// so the room came back "corrupt".
|
||||
///
|
||||
/// Every room loaded fine while these were absent. Their absence is the
|
||||
/// fix, not a gap to close later — do not "restore parity" here without a
|
||||
/// dorm join to prove it.</summary>
|
||||
private static readonly HashSet<(string Reader, string Key)> DeliberatelyOmitted =
|
||||
[
|
||||
(RoomDetailsReader, "DataBlob"),
|
||||
(RoomDetailsReader, "DataBlobHash"),
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public async Task Room_details_carries_exactly_the_keys_the_client_reads()
|
||||
{
|
||||
@@ -91,72 +110,15 @@ public sealed class ClientDtoParityTests : IClassFixture<DorkNetServerFactory>
|
||||
AssertKeyParity(ChatThreadReader, body, "POST thread/withmembers");
|
||||
}
|
||||
|
||||
/// <summary>The room-level DataBlob must be the SAME file sub-room 0 points
|
||||
/// at. The client loads a room from one blob, and if the two disagree it
|
||||
/// fetches a file that was never written and rejects the whole room with
|
||||
/// "the data in the room you are trying to load is corrupt" — which reads
|
||||
/// like a broken save rather than a mismatched payload.
|
||||
///
|
||||
/// A dorm is where this bites: its data is per-PLAYER
|
||||
/// (<c>dorm_p{id}_v*.dat</c>) and reaches the builder as an override, while
|
||||
/// the room-level name is per-ROOM (<c>room_116_*</c>). Reporting the
|
||||
/// room-level one hands the client another player's dorm.</summary>
|
||||
[Fact]
|
||||
public async Task Room_level_data_blob_matches_sub_room_zero()
|
||||
{
|
||||
using var client = RoomsClient();
|
||||
var session = await GameClientSessionFactory.CreateAsync(client, _factory.ApexDomain);
|
||||
client.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", session.AccessToken);
|
||||
|
||||
var roomId = 9_900_000 + Random.Shared.Next(1, 99_999);
|
||||
await using (var scope = _factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
db.Rooms.Add(new RoomEntity
|
||||
{
|
||||
Id = roomId,
|
||||
Name = $"Blob{Guid.NewGuid():N}"[..18],
|
||||
CreatorPlayerId = session.PlayerId,
|
||||
CurrentDataBlobName = $"room_{roomId}_v1.dat",
|
||||
});
|
||||
db.RoomScenes.Add(new RoomSceneEntity
|
||||
{
|
||||
RoomId = roomId,
|
||||
Name = "Home",
|
||||
OrderIndex = 0,
|
||||
DataBlobName = $"room_{roomId}_home_v3.dat",
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
using var response = await client.GetAsync($"/rooms/{roomId}");
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.True(response.IsSuccessStatusCode, $"GET /rooms/{roomId} -> {(int)response.StatusCode}: {body}");
|
||||
|
||||
var details = JsonDocument.Parse(body).RootElement;
|
||||
var roomLevel = details.GetProperty("DataBlob").GetString();
|
||||
var subRoomZero = details.GetProperty("SubRooms").EnumerateArray()
|
||||
.First(e => e.GetProperty("SubRoomId").GetInt64() == 0)
|
||||
.GetProperty("DataBlob").GetString();
|
||||
|
||||
Assert.True(roomLevel == subRoomZero,
|
||||
$"""
|
||||
The room reports one data blob and sub-room 0 reports another, so
|
||||
which file the client loads depends on which key it reads first:
|
||||
|
||||
room level : {roomLevel}
|
||||
sub-room 0 : {subRoomZero}
|
||||
""");
|
||||
}
|
||||
|
||||
/// <summary>Compare one response object's keys against the reader's set.
|
||||
/// Matching is case-insensitive because the readers register three spellings
|
||||
/// per field (Pascal, camel, lower) and accept any of them — casing is the
|
||||
/// one thing they are NOT strict about.</summary>
|
||||
private static void AssertKeyParity(string readerType, string body, string what)
|
||||
{
|
||||
var expected = LoadReaderKeys(readerType);
|
||||
var expected = LoadReaderKeys(readerType)
|
||||
.Where(k => !DeliberatelyOmitted.Contains((readerType, k)))
|
||||
.ToList();
|
||||
var actual = JsonDocument.Parse(body).RootElement
|
||||
.EnumerateObject().Select(p => p.Name)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Reference in New Issue
Block a user