mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
fix: room-level DataBlob must be the blob sub-room 0 actually loads
Regression from adding the room-level DataBlob key: it was filled from the
per-ROOM name while sub-room 0 resolves its own, override included. A dorm's
data is per-PLAYER (dorm_p{id}_v*.dat) and reaches the builder as that
override, so the room level advertised a room_{id}_* file that was never
written. The client fetched it and rejected 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 payload that disagrees with itself.
Both now come from one resolved value, so whichever key the client reads
first it gets the same file. The no-scenes fallback sub-room honours the
override too, instead of falling back to the per-room name.
Adds the invariant as a test: room-level DataBlob == SubRooms[0].DataBlob.
The DTO parity tests could not have caught this — the key was present and
correctly named, it just pointed somewhere else.
This commit is contained in:
@@ -91,6 +91,65 @@ 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
|
||||
|
||||
@@ -1955,6 +1955,20 @@ public class RoomsController(
|
||||
var updatedAt = (room.UpdatedAt == default ? DateTime.UtcNow : room.UpdatedAt)
|
||||
.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
|
||||
// The blob the room actually loads from: sub-room 0's, override and
|
||||
// all. `dataBlobName` above is the per-ROOM name, which is the wrong
|
||||
// answer whenever a caller passes an override — a dorm's data is
|
||||
// per-PLAYER (dorm_p{id}_v*.dat) and arrives that way.
|
||||
var primarySubRoom = sceneRows is { Count: > 0 }
|
||||
? sceneRows.FirstOrDefault(s => s.OrderIndex == 0) ?? sceneRows[0]
|
||||
: null;
|
||||
var primarySubRoomDataBlob =
|
||||
!string.IsNullOrWhiteSpace(overrideDataBlobName) && (sceneRows?.Count ?? 0) <= 1
|
||||
? overrideDataBlobName!
|
||||
: primarySubRoom is not null
|
||||
? SceneOrSyntheticDataBlobName(room, primarySubRoom.DataBlobName, dataBlobName)
|
||||
: dataBlobName;
|
||||
|
||||
object[] subRooms = sceneRows is { Count: > 0 }
|
||||
? sceneRows.Select(s =>
|
||||
{
|
||||
@@ -2052,7 +2066,10 @@ public class RoomsController(
|
||||
SubRoomUnityAssetId = string.Empty,
|
||||
UnityAsset = string.Empty,
|
||||
UnityAssetHash = string.Empty,
|
||||
DataBlob = dataBlobName,
|
||||
// Same rule as the populated branch: honour the override,
|
||||
// so a dorm with no scene rows still points at that
|
||||
// player's own save rather than a per-room name.
|
||||
DataBlob = primarySubRoomDataBlob,
|
||||
DataBlobHash = (string?)null,
|
||||
DataSavedAt = updatedAt,
|
||||
IsSandbox = false,
|
||||
@@ -2141,7 +2158,14 @@ public class RoomsController(
|
||||
// already answered 200 to. Kept last so the additions are visible
|
||||
// against the reader's own list; key ORDER does not matter to a
|
||||
// name-keyed reader, presence does.
|
||||
DataBlob = dataBlobName,
|
||||
// Must be the SAME blob sub-room 0 resolves to, override included.
|
||||
// A dorm's data is per-player (dorm_p{id}_v*.dat) and arrives here
|
||||
// as overrideDataBlobName; the room-level name is per-ROOM
|
||||
// (room_116_*), so reporting that instead hands the client another
|
||||
// player's dorm — or one that was never written — and it rejects
|
||||
// the load outright with "the data in the room you are trying to
|
||||
// load is corrupt".
|
||||
DataBlob = subRooms.Length > 0 ? primarySubRoomDataBlob : dataBlobName,
|
||||
DataBlobHash = (string?)null,
|
||||
// The room-level cap is the room's own advertised capacity, which is
|
||||
// what RoomEntity.MaxCapacity exists for and what matchmaking hands
|
||||
|
||||
Reference in New Issue
Block a user