diff --git a/DorkNet.Server/Controllers/API/Inventions/InventionsController.cs b/DorkNet.Server/Controllers/API/Inventions/InventionsController.cs
index bbfba6c..54b5816 100644
--- a/DorkNet.Server/Controllers/API/Inventions/InventionsController.cs
+++ b/DorkNet.Server/Controllers/API/Inventions/InventionsController.cs
@@ -167,13 +167,58 @@ public class InventionsController(
});
}
+ /// GET api/inventions/v1/creatorIds — the distinct
+ /// creator account ids of every invention placed in a room
+ /// (GetCreatorIdsOfInventionsInRoom). The watch uses this to
+ /// resolve which creators it must request invention permissions for
+ /// before spawning a room's invention-based geometry. Returns
+ /// List<int> — empty when the room has no tracked
+ /// inventions.
[HttpGet("api/inventions/v1/creatorIds")]
- public IActionResult CreatorIdsInRoom([FromQuery] long roomId)
+ public async Task CreatorIdsInRoom([FromQuery] long roomId)
{
- // We don't yet track per-room invention placements; return
- // empty list (correct shape: List).
- _ = roomId;
- return Ok(Array.Empty());
+ var inventions = await ResolveRoomInventionsAsync(roomId);
+ var creatorIds = inventions
+ .Select(i => (int)i.CreatorPlayerId)
+ .Distinct()
+ .ToList();
+ return Ok(creatorIds);
+ }
+
+ /// Resolve the inventions that belong to a room. Direct
+ /// matches are inventions whose
+ /// equals the room's
+ /// local id (RR-Originals, dorms, anything created in-place).
+ /// Zip-imported rooms instead carry each invention's ORIGINAL Rec
+ /// Room CreationRoomId, which won't equal our local id and can't be
+ /// reverse-resolved without an OriginalRoomId column; we approximate
+ /// that cluster as the most-frequent non-local CreationRoomId among
+ /// the room creator's inventions. The two sets are unioned so a room
+ /// with both kinds resolves fully.
+ private async Task> ResolveRoomInventionsAsync(long roomId)
+ {
+ var creatorId = await db.Rooms
+ .Where(r => r.Id == roomId)
+ .Select(r => (long?)r.CreatorPlayerId)
+ .FirstOrDefaultAsync();
+
+ long? importedRoomId = null;
+ if (creatorId is long creator)
+ {
+ importedRoomId = await db.Inventions
+ .Where(i => !i.IsDeleted && i.CreatorPlayerId == creator
+ && i.CreationRoomId != null && i.CreationRoomId != roomId)
+ .GroupBy(i => i.CreationRoomId)
+ .OrderByDescending(g => g.Count())
+ .Select(g => g.Key)
+ .FirstOrDefaultAsync();
+ }
+
+ return await db.Inventions
+ .Where(i => !i.IsDeleted
+ && (i.CreationRoomId == roomId
+ || (importedRoomId != null && i.CreationRoomId == importedRoomId)))
+ .ToListAsync();
}
/// GET api/inventions/v1/fulllineageowner —
@@ -225,46 +270,7 @@ public class InventionsController(
[HttpGet("api/inventions/v1/room")]
public async Task ByRoom([FromQuery] long id)
{
- // The watch sends our local room id, but zip-imported inventions
- // carry the ORIGINAL Rec Room room id in CreationRoomId (the
- // importer preserves it so cross-zip references survive). Match
- // by name first to find any candidate original ids, then union
- // the lookup. For dorm/RR-Original rooms (CreatorPlayerId=1,
- // never imported), CreationRoomId equals the local id directly.
- var roomName = await db.Rooms
- .Where(r => r.Id == id)
- .Select(r => r.Name)
- .FirstOrDefaultAsync();
- var inventions = await db.Inventions
- .Where(i => !i.IsDeleted
- && (i.CreationRoomId == id
- // Imported rooms: the invention has CreationRoomId =
- // the original Rec Room id. We can't reverse-resolve
- // without an OriginalRoomId column, so use
- // ReplicationId-based name match: every invention
- // imported alongside this room has CreatorPlayerId
- // matching the room creator AND CreationRoomId set
- // to a non-zero value that isn't an RR-Original.
- // Loose match — accept any invention whose creator
- // is the room creator and CreationRoomId is "the same
- // remote id" for that room. We approximate by
- // looking at the most-frequent CreationRoomId among
- // inventions whose creator matches the room creator
- // and joining on that.
- || (roomName != null && db.Inventions
- .Where(j => !j.IsDeleted
- && j.CreatorPlayerId == db.Rooms
- .Where(r => r.Id == id)
- .Select(r => r.CreatorPlayerId)
- .FirstOrDefault()
- && j.CreationRoomId != null
- && j.CreationRoomId != id)
- .GroupBy(j => j.CreationRoomId)
- .OrderByDescending(g => g.Count())
- .Select(g => g.Key)
- .FirstOrDefault() == i.CreationRoomId)
- ))
- .ToListAsync();
+ var inventions = await ResolveRoomInventionsAsync(id);
return Ok(inventions.Select(ToWire).ToList());
}
diff --git a/DorkNet.Server/Data/url-coverage.md b/DorkNet.Server/Data/url-coverage.md
index 244a82f..07bb0e6 100644
--- a/DorkNet.Server/Data/url-coverage.md
+++ b/DorkNet.Server/Data/url-coverage.md
@@ -140,7 +140,7 @@ Note: counts are approximate because several client URLs (`api/avatar/v3/saved`,
| POST | `api/inventions/v1/batch` | REAL | `InventionsController.Batch` — bulk-fetch inventions by id list, returns multi-row payload |
| GET | `api/inventions/v1/details` | REAL | `InventionsController.Details` — extended invention payload with version/creator metadata |
| GET | `api/inventions/v1/versions` | REAL | `InventionsController.Versions` — list of `InventionVersionEntity` rows for an invention |
-| GET | `api/inventions/v1/creatorIds` | REAL | `InventionsController.CreatorIds` — list of distinct creator player ids over the matching invention set |
+| GET | `api/inventions/v1/creatorIds` | REAL | `InventionsController.CreatorIdsInRoom` — distinct creator account ids of the inventions placed in the room, resolved via `InventionEntity.CreationRoomId` (direct + imported-cluster, shared with `/room`). `List` |
| GET | `api/inventions/v1/tagfilters` | REAL | `InventionsController.TagFilters` — returns the curated tag-filter set (currently empty list, but registered as a real handler) |
| GET | `api/inventions/v1/delete` | REAL | `InventionsController.Delete` — soft-deletes an invention the caller owns |
| GET | `api/inventions/v2/publish` | REAL | `InventionsController.PublishV2` — flips `IsPublished=true`, sets `FirstPublishedAt` |