mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Merge pull request #2 from DorkSquadRR/fix/personal-dorm-blob-recovery
Fix personal dorm blob recovery
This commit is contained in:
@@ -897,12 +897,7 @@ public class RoomsController(
|
|||||||
// owner's DormStateEntity blob so they load the same room state.
|
// owner's DormStateEntity blob so they load the same room state.
|
||||||
if (room.IsDormRoom)
|
if (room.IsDormRoom)
|
||||||
{
|
{
|
||||||
var dormBlobName = await db.DormStates
|
room = await ShapeDormRoomAsync(room);
|
||||||
.Where(d => d.PlayerId == room.CreatorPlayerId)
|
|
||||||
.Select(d => d.CurrentDataBlobName)
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
?? string.Empty;
|
|
||||||
room = CloneWithCreator(room, room.CreatorPlayerId, dormBlobName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var scenes = await db.RoomScenes
|
var scenes = await db.RoomScenes
|
||||||
@@ -939,15 +934,9 @@ public class RoomsController(
|
|||||||
room ??= Synthetic(roomName);
|
room ??= Synthetic(roomName);
|
||||||
if (room.IsDormRoom)
|
if (room.IsDormRoom)
|
||||||
{
|
{
|
||||||
var dormBlobName = await db.DormStates
|
|
||||||
.Where(d => d.PlayerId == room.CreatorPlayerId)
|
|
||||||
.Select(d => d.CurrentDataBlobName)
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
?? string.Empty;
|
|
||||||
// Stamp the wire Name with the requested name so the watch's
|
// Stamp the wire Name with the requested name so the watch's
|
||||||
// name-keyed room cache (OJMCBOKJFOF.NHBPIIGDAJP) can find it.
|
// name-keyed room cache (OJMCBOKJFOF.NHBPIIGDAJP) can find it.
|
||||||
room = CloneWithCreator(room, room.CreatorPlayerId, dormBlobName,
|
room = await ShapeDormRoomAsync(room, DormNameOverride(roomName));
|
||||||
overrideName: DormNameOverride(roomName));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var scenes = await db.RoomScenes
|
var scenes = await db.RoomScenes
|
||||||
@@ -997,15 +986,9 @@ public class RoomsController(
|
|||||||
room ??= Synthetic(name);
|
room ??= Synthetic(name);
|
||||||
if (room.IsDormRoom)
|
if (room.IsDormRoom)
|
||||||
{
|
{
|
||||||
var dormBlobName = await db.DormStates
|
|
||||||
.Where(d => d.PlayerId == room.CreatorPlayerId)
|
|
||||||
.Select(d => d.CurrentDataBlobName)
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
?? string.Empty;
|
|
||||||
// Stamp the wire Name with the requested name so the watch's
|
// Stamp the wire Name with the requested name so the watch's
|
||||||
// name-keyed room cache (OJMCBOKJFOF.NHBPIIGDAJP) finds it.
|
// name-keyed room cache (OJMCBOKJFOF.NHBPIIGDAJP) finds it.
|
||||||
room = CloneWithCreator(room, room.CreatorPlayerId, dormBlobName,
|
room = await ShapeDormRoomAsync(room, DormNameOverride(name));
|
||||||
overrideName: DormNameOverride(name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var scenes = await db.RoomScenes
|
var scenes = await db.RoomScenes
|
||||||
@@ -1028,12 +1011,7 @@ public class RoomsController(
|
|||||||
|
|
||||||
if (room.IsDormRoom)
|
if (room.IsDormRoom)
|
||||||
{
|
{
|
||||||
var dormBlobName = await db.DormStates
|
room = await ShapeDormRoomAsync(room);
|
||||||
.Where(d => d.PlayerId == room.CreatorPlayerId)
|
|
||||||
.Select(d => d.CurrentDataBlobName)
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
?? string.Empty;
|
|
||||||
room = CloneWithCreator(room, room.CreatorPlayerId, dormBlobName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var scenes = await db.RoomScenes
|
var scenes = await db.RoomScenes
|
||||||
@@ -1065,6 +1043,13 @@ public class RoomsController(
|
|||||||
? requestedName
|
? requestedName
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
private async Task<RoomEntity> ShapeDormRoomAsync(RoomEntity room, string? overrideName = null)
|
||||||
|
{
|
||||||
|
var dormBlobName = await rooms.ResolveDormDataBlobNameAsync(room.CreatorPlayerId, room.Id);
|
||||||
|
var dormName = overrideName ?? await rooms.BuildPersonalDormDisplayNameAsync(room.CreatorPlayerId);
|
||||||
|
return CloneWithCreator(room, room.CreatorPlayerId, dormBlobName, dormName);
|
||||||
|
}
|
||||||
|
|
||||||
public static RoomEntity CloneWithCreator(
|
public static RoomEntity CloneWithCreator(
|
||||||
RoomEntity src, long creatorId, string? overrideBlobName = null,
|
RoomEntity src, long creatorId, string? overrideBlobName = null,
|
||||||
string? overrideName = null) => new()
|
string? overrideName = null) => new()
|
||||||
@@ -1165,11 +1150,13 @@ public class RoomsController(
|
|||||||
// no DeserializeRoomDataBlobCoroutine, the master init
|
// no DeserializeRoomDataBlobCoroutine, the master init
|
||||||
// proceeds with default-zero permissions.
|
// proceeds with default-zero permissions.
|
||||||
//
|
//
|
||||||
// • Dorm / customisable rooms: DataBlobName is either the
|
// • Saved dorm / customisable rooms: DataBlobName is the
|
||||||
// latest uploaded blob name (RoomEntity.CurrentDataBlobName,
|
// latest uploaded blob name (RoomEntity.CurrentDataBlobName,
|
||||||
// written by StorageController on /upload) or, before any
|
// written by StorageController on /upload). Unsaved dorms
|
||||||
// save, "room_<id>_v1.dat" — which the catch-all serves
|
// deliberately use "" so the baked DormRoom scene loads
|
||||||
// from RoomDataBlobService (the all-perms default blob).
|
// without deserializing the synthetic all-perms blob. The
|
||||||
|
// role-only default blob made December wait forever on
|
||||||
|
// StandaloneShapeContainer big-data during first-dorm entry.
|
||||||
// • AG-Original rooms: DataBlobName="" → completed-promise
|
// • AG-Original rooms: DataBlobName="" → completed-promise
|
||||||
// short-circuit → no download → no leaked stale-blob
|
// short-circuit → no download → no leaked stale-blob
|
||||||
// behaviour → master flow proceeds → spawn fires. Maker
|
// behaviour → master flow proceeds → spawn fires. Maker
|
||||||
@@ -1187,6 +1174,7 @@ public class RoomsController(
|
|||||||
var customisable = room.IsDormRoom || room.CreatorPlayerId != 1 || editableRro;
|
var customisable = room.IsDormRoom || room.CreatorPlayerId != 1 || editableRro;
|
||||||
var dataBlobName = !string.IsNullOrEmpty(room.CurrentDataBlobName)
|
var dataBlobName = !string.IsNullOrEmpty(room.CurrentDataBlobName)
|
||||||
? room.CurrentDataBlobName
|
? room.CurrentDataBlobName
|
||||||
|
: room.IsDormRoom ? ""
|
||||||
: customisable ? $"room_{room.Id}_v1.dat" : "";
|
: customisable ? $"room_{room.Id}_v1.dat" : "";
|
||||||
|
|
||||||
// Multi-scene rooms (imported via tools/import-room.ps1) carry one
|
// Multi-scene rooms (imported via tools/import-room.ps1) carry one
|
||||||
@@ -1209,6 +1197,7 @@ public class RoomsController(
|
|||||||
DataBlobName = !string.IsNullOrWhiteSpace(overrideDataBlobName) &&
|
DataBlobName = !string.IsNullOrWhiteSpace(overrideDataBlobName) &&
|
||||||
(overrideSceneId == s.OrderIndex || sceneRows.Count == 1)
|
(overrideSceneId == s.OrderIndex || sceneRows.Count == 1)
|
||||||
? overrideDataBlobName
|
? overrideDataBlobName
|
||||||
|
: room.IsDormRoom ? dataBlobName
|
||||||
: s.DataBlobName,
|
: s.DataBlobName,
|
||||||
MaxPlayers = s.MaxPlayers,
|
MaxPlayers = s.MaxPlayers,
|
||||||
CanMatchmakeInto = s.CanMatchmakeInto,
|
CanMatchmakeInto = s.CanMatchmakeInto,
|
||||||
@@ -1303,6 +1292,7 @@ public class RoomsController(
|
|||||||
var customisable = room.IsDormRoom || room.CreatorPlayerId != 1 || editableRro;
|
var customisable = room.IsDormRoom || room.CreatorPlayerId != 1 || editableRro;
|
||||||
var dataBlobName = !string.IsNullOrEmpty(room.CurrentDataBlobName)
|
var dataBlobName = !string.IsNullOrEmpty(room.CurrentDataBlobName)
|
||||||
? room.CurrentDataBlobName
|
? room.CurrentDataBlobName
|
||||||
|
: room.IsDormRoom ? ""
|
||||||
: customisable ? $"room_{room.Id}_v1.dat" : "";
|
: customisable ? $"room_{room.Id}_v1.dat" : "";
|
||||||
var updatedAt = (room.UpdatedAt == default ? DateTime.UtcNow : room.UpdatedAt)
|
var updatedAt = (room.UpdatedAt == default ? DateTime.UtcNow : room.UpdatedAt)
|
||||||
.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||||
@@ -1315,6 +1305,7 @@ public class RoomsController(
|
|||||||
Name = s.Name,
|
Name = s.Name,
|
||||||
DataBlob = !string.IsNullOrWhiteSpace(overrideDataBlobName) && sceneRows.Count == 1
|
DataBlob = !string.IsNullOrWhiteSpace(overrideDataBlobName) && sceneRows.Count == 1
|
||||||
? overrideDataBlobName
|
? overrideDataBlobName
|
||||||
|
: room.IsDormRoom ? dataBlobName
|
||||||
: s.DataBlobName,
|
: s.DataBlobName,
|
||||||
DataSavedAt = (s.DataModifiedAt == default ? DateTime.UtcNow : s.DataModifiedAt)
|
DataSavedAt = (s.DataModifiedAt == default ? DateTime.UtcNow : s.DataModifiedAt)
|
||||||
.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
||||||
|
|||||||
@@ -1166,16 +1166,15 @@ public class GoToController(
|
|||||||
|
|
||||||
if (isDorm && dormOwnerId is long ownerId)
|
if (isDorm && dormOwnerId is long ownerId)
|
||||||
{
|
{
|
||||||
var dormBlob = await db.DormStates.AsNoTracking()
|
var dormBlob = await rooms.ResolveDormDataBlobNameAsync(ownerId, room.Id);
|
||||||
.Where(d => d.PlayerId == ownerId)
|
|
||||||
.Select(d => d.CurrentDataBlobName)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (!string.IsNullOrWhiteSpace(dormBlob)) return dormBlob;
|
if (!string.IsNullOrWhiteSpace(dormBlob)) return dormBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(room.CurrentDataBlobName)) return room.CurrentDataBlobName;
|
if (!string.IsNullOrWhiteSpace(room.CurrentDataBlobName)) return room.CurrentDataBlobName;
|
||||||
|
|
||||||
var customisable = room.IsDormRoom || room.CreatorPlayerId != 1;
|
if (room.IsDormRoom) return string.Empty;
|
||||||
|
|
||||||
|
var customisable = room.CreatorPlayerId != 1;
|
||||||
return customisable ? $"room_{room.Id}_v1.dat" : string.Empty;
|
return customisable ? $"room_{room.Id}_v1.dat" : string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -397,10 +397,7 @@ public class MatchPlayerController(
|
|||||||
|
|
||||||
if (room.IsDormRoom)
|
if (room.IsDormRoom)
|
||||||
{
|
{
|
||||||
var dormBlob = await db.DormStates.AsNoTracking()
|
var dormBlob = await rooms.ResolveDormDataBlobNameAsync(playerId, room.Id);
|
||||||
.Where(d => d.PlayerId == playerId)
|
|
||||||
.Select(d => d.CurrentDataBlobName)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (!string.IsNullOrWhiteSpace(dormBlob)) return dormBlob;
|
if (!string.IsNullOrWhiteSpace(dormBlob)) return dormBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +412,9 @@ public class MatchPlayerController(
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(room.CurrentDataBlobName)) return room.CurrentDataBlobName;
|
if (!string.IsNullOrWhiteSpace(room.CurrentDataBlobName)) return room.CurrentDataBlobName;
|
||||||
|
|
||||||
var customisable = room.IsDormRoom || room.CreatorPlayerId != 1;
|
if (room.IsDormRoom) return string.Empty;
|
||||||
|
|
||||||
|
var customisable = room.CreatorPlayerId != 1;
|
||||||
return customisable ? $"room_{room.Id}_v1.dat" : string.Empty;
|
return customisable ? $"room_{room.Id}_v1.dat" : string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -450,15 +450,17 @@ public class RoomService(DorkNetDbContext db)
|
|||||||
// RoomsController.Details substitutes into the dorm response so
|
// RoomsController.Details substitutes into the dorm response so
|
||||||
// each player loads THEIR save instead of whoever-saved-last's).
|
// each player loads THEIR save instead of whoever-saved-last's).
|
||||||
// Both are idempotent: we no-op if either side already exists,
|
// Both are idempotent: we no-op if either side already exists,
|
||||||
// and we always end with both rows present so the watch's
|
// and we always end with both rows present. Empty CurrentDataBlobName
|
||||||
// GetRoomData → /room/{blobName} flow has a deterministic
|
// means "no saved dorm yet"; the details/goto paths keep the blob
|
||||||
// CurrentDataBlobName to fetch (empty string until the player
|
// name empty so the watch uses the baked DormRoom scene.
|
||||||
// saves; CdnController.Serve treats empty as "return the
|
|
||||||
// default dorm bytes").
|
|
||||||
const string DormLocationId = "76d98498-60a1-430c-ab76-b54a29b7a163";
|
const string DormLocationId = "76d98498-60a1-430c-ab76-b54a29b7a163";
|
||||||
|
|
||||||
var existing = await db.Rooms
|
var existing = await db.Rooms
|
||||||
.FirstOrDefaultAsync(r => r.IsDormRoom && r.CreatorPlayerId == playerId);
|
.Where(r => r.IsDormRoom && r.CreatorPlayerId == playerId)
|
||||||
|
.OrderByDescending(r => r.CurrentDataBlobName != "")
|
||||||
|
.ThenByDescending(r => r.UpdatedAt)
|
||||||
|
.ThenByDescending(r => r.Id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
var dormStateExists = await db.DormStates
|
var dormStateExists = await db.DormStates
|
||||||
.AnyAsync(d => d.PlayerId == playerId);
|
.AnyAsync(d => d.PlayerId == playerId);
|
||||||
|
|
||||||
@@ -470,15 +472,24 @@ public class RoomService(DorkNetDbContext db)
|
|||||||
// infinite-reloading. Every dorm renders the same canonical scene, so
|
// infinite-reloading. Every dorm renders the same canonical scene, so
|
||||||
// force any non-canonical dorm row (and its saved RoomScene rows) back
|
// force any non-canonical dorm row (and its saved RoomScene rows) back
|
||||||
// to it. Idempotent — runs on the next goto/heartbeat for that player.
|
// to it. Idempotent — runs on the next goto/heartbeat for that player.
|
||||||
|
var existingChanged = false;
|
||||||
if (existing is not null && existing.LocationReplicationId != DormLocationId)
|
if (existing is not null && existing.LocationReplicationId != DormLocationId)
|
||||||
{
|
{
|
||||||
existing.LocationReplicationId = DormLocationId;
|
existing.LocationReplicationId = DormLocationId;
|
||||||
await db.RoomScenes
|
await db.RoomScenes
|
||||||
.Where(s => s.RoomId == existing.Id && s.RoomSceneLocationId != DormLocationId)
|
.Where(s => s.RoomId == existing.Id && s.RoomSceneLocationId != DormLocationId)
|
||||||
.ExecuteUpdateAsync(u => u.SetProperty(s => s.RoomSceneLocationId, DormLocationId));
|
.ExecuteUpdateAsync(u => u.SetProperty(s => s.RoomSceneLocationId, DormLocationId));
|
||||||
await db.SaveChangesAsync();
|
existingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existing is not null && ShouldRepairDormName(existing.Name, playerId))
|
||||||
|
{
|
||||||
|
existing.Name = await BuildUniquePersonalDormNameAsync(playerId, existing.Id);
|
||||||
|
existingChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingChanged) await db.SaveChangesAsync();
|
||||||
|
|
||||||
if (existing is not null && dormStateExists)
|
if (existing is not null && dormStateExists)
|
||||||
return existing;
|
return existing;
|
||||||
|
|
||||||
@@ -520,11 +531,9 @@ public class RoomService(DorkNetDbContext db)
|
|||||||
{
|
{
|
||||||
PlayerId = playerId,
|
PlayerId = playerId,
|
||||||
// Empty until the player saves their dorm via Maker
|
// Empty until the player saves their dorm via Maker
|
||||||
// Pen. CdnController.Serve sees the empty blob name in
|
// Pen. Details/goto emit an empty blob name so the
|
||||||
// the Details response and serves
|
// watch short-circuits persisted-room download and boots
|
||||||
// RoomDataBlobService.GetDefaultBlob() back to the
|
// into the stock baked dorm scene.
|
||||||
// watch — boots into the stock dorm scene without a
|
|
||||||
// 500.
|
|
||||||
CurrentDataBlobName = string.Empty,
|
CurrentDataBlobName = string.Empty,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -564,6 +573,160 @@ public class RoomService(DorkNetDbContext db)
|
|||||||
// EnsurePersonalDorm throws we still backfilled the rest.)
|
// EnsurePersonalDorm throws we still backfilled the rest.)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve the saved room-data blob for a personal dorm and repair stale
|
||||||
|
/// pointers left by older builds. Some existing dorm rows only have the
|
||||||
|
/// real save in RoomDataBlobs or RoomScenes while DormStates/Rooms still
|
||||||
|
/// point at the synthetic room_{id}_v1.dat fallback.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> ResolveDormDataBlobNameAsync(long playerId, long dormRoomId)
|
||||||
|
{
|
||||||
|
var dormState = await db.DormStates
|
||||||
|
.FirstOrDefaultAsync(d => d.PlayerId == playerId);
|
||||||
|
if (await IsUsableDormBlobNameAsync(dormState?.CurrentDataBlobName, dormRoomId))
|
||||||
|
return dormState!.CurrentDataBlobName;
|
||||||
|
|
||||||
|
var room = await db.Rooms
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == dormRoomId && r.IsDormRoom);
|
||||||
|
if (await IsUsableDormBlobNameAsync(room?.CurrentDataBlobName, dormRoomId))
|
||||||
|
{
|
||||||
|
await RepairDormCurrentBlobAsync(playerId, dormRoomId, room!.CurrentDataBlobName);
|
||||||
|
return room.CurrentDataBlobName;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entrySceneBlob = await db.RoomScenes.AsNoTracking()
|
||||||
|
.Where(s => s.RoomId == dormRoomId && s.OrderIndex == 0)
|
||||||
|
.Select(s => s.DataBlobName)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (await IsUsableDormBlobNameAsync(entrySceneBlob, dormRoomId))
|
||||||
|
{
|
||||||
|
await RepairDormCurrentBlobAsync(playerId, dormRoomId, entrySceneBlob!);
|
||||||
|
return entrySceneBlob!;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blobPrefix = $"dorm_p{playerId}_v";
|
||||||
|
var latestDormBlob = await db.RoomDataBlobs.AsNoTracking()
|
||||||
|
.Where(b => b.RoomId == dormRoomId
|
||||||
|
&& b.UploadedByPlayerId == playerId
|
||||||
|
&& b.BlobName.StartsWith(blobPrefix))
|
||||||
|
.OrderByDescending(b => b.UploadedAt)
|
||||||
|
.ThenByDescending(b => b.Id)
|
||||||
|
.Select(b => b.BlobName)
|
||||||
|
.FirstOrDefaultAsync()
|
||||||
|
?? await db.RoomDataBlobs.AsNoTracking()
|
||||||
|
.Where(b => b.UploadedByPlayerId == playerId
|
||||||
|
&& b.BlobName.StartsWith(blobPrefix))
|
||||||
|
.OrderByDescending(b => b.UploadedAt)
|
||||||
|
.ThenByDescending(b => b.Id)
|
||||||
|
.Select(b => b.BlobName)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(latestDormBlob))
|
||||||
|
{
|
||||||
|
await RepairDormCurrentBlobAsync(playerId, dormRoomId, latestDormBlob);
|
||||||
|
return latestDormBlob;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsUsableDormBlobNameAsync(string? blobName, long dormRoomId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(blobName)) return false;
|
||||||
|
|
||||||
|
if (!blobName.StartsWith($"room_{dormRoomId}_v", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return await db.RoomDataBlobs.AsNoTracking().AnyAsync(b => b.BlobName == blobName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RepairDormCurrentBlobAsync(long playerId, long dormRoomId, string blobName)
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
|
||||||
|
var dormState = await db.DormStates.FirstOrDefaultAsync(d => d.PlayerId == playerId);
|
||||||
|
if (dormState is null)
|
||||||
|
{
|
||||||
|
dormState = new DormStateEntity { PlayerId = playerId };
|
||||||
|
db.DormStates.Add(dormState);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(dormState.CurrentDataBlobName, blobName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
dormState.CurrentDataBlobName = blobName;
|
||||||
|
dormState.UpdatedAt = DateTime.UtcNow;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == dormRoomId && r.IsDormRoom);
|
||||||
|
if (room is not null && !string.Equals(room.CurrentDataBlobName, blobName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
room.CurrentDataBlobName = blobName;
|
||||||
|
room.UpdatedAt = DateTime.UtcNow;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entryScene = await db.RoomScenes
|
||||||
|
.FirstOrDefaultAsync(s => s.RoomId == dormRoomId && s.OrderIndex == 0);
|
||||||
|
if (entryScene is not null && !string.Equals(entryScene.DataBlobName, blobName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
entryScene.DataBlobName = blobName;
|
||||||
|
entryScene.DataModifiedAt = DateTime.UtcNow;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> BuildUniquePersonalDormNameAsync(long playerId, long roomId)
|
||||||
|
{
|
||||||
|
var baseName = await BuildPersonalDormDisplayNameAsync(playerId);
|
||||||
|
var candidate = baseName;
|
||||||
|
var suffix = 2;
|
||||||
|
while (await db.Rooms.AsNoTracking().AnyAsync(r => r.Id != roomId && r.Name == candidate))
|
||||||
|
{
|
||||||
|
var suffixText = $"_{suffix++}";
|
||||||
|
candidate = TrimRoomName(baseName, suffixText.Length) + suffixText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> BuildPersonalDormDisplayNameAsync(long playerId)
|
||||||
|
{
|
||||||
|
var player = await db.Players.AsNoTracking()
|
||||||
|
.Where(p => p.Id == playerId)
|
||||||
|
.Select(p => new { p.DisplayName, p.Username })
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
var rawName = !string.IsNullOrWhiteSpace(player?.DisplayName)
|
||||||
|
? player!.DisplayName
|
||||||
|
: !string.IsNullOrWhiteSpace(player?.Username)
|
||||||
|
? player!.Username
|
||||||
|
: $"Player{playerId}";
|
||||||
|
var cleaned = CleanRoomNameStem(rawName);
|
||||||
|
return TrimRoomName($"{cleaned}_dorm");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldRepairDormName(string? name, long playerId) =>
|
||||||
|
string.IsNullOrWhiteSpace(name)
|
||||||
|
|| Guid.TryParse(name, out _)
|
||||||
|
|| string.Equals(name, $"Dorm_{playerId}", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static string CleanRoomNameStem(string raw)
|
||||||
|
{
|
||||||
|
var cleaned = new string(raw
|
||||||
|
.Select(ch => char.IsLetterOrDigit(ch) || ch is '_' or '-' ? ch : '_')
|
||||||
|
.ToArray()).Trim('_');
|
||||||
|
return string.IsNullOrWhiteSpace(cleaned) ? "Player" : cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TrimRoomName(string name, int reservedSuffixLength = 0)
|
||||||
|
{
|
||||||
|
var max = Math.Max(1, 128 - reservedSuffixLength);
|
||||||
|
return name.Length <= max ? name : name[..max];
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hot-list query — drives `api/rooms/v1/hot?roomScoreType=...&tags=...`.
|
/// Hot-list query — drives `api/rooms/v1/hot?roomScoreType=...&tags=...`.
|
||||||
/// Filters by tag prefix-match and orders by HotScore desc.
|
/// Filters by tag prefix-match and orders by HotScore desc.
|
||||||
|
|||||||
Reference in New Issue
Block a user