feat: Enhance room cloning functionality; add support for roomserver routes and blob management for first-party templates

This commit is contained in:
Alexa
2026-07-13 11:51:20 +03:00
parent e4126e38ce
commit 690e77f520
5 changed files with 187 additions and 18 deletions
@@ -3449,6 +3449,7 @@ public class RoomsController(
/// a scene (DataBlobName + location pointer). New scene goes at the
/// end with " Copy" suffix; per-room name uniqueness preserved.</summary>
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/clone")]
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/clone")]
[Authorize]
public async Task<IActionResult> CloneSubRoom(long roomId, long subRoomId)
{
@@ -5,6 +5,7 @@ using System.Text.Json;
using DorkNet.Server.Auth;
using DorkNet.Server.Data;
using DorkNet.Server.Data.Entities;
using DorkNet.Server.Services;
namespace DorkNet.Server.Controllers.API.Rooms.V2;
@@ -965,7 +966,14 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
// handler runs → the client reports "Failed to clone room" and boots the
// player to their dorm. Accept all three content types and read the name
// manually from form / JSON body / query.
// The 2023 in-room "copy room" flow (RecNet.Runtime NLDBPDCNNCF, the
// roomserver client that also sends the subrooms/... mutations) POSTs
// this with the roomserver/ prefix — bare-only routing 404s with an
// empty body, which the client's promise layer surfaces as a
// message-less exception ("Failed to copy room: Exception of type
// '...' was thrown"). Same fix as maxplayers/modify/name.
[HttpPost("rooms/{roomId:long}/clone")]
[HttpPost("roomserver/rooms/{roomId:long}/clone")]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
public async Task<IActionResult> BareClone(long roomId)
{
@@ -1052,7 +1060,15 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
// badge, and quest-instance logic can boot the player). Match
// RoomService.CloneAsync and tag it "community".
TagsCsv = "community",
CurrentDataBlobName = source.CurrentDataBlobName,
// 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,
};
db.Rooms.Add(clone);
await db.SaveChangesAsync();
@@ -1077,7 +1093,9 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
OrderIndex = s.OrderIndex,
Name = s.Name,
RoomSceneLocationId = s.RoomSceneLocationId,
DataBlobName = s.DataBlobName,
// Fresh blob when cloning a first-party template — see
// CurrentDataBlobName above.
DataBlobName = IsFirstPartyTemplate(source) ? string.Empty : s.DataBlobName,
StudioSubRoomDataSaveId = s.StudioSubRoomDataSaveId,
StudioUnityAssetId = s.StudioUnityAssetId,
StudioAssetBundleNamesCsv = s.StudioAssetBundleNamesCsv,
@@ -1103,6 +1121,12 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
// room".
return Ok(RoomsController.BuildRoomServerDetails(clone, clonedScenes));
}
/// <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()
{
// Form: name=... (what the 2023 client sends).