fix: a cloned template carries its content instead of starting empty

Cloning a first-party template (RecCenter and friends) deliberately left the
new room with no blob, on the reasoning that its content is baked client
geometry and any blob is just a MakerPen overlay. That left the copy with
nothing to load.

The copy flow is what exposes it. IBEOONPEELF.GetRoomData / GetRoomMetadata
fetch "/room/{blob}" from the CDN as part of copying and throw a
BCPHBJHOIEM-family exception when that does not give them a usable room —
surfacing as the bare "Failed to copy room: Exception of type 'CEMNLBKJABA'"
with no message. For a name nothing is stored under, the CDN synthesises a
~1.8 KB stub: room_1583_dorknet_v8.dat served 1872 bytes where the source's
room_100_v1.dat is 10,930. The clone that DID work carried real content.

Since GiveCloneItsOwnBlobsAsync copies the bytes under the clone's own name,
the original worry — inheriting an overlay on a SHARED template — no longer
applies: nothing is shared either way. IsFirstPartyTemplate had no other
callers and is gone.

The existing assertion that a template clone must have NO blob encoded the
belief this replaces, so it now asserts the clone carries content. The
"doesn't share the source's object" half is covered by
RoomCloneShapeTests.Clone_does_not_reference_the_source_rooms_blob, which runs
with a real object store; this suite has none and deliberately falls back to
keeping the reference.
This commit is contained in:
Alexa
2026-07-28 12:00:15 +03:00
parent ec37f1c790
commit 9f173193a7
2 changed files with 33 additions and 19 deletions
+16 -2
View File
@@ -198,8 +198,22 @@ public sealed class RoomSave2023Tests : IClassFixture<DorkNetServerFactory>
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";
@@ -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";
/// <summary>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.</summary>
private static bool IsFirstPartyTemplate(RoomEntity room)
=> room.IsAGRoom && room.CreatorPlayerId == PlayerService.SystemAccountId;
private async Task<string> ReadCloneNameAsync()
{