feat: Implement base room management; add API endpoints for retrieving and setting curated base rooms

This commit is contained in:
Alexa
2026-07-13 00:40:35 +03:00
parent 2795d2ed7d
commit 2c749c2677
7 changed files with 291 additions and 4 deletions
@@ -293,14 +293,30 @@ public class RoomsController(
=> Ok((await rooms.HotAsync(null)).Select(RoomService.ToWireRoom).ToList());
/// <summary>
/// `Rooms.GetBaseRooms` — list of "base" rooms used by Rec Room Originals
/// to seed the room creation UI. Returns the same set as #recroomoriginal.
/// `Rooms.GetBaseRooms` — the room-creation "base room" picker. A fixed,
/// admin-curated list of room NAMES (server setting
/// <c>BaseRoomNamesJson</c>, default <see
/// cref="ServerSettingsService.DefaultBaseRoomNames"/>), resolved to rooms
/// in the configured order. Names are looked up via
/// <see cref="RoomService.GetByNameAsync"/>, which also finds
/// HiddenFromBrowse rooms like MakerRoom. Edit the list from the admin SPA
/// (<c>api/admin/v1/settings/base-rooms</c>).
/// </summary>
[HttpGet("api/rooms/v2/baserooms")]
[HttpGet("api/rooms/v3/baserooms")]
[HttpGet("rooms/base")]
public async Task<IActionResult> BaseRooms()
=> Ok((await rooms.HotAsync("#recroomoriginal")).Select(RoomService.ToWireRoom).ToList());
{
var names = await serverSettings.GetBaseRoomNamesAsync();
var result = new List<object>();
foreach (var name in names)
{
var room = await rooms.GetByNameAsync(name);
if (room is not null)
result.Add(RoomService.ToWireRoom(room));
}
return Ok(result);
}
/// <summary>
/// `Rooms.GetFilters` — pinned/popular tag chips shown above the room
@@ -984,8 +984,20 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
newName = $"{baseName}{suffix}";
}
// Assign an explicit user-room id (> 1000), the same way
// RoomService.CloneAsync does. Relying on DB auto-increment is unsafe:
// seeded rooms were inserted with manual ids (100..1000) without
// advancing the Postgres identity sequence, so auto-increment collides
// with a seeded id → PK violation → the clone POST 500s → the client
// reports "Failed to clone room" and boots the player to their dorm.
// (SQLite in tests tracks max(id), which is why the probe passed.)
var nextId = await db.Rooms
.Where(r => r.Id > 1000)
.MaxAsync(r => (long?)r.Id) ?? 1000L;
var clone = new RoomEntity
{
Id = nextId + 1,
Name = newName,
Description = source.Description,
CreatorPlayerId = Me,
@@ -1015,7 +1027,12 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
IsStudioRoom = source.IsStudioRoom,
IsRoomLinkedToRecRoomStudio = source.IsRoomLinkedToRecRoomStudio,
StudioSessionId = source.StudioSessionId,
TagsCsv = source.TagsCsv,
// A user clone is community content — NOT an RRO. Copying the
// source's tags verbatim would keep "recroomoriginal,quest,…" on
// the clone, making the client treat it as a quest room (wrong
// badge, and quest-instance logic can boot the player). Match
// RoomService.CloneAsync and tag it "community".
TagsCsv = "community",
CurrentDataBlobName = source.CurrentDataBlobName,
};
db.Rooms.Add(clone);
@@ -1810,6 +1810,32 @@ public class AdminController(
return Ok(doors);
}
/// <summary>GET <c>api/admin/v1/settings/base-rooms</c> — the curated
/// room-creation base-room list (ordered room names) plus the catalogue of
/// Rec Room Original rooms the SPA can offer as pickable candidates.</summary>
[HttpGet("settings/base-rooms")]
public async Task<ActionResult> GetBaseRooms()
{
var names = await serverSettings.GetBaseRoomNamesAsync();
var available = await db.Rooms
.Where(r => EF.Functions.Like(r.TagsCsv, "%recroomoriginal%"))
.OrderBy(r => r.Name)
.Select(r => new { r.Name, r.ImageName, r.TagsCsv })
.ToListAsync();
return Ok(new { Names = names, Available = available });
}
public sealed record BaseRoomsRequest(List<string>? Names);
[HttpPost("settings/base-rooms")]
public async Task<ActionResult> SetBaseRooms([FromBody] BaseRoomsRequest body)
{
var names = await serverSettings.SetBaseRoomNamesAsync(body.Names ?? []);
await LogAsync("base_rooms_updated", "system", 0, string.Join(",", names));
await db.SaveChangesAsync();
return Ok(new { Names = names });
}
[HttpGet("settings/gameconfigs")]
public async Task<ActionResult> GetDiscoveredGameConfigs()
=> Ok(await serverSettings.GetDiscoveredGameConfigsAsync());