diff --git a/DorkNet.Server.Tests/RoomSave2023Tests.cs b/DorkNet.Server.Tests/RoomSave2023Tests.cs index a05892d..772c78f 100644 --- a/DorkNet.Server.Tests/RoomSave2023Tests.cs +++ b/DorkNet.Server.Tests/RoomSave2023Tests.cs @@ -198,8 +198,22 @@ public sealed class RoomSave2023Tests : IClassFixture var firstCloneId = await CloneAndAssertAsync( roomsClient, roomId, "FirstClone", pathPrefix: "/roomserver"); Assert.NotEqual(roomId, firstCloneId); - await AssertBlobsAsync(firstCloneId, expectEmpty: true, - "first-party template blob must not be copied onto the clone"); + // A clone of a first-party template used to be left with NO blob, on + // the reasoning that its content is baked client geometry. That left + // the copy with nothing to load: the CDN synthesises a ~1.8 KB stub for + // a name it has nothing stored under, and the client fetches exactly + // that through GetRoomData ("/room/{blob}") while copying, then rejects + // it. Clones that carried real content joined fine. + // + // The content now comes across. Sharing the SOURCE's object is still + // wrong, but that is prevented by copying it under the clone's own name + // rather than by having no blob at all — see + // RoomCloneShapeTests.Clone_does_not_reference_the_source_rooms_blob, + // which runs with a real object store. This suite has none, so the + // copy deliberately falls back to keeping the reference and only the + // "has content" half is checkable here. + await AssertBlobsAsync(firstCloneId, expectEmpty: false, + "a clone of a first-party template must carry its content"); // Simulate the player saving MakerPen edits into their clone. var editedBlob = $"room_{firstCloneId}_edit_{Guid.NewGuid():N}.dat"; diff --git a/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs b/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs index 44088b6..775dfb4 100644 --- a/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs +++ b/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs @@ -1460,15 +1460,20 @@ public class RoomsModerationController( // badge, and quest-instance logic can boot the player). Match // RoomService.CloneAsync and tag it "community". TagsCsv = "community", - // First-party RRO templates (AG rooms owned by the system - // account — RecCenter and friends) keep their content in baked - // client geometry; any blob on the row is a MakerPen OVERLAY - // save made against the shared template. A clone must start - // from the pristine baked scene with a fresh (empty) blob, not - // inherit that overlay. Scoped to system-owned templates so - // copying a USER's RRO-derived room (IsAGRoom inherited) still - // carries their MakerPen edits along. - CurrentDataBlobName = IsFirstPartyTemplate(source) ? string.Empty : source.CurrentDataBlobName, + // Carry the source's blob NAME here; GiveCloneItsOwnBlobsAsync + // below replaces it with a copy under the clone's own name, so + // nothing ends up shared. + // + // First-party RRO templates (RecCenter and friends) used to be + // emptied here on the reasoning that their content is baked client + // geometry and any blob is just a MakerPen overlay. That left the + // clone with no data at all: the CDN synthesises a ~1.8 KB stub for + // a name it has nothing stored under, the client fetches it through + // GetRoomData ("/room/{blob}") as part of the copy, and rejects it. + // A clone that kept real content joined fine. Since the blob is now + // copied rather than referenced, the original worry — inheriting an + // overlay on a SHARED template — no longer applies. + CurrentDataBlobName = source.CurrentDataBlobName, }; db.Rooms.Add(clone); await db.SaveChangesAsync(); @@ -1493,9 +1498,9 @@ public class RoomsModerationController( OrderIndex = s.OrderIndex, Name = s.Name, RoomSceneLocationId = s.RoomSceneLocationId, - // Fresh blob when cloning a first-party template — see - // CurrentDataBlobName above. - DataBlobName = IsFirstPartyTemplate(source) ? string.Empty : s.DataBlobName, + // See CurrentDataBlobName above: copied under the clone's own + // name by GiveCloneItsOwnBlobsAsync, never shared. + DataBlobName = s.DataBlobName, StudioSubRoomDataSaveId = s.StudioSubRoomDataSaveId, StudioUnityAssetId = s.StudioUnityAssetId, StudioAssetBundleNamesCsv = s.StudioAssetBundleNamesCsv, @@ -1661,11 +1666,6 @@ public class RoomsModerationController( ? RoomService.SyntheticDefaultRoomDataBlobName(roomId) : $"room_{roomId}_dorknet_v8_sub{orderIndex}.dat"; - /// A seeded first-party RRO template (RecCenter, the games): - /// AG-flagged AND owned by the system account. User clones inherit the - /// AG flag but are owned by the player, so they don't match. - private static bool IsFirstPartyTemplate(RoomEntity room) - => room.IsAGRoom && room.CreatorPlayerId == PlayerService.SystemAccountId; private async Task ReadCloneNameAsync() {