feat: Enhance room cloning functionality; ensure cloned rooms include source scenes for complete structure

This commit is contained in:
Alexa
2026-07-13 00:15:57 +03:00
parent 757308b44d
commit 2795d2ed7d
2 changed files with 48 additions and 9 deletions
@@ -1020,7 +1020,42 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
}; };
db.Rooms.Add(clone); db.Rooms.Add(clone);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return Ok(Services.RoomService.ToWireRoom(clone));
// Copy the source's scenes (subrooms) so the clone is structurally
// complete. Without this the clone has zero subrooms and the client's
// room reader rejects it. Shallow copy — scenes point at the same data
// blobs (copy-on-write happens when the clone is later saved).
var sourceScenes = await db.RoomScenes
.Where(s => s.RoomId == source.Id)
.OrderBy(s => s.OrderIndex)
.ToListAsync();
var clonedScenes = sourceScenes.Select(s => new RoomSceneEntity
{
RoomId = clone.Id,
OrderIndex = s.OrderIndex,
Name = s.Name,
RoomSceneLocationId = s.RoomSceneLocationId,
DataBlobName = s.DataBlobName,
StudioSubRoomDataSaveId = s.StudioSubRoomDataSaveId,
StudioUnityAssetId = s.StudioUnityAssetId,
StudioAssetBundleNamesCsv = s.StudioAssetBundleNamesCsv,
MaxPlayers = s.MaxPlayers,
IsSandbox = s.IsSandbox,
CanMatchmakeInto = s.CanMatchmakeInto,
DataModifiedAt = DateTime.UtcNow,
}).ToList();
if (clonedScenes.Count > 0)
{
db.RoomScenes.AddRange(clonedScenes);
await db.SaveChangesAsync();
}
// The 2023 client's clone (RecNet.Runtime NLDBPDCNNCF.GDHIIAHCBMN)
// deserializes the response as the FULL room-details object
// (FGCPNAACHIK — the same type get-by-id / rename return), NOT the
// slim list-room shape. Returning ToWireRoom here made the strict
// reader fail → "Failed to copy room: Failed to clone room".
return Ok(RoomsController.BuildRoomDetails(clone, clonedScenes));
} }
public sealed class CloneBareRequest { public string? Name { get; set; } } public sealed class CloneBareRequest { public string? Name { get; set; } }
@@ -551,25 +551,29 @@ public class CdnController(
{ {
if (string.IsNullOrWhiteSpace(fileName)) return false; if (string.IsNullOrWhiteSpace(fileName)) return false;
// MakerPen across ALL Rec Room Originals: overlay the permissive
// room-role data (which grants CanUseMakerPen etc.) onto any AG/RRO
// room's data blob. This used to also require an accepted RoomRole,
// which limited MakerPen to hand-role-granted rooms (Stunt Runner,
// Rec Center). Dropping that requirement gives every RRO editing
// rights. Chips are preserved because the overlay only REPLACES the
// blob's RoomRoleData field (RoomDataBlobService.OverlayRroEditable-
// RoleData) — the game-rule circuit data and GameRoleData are left
// intact — so quest rooms keep working, exactly as Stunt Runner does.
if (await db.Rooms.AsNoTracking().AnyAsync(r => if (await db.Rooms.AsNoTracking().AnyAsync(r =>
r.IsAGRoom && r.IsAGRoom && r.CurrentDataBlobName == fileName))
r.CurrentDataBlobName == fileName &&
db.RoomRoles.Any(rr => rr.RoomId == r.Id && rr.Accepted)))
return true; return true;
if (fileName.StartsWith("room_", StringComparison.OrdinalIgnoreCase) && if (fileName.StartsWith("room_", StringComparison.OrdinalIgnoreCase) &&
fileName.EndsWith("_v1.dat", StringComparison.OrdinalIgnoreCase) && fileName.EndsWith("_v1.dat", StringComparison.OrdinalIgnoreCase) &&
long.TryParse(fileName[5..^7], out var roomId) && long.TryParse(fileName[5..^7], out var roomId) &&
await db.Rooms.AsNoTracking().AnyAsync(r => r.Id == roomId && r.IsAGRoom) && await db.Rooms.AsNoTracking().AnyAsync(r => r.Id == roomId && r.IsAGRoom))
await db.RoomRoles.AsNoTracking().AnyAsync(r => r.RoomId == roomId && r.Accepted))
return true; return true;
return await ( return await (
from scene in db.RoomScenes.AsNoTracking() from scene in db.RoomScenes.AsNoTracking()
join room in db.Rooms.AsNoTracking() on scene.RoomId equals room.Id join room in db.Rooms.AsNoTracking() on scene.RoomId equals room.Id
where room.IsAGRoom && where room.IsAGRoom && scene.DataBlobName == fileName
scene.DataBlobName == fileName &&
db.RoomRoles.Any(rr => rr.RoomId == room.Id && rr.Accepted)
select scene.Id select scene.Id
).AnyAsync(); ).AnyAsync();
} }