mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
fix: 2023 room instances, publish, role permissions and co-owner invites
- Room roles: list the creator as Creator (255), not CoOwner (30). The
client takes the local role from the Roles list and only lets a player
edit roles below their own (Creator excepted), so the creator could not
edit Co-Owner and Creator permissions. Enforce the role ladder on the
server (RoomRoleRules), let invitees accept (PUT roles/{me} role=30) and
reject (PUT roles/{me}/invite role=0), send message Types 62/60/61, and
push RoomUpdate after every role change.
- Permissions: persist PUT subrooms/{sub}/permissions cells in a new
RoomRolePermissions table, serve them from photon_access_token (plus a
roomserver/ twin) and push PhotonAccessToken to the sub-room.
- Publish: player-owned rooms are never IsRRO on the wire; the client
hides Publish and every room setting for an RRO.
- Sub-rooms: wire SubRoomId = OrderIndex + 1 (the client will not load a
sub-room with id 0), new rooms always get a named Home sub-room, and a
startup pass repairs rooms without one.
- Instance browser: per-room Redis presence index, live players only,
private instances for the people allowed in, real IsFull.
- Room/sub-room settings: creator or accepted co-owner everywhere.
This commit is contained in:
@@ -1254,8 +1254,10 @@ public class InventionsController(
|
||||
if (!canPush) return Forbid();
|
||||
|
||||
var subRoomId = req.SubRoomId;
|
||||
// Wire SubRoomId (OrderIndex + 1) → the scene's OrderIndex.
|
||||
var sceneOrderIndex = Services.RoomSceneIds.ToOrderIndex(subRoomId);
|
||||
var scene = await db.RoomScenes
|
||||
.FirstOrDefaultAsync(s => s.RoomId == room.Id && s.OrderIndex == subRoomId);
|
||||
.FirstOrDefaultAsync(s => s.RoomId == room.Id && s.OrderIndex == sceneOrderIndex);
|
||||
|
||||
var subRoomDataFilename = Clamp((req.SubRoomData?.Filename ?? string.Empty).Trim(), 128);
|
||||
if (subRoomDataFilename.Length == 0)
|
||||
@@ -1269,7 +1271,7 @@ public class InventionsController(
|
||||
if (subRoomBlob is null)
|
||||
return BadRequest(new { Error = "unknown SubRoomData.Filename" });
|
||||
if (subRoomBlob.RoomId == 0) subRoomBlob.RoomId = room.Id;
|
||||
subRoomBlob.SubRoomId ??= subRoomId;
|
||||
subRoomBlob.SubRoomId ??= sceneOrderIndex;
|
||||
|
||||
var roomDataFilename = Clamp((req.RoomData?.Filename ?? string.Empty).Trim(), 128);
|
||||
if (roomDataFilename.Length > 0)
|
||||
|
||||
@@ -394,7 +394,11 @@ public class PlayerEventsController(
|
||||
if (evt is null) return NotFound();
|
||||
|
||||
var extras = (await LoadExtrasAsync(new[] { evt }))[evt.Id];
|
||||
var subRoomId = extras.SubRoomId is long sub && sub > 0 ? sub : (long?)null;
|
||||
// The event stores the client's wire SubRoomId; presence and private
|
||||
// instances hold the OrderIndex (RoomSceneIds).
|
||||
var subRoomId = extras.SubRoomId is long sub && sub > 0
|
||||
? RoomSceneIds.ToOrderIndex(sub)
|
||||
: (long?)null;
|
||||
|
||||
// Same sourcing as the room browser: presence rows are the live truth
|
||||
// (one instance per distinct PhotonRoomId), and the caller's OWN private
|
||||
@@ -417,7 +421,7 @@ public class PlayerEventsController(
|
||||
{
|
||||
roomInstanceId = room.RoomInstanceId,
|
||||
roomId = room.RoomId,
|
||||
subRoomId = room.SubRoomId,
|
||||
subRoomId = RoomSceneIds.ToWire(room.SubRoomId),
|
||||
// MaxCapacity is the per-instance cap the matchmaker handed
|
||||
// out, so this is a real answer rather than a constant false.
|
||||
isFull = room.MaxCapacity > 0 && playerIds.Count >= room.MaxCapacity,
|
||||
@@ -438,7 +442,7 @@ public class PlayerEventsController(
|
||||
{
|
||||
roomInstanceId = p.Id,
|
||||
roomId = p.RoomId,
|
||||
subRoomId = p.SubRoomId,
|
||||
subRoomId = RoomSceneIds.ToWire(p.SubRoomId),
|
||||
isFull = false,
|
||||
createdAt = DateTime.SpecifyKind(p.CreatedAt, DateTimeKind.Utc),
|
||||
playerIds = Array.Empty<int>(),
|
||||
|
||||
@@ -35,6 +35,7 @@ public class RoomsController(
|
||||
DomainConfig domain,
|
||||
NotificationService notifications,
|
||||
ServerSettingsService serverSettings,
|
||||
SystemNotificationService systemNotifications,
|
||||
ILogger<RoomsController> logger) : ControllerBase
|
||||
{
|
||||
private long? CurrentPlayerId => ControllerBaseExtensions.CurrentPlayerId(this);
|
||||
@@ -574,6 +575,7 @@ public class RoomsController(
|
||||
[HttpGet("rooms/{roomId:long}/subrooms/{subRoomId:long}/datahistory")]
|
||||
public async Task<IActionResult> SubRoomDataHistory(long roomId, long subRoomId)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var blobs = await db.RoomDataBlobs
|
||||
.Where(b => b.RoomId == roomId
|
||||
&& (b.SubRoomId == subRoomId || b.SubRoomId == null))
|
||||
@@ -586,7 +588,7 @@ public class RoomsController(
|
||||
.Take(50)
|
||||
.Select(b => new
|
||||
{
|
||||
SubRoomId = subRoomId,
|
||||
SubRoomId = RoomSceneIds.ToWire(subRoomId),
|
||||
DataBlob = b.BlobName,
|
||||
SavedByAccountId = b.UploadedByPlayerId,
|
||||
CreatedAt = b.UploadedAt,
|
||||
@@ -613,6 +615,7 @@ public class RoomsController(
|
||||
[FromQuery] int? unityAssetTarget,
|
||||
[FromQuery] int? unityAssetVersion)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var room = await rooms.GetByIdAsync(roomId);
|
||||
if (room is null) return NotFound(new { error = "room_not_found", roomId });
|
||||
|
||||
@@ -661,6 +664,7 @@ public class RoomsController(
|
||||
[Authorize]
|
||||
public async Task<IActionResult> SubRoom(long roomId, long subRoomId)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var room = await rooms.GetByIdAsync(roomId);
|
||||
if (room is null) return NotFound(new { error = "room_not_found", roomId });
|
||||
|
||||
@@ -689,6 +693,7 @@ public class RoomsController(
|
||||
[FromQuery] int? unityAssetTarget,
|
||||
[FromQuery] int? unityAssetVersion)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var room = await rooms.GetByIdAsync(roomId);
|
||||
if (room is null) return NotFound(new { error = "room_not_found", roomId });
|
||||
|
||||
@@ -731,6 +736,7 @@ public class RoomsController(
|
||||
long roomId, long subRoomId,
|
||||
[FromQuery] int skip = 0, [FromQuery] int take = 50)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var room = await rooms.GetByIdAsync(roomId);
|
||||
if (room is null) return NotFound(new { error = "room_not_found", roomId });
|
||||
|
||||
@@ -763,7 +769,7 @@ public class RoomsController(
|
||||
saves.Add(new
|
||||
{
|
||||
SubRoomDataSaveId = currentSaveId,
|
||||
SubRoomId = subRoomId,
|
||||
SubRoomId = RoomSceneIds.ToWire(subRoomId),
|
||||
UnityAssetId = scene?.StudioUnityAssetId ?? string.Empty,
|
||||
SubRoomUnityAssetId = scene?.StudioUnityAssetId ?? string.Empty,
|
||||
ReferencedUnityAssetIds = Array.Empty<string>(),
|
||||
@@ -809,7 +815,7 @@ public class RoomsController(
|
||||
var modified = scene?.DataModifiedAt ?? room.UpdatedAt;
|
||||
return new
|
||||
{
|
||||
SubRoomId = (long)orderIndex,
|
||||
SubRoomId = RoomSceneIds.ToWire(orderIndex),
|
||||
RoomId = room.Id,
|
||||
Name = scene?.Name ?? "Home",
|
||||
DataBlob = dataBlob,
|
||||
@@ -824,7 +830,7 @@ public class RoomsController(
|
||||
CurrentSave = new
|
||||
{
|
||||
SubRoomDataSaveId = scene?.StudioSubRoomDataSaveId ?? 0L,
|
||||
SubRoomId = (long)orderIndex,
|
||||
SubRoomId = RoomSceneIds.ToWire(orderIndex),
|
||||
UnityAssetId = scene?.StudioUnityAssetId ?? string.Empty,
|
||||
SubRoomUnityAssetId = scene?.StudioUnityAssetId ?? string.Empty,
|
||||
ReferencedUnityAssetIds = Array.Empty<string>(),
|
||||
@@ -871,7 +877,7 @@ public class RoomsController(
|
||||
return new
|
||||
{
|
||||
SubRoomDataSaveId = saveId,
|
||||
SubRoomId = subRoomId,
|
||||
SubRoomId = RoomSceneIds.ToWire(subRoomId),
|
||||
UnityAssetId = unityAssetId,
|
||||
SubRoomUnityAssetId = unityAssetId,
|
||||
ReferencedUnityAssetIds = Array.Empty<string>(),
|
||||
@@ -916,13 +922,14 @@ public class RoomsController(
|
||||
public async Task<IActionResult> SubRoomRestoreData(
|
||||
long roomId, long subRoomId, [FromForm] string? filename)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
if (string.IsNullOrWhiteSpace(filename))
|
||||
return BadRequest(new { error = "missing_filename" });
|
||||
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var room = await rooms.GetByIdAsync(roomId);
|
||||
if (room is null) return NotFound(new { error = "room_not_found" });
|
||||
if (room.CreatorPlayerId != pid) return Forbid();
|
||||
if (!await CanEditRoomAsync(room, pid)) return Forbid();
|
||||
|
||||
var blob = await db.RoomDataBlobs
|
||||
.FirstOrDefaultAsync(b => b.BlobName == filename && b.RoomId == roomId);
|
||||
@@ -2001,7 +2008,7 @@ public class RoomsController(
|
||||
UseLevelBasedMatchmaking = false,
|
||||
UseAgeBasedMatchmaking = false,
|
||||
UseRecRoyaleMatchmaking = false,
|
||||
SubRoomId = (long)s.OrderIndex,
|
||||
SubRoomId = RoomSceneIds.ToWire(s.OrderIndex),
|
||||
RoomId = room.Id,
|
||||
Name = s.Name,
|
||||
UnityAssetId = s.StudioUnityAssetId,
|
||||
@@ -2027,7 +2034,7 @@ public class RoomsController(
|
||||
UseLevelBasedMatchmaking = false,
|
||||
UseAgeBasedMatchmaking = false,
|
||||
UseRecRoyaleMatchmaking = false,
|
||||
SubRoomId = 0L,
|
||||
SubRoomId = RoomSceneIds.ToWire(0),
|
||||
RoomId = room.Id,
|
||||
Name = "Home",
|
||||
UnityAssetId = string.Empty,
|
||||
@@ -2046,11 +2053,10 @@ public class RoomsController(
|
||||
},
|
||||
};
|
||||
|
||||
var wireRoles = new List<object>
|
||||
{
|
||||
BuildRoomAccountRoleWire(room.CreatorPlayerId, 30),
|
||||
};
|
||||
wireRoles.AddRange(roleList.Select(BuildRoomRoleGrantWire));
|
||||
// The creator is Creator (255), never CoOwner (30): the client reads
|
||||
// the local player's role from this list and only lets a player edit
|
||||
// roles strictly below their own (see RoomRoleRules).
|
||||
var wireRoles = RoomRoleRules.BuildWires(room, roleList);
|
||||
|
||||
var tags = string.IsNullOrEmpty(room.TagsCsv)
|
||||
? Array.Empty<object>()
|
||||
@@ -2075,7 +2081,9 @@ public class RoomsController(
|
||||
State = room.State,
|
||||
Accessibility = room.Accessibility,
|
||||
SupportsLevelVoting = room.SupportsLevelVoting,
|
||||
IsRRO = room.IsAGRoom,
|
||||
// Player rooms are never RRO on the wire (see RoomService.WireIsRRO):
|
||||
// the client refuses publish and room settings for an RRO.
|
||||
IsRRO = RoomService.WireIsRRO(room),
|
||||
SupportsScreens = room.SupportsScreens,
|
||||
SupportsWalkVR = room.SupportsWalkVR,
|
||||
SupportsTeleportVR = room.SupportsTeleportVR,
|
||||
@@ -2922,9 +2930,9 @@ public class RoomsController(
|
||||
{
|
||||
// RoomRoles co-owner check — same authorisation as the
|
||||
// existing modify endpoint, lets co-owners rename too.
|
||||
var coOwner = await db.RoomRoles.AnyAsync(rr =>
|
||||
rr.RoomId == roomId && rr.PlayerId == me && rr.Accepted && (rr.Role == 0 || rr.Role == 1));
|
||||
if (!coOwner) return Forbid();
|
||||
// Room settings are creator / co-owner only; moderators and
|
||||
// hosts moderate players, they don't edit the room.
|
||||
if (!await CanEditRoomAsync(room, me)) return Forbid();
|
||||
}
|
||||
|
||||
// Uniqueness — Rooms.Name has a unique index (DorkNetDbContext.cs:118).
|
||||
@@ -2964,6 +2972,7 @@ public class RoomsController(
|
||||
long subRoomId,
|
||||
[ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] RenameBody body)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var newName = body?.Name;
|
||||
if (string.IsNullOrWhiteSpace(newName))
|
||||
return BadRequest(new { Result = 1, error = "empty_name" });
|
||||
@@ -2976,9 +2985,9 @@ public class RoomsController(
|
||||
if (room is null) return NotFound(new { Result = 4, error = "room_not_found" });
|
||||
if (room.CreatorPlayerId != me)
|
||||
{
|
||||
var coOwner = await db.RoomRoles.AnyAsync(rr =>
|
||||
rr.RoomId == roomId && rr.PlayerId == me && rr.Accepted && (rr.Role == 0 || rr.Role == 1));
|
||||
if (!coOwner) return Forbid();
|
||||
// Room settings are creator / co-owner only; moderators and
|
||||
// hosts moderate players, they don't edit the room.
|
||||
if (!await CanEditRoomAsync(room, me)) return Forbid();
|
||||
}
|
||||
|
||||
var scene = await db.RoomScenes.FirstOrDefaultAsync(s =>
|
||||
@@ -3614,7 +3623,7 @@ public class RoomsController(
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
if (room.CreatorPlayerId != pid) return Forbid();
|
||||
if (!await CanEditRoomAsync(room, pid)) return Forbid();
|
||||
|
||||
var name = (req.Name ?? string.Empty).Trim();
|
||||
if (name.Length == 0) name = "NewScene";
|
||||
@@ -3651,10 +3660,11 @@ public class RoomsController(
|
||||
[Authorize]
|
||||
public async Task<IActionResult> DeleteSubRoom(long roomId, long subRoomId)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
if (room.CreatorPlayerId != pid) return Forbid();
|
||||
if (!await CanEditRoomAsync(room, pid)) return Forbid();
|
||||
var scene = await db.RoomScenes
|
||||
.FirstOrDefaultAsync(s => s.RoomId == roomId && s.OrderIndex == subRoomId);
|
||||
if (scene is null) return NotFound();
|
||||
@@ -3710,20 +3720,6 @@ public class RoomsController(
|
||||
/// <summary>One row of the per-subroom role-permission matrix the 2023
|
||||
/// client PUTs to <c>subrooms/{sub}/permissions</c>. Keys verified in the
|
||||
/// writer <c>HDOJMEBCAGP.txt</c> ISIL 050/069/085/101/117.</summary>
|
||||
public sealed class SubRoomPermissionEntry
|
||||
{
|
||||
// Field order + CLR types come from the BNLPBMJJOMM constructor
|
||||
// (BNLPBMJJOMM.txt:126): (String Permission, OMMBGJMJJPN Role,
|
||||
// Boolean Override, FEOJFGECKGH Type, String Value). The two enum
|
||||
// members serialize as numbers (the role writer boxes them
|
||||
// System.Int32 — BFKNABDHMGG.txt ISIL 015), hence int? here.
|
||||
public string? Permission { get; set; }
|
||||
public int? Role { get; set; }
|
||||
public bool? Override { get; set; }
|
||||
public int? Type { get; set; }
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PublishSubRoomSaveRequest
|
||||
{
|
||||
public long? SubRoomDataSaveId { get; set; }
|
||||
@@ -3773,45 +3769,103 @@ public class RoomsController(
|
||||
s.CanMatchmakeInto = a != AccessibilityPrivate;
|
||||
});
|
||||
|
||||
/// <summary>PUT <c>rooms/{id}/subrooms/{sub}/permissions</c> — the
|
||||
/// role-permission matrix. Client:
|
||||
/// <summary>PUT <c>rooms/{id}/subrooms/{sub}/permissions</c> — "change
|
||||
/// subroom permissions", the role-permission matrix. Client:
|
||||
/// <c>NLDBPDCNNCF.AAMEOMHKPHF(long, long, IReadOnlyList<BNLPBMJJOMM>)</c>
|
||||
/// (NLDBPDCNNCF.txt:9198, verb 3). Unlike its siblings this one does NOT
|
||||
/// form-encode: the closure calls <c>BNDIAONDFFF.FJLLPHFOOJJ(String)</c>
|
||||
/// (CCNOMNNHNNG.txt ISIL 009), which stamps body-kind 4 = raw string
|
||||
/// (BNDIAONDFFF.txt:275, <c>mov dword ptr [rbx+38h],4</c>) — i.e. a raw
|
||||
/// JSON ARRAY. FormOrJsonModelBinder handles that (non-form → JSON
|
||||
/// deserialize into the List).
|
||||
/// (NLDBPDCNNCF.txt:9198, verb 3, same typed helper as every other room
|
||||
/// mutation, so the response is the full <c>FGCPNAACHIK</c> details).
|
||||
/// Unlike its siblings the body is NOT form-encoded: the closure calls
|
||||
/// <c>BNDIAONDFFF.FJLLPHFOOJJ(String)</c> (CCNOMNNHNNG.txt ISIL 009), a raw
|
||||
/// JSON array of cells. The editor sends one changed cell per call
|
||||
/// (OMJKHJLFOCO.AFHJLBBFAGP.<LocalRequestModifyRolePermission>g__ChangeAndRefresh
|
||||
/// builds a one-element BNLPBMJJOMM[]), so each cell is upserted; cells not
|
||||
/// sent are left alone.
|
||||
///
|
||||
/// <para><b>Scope note — deliberately not persisted.</b> DorkNet has no
|
||||
/// per-subroom permission storage: <see cref="RoomSceneEntity"/> has no
|
||||
/// column for it and there is no permissions table. Adding one means an
|
||||
/// entity + DbContext + migration, which is outside this file. Until then
|
||||
/// the matrix is accepted, logged, and the room details echoed back so the
|
||||
/// client's promise resolves instead of 415-ing; the values do not survive
|
||||
/// a round-trip. The old handler silently bound an unrelated DTO whose keys
|
||||
/// the client never sends, which was strictly worse.</para></summary>
|
||||
/// <para>Read back through <c>GET photon_access_token</c> (its
|
||||
/// <c>Permissions</c> list), and pushed to everyone in that sub-room as
|
||||
/// the <c>PhotonAccessToken</c> hub message so the change takes effect
|
||||
/// without a rejoin.</para>
|
||||
///
|
||||
/// <para>Authorization mirrors the client's own rule
|
||||
/// (OMJKHJLFOCO.DIIMLMKEJLC): the creator may edit every role, including
|
||||
/// Co-Owner and Creator; a co-owner only the roles below their own;
|
||||
/// nobody else may edit (<see cref="RoomRoleRules.CanEditRolePermissions"/>).</para></summary>
|
||||
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/permissions")]
|
||||
[HttpPut("rooms/{roomId:long}/subrooms/{subRoomId:long}/permissions")]
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/permissions")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/permissions")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> SubRoomPermissions(
|
||||
long roomId,
|
||||
long subRoomId,
|
||||
[ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] List<SubRoomPermissionEntry> req)
|
||||
public async Task<IActionResult> SubRoomPermissions(long roomId, long subRoomId)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
if (room.CreatorPlayerId != pid) return Forbid();
|
||||
var roleRows = await db.RoomRoles.Where(r => r.RoomId == roomId).ToListAsync();
|
||||
if (!RoomRoleRules.CanEditRoom(room, roleRows, pid)) return Forbid();
|
||||
|
||||
string body;
|
||||
using (var reader = new StreamReader(Request.Body))
|
||||
body = await reader.ReadToEndAsync();
|
||||
var cells = RoomPermissionMatrix.Parse(body);
|
||||
if (cells is null) return BadRequest(new { error = "invalid_permissions_body" });
|
||||
|
||||
var denied = cells.Where(c => !RoomRoleRules.CanEditRolePermissions(room, roleRows, pid, c.Role)).ToList();
|
||||
if (denied.Count > 0)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"[subroom-permissions] denied room={Room} sub={Sub} by={By} roles={Roles}",
|
||||
roomId, subRoomId, pid, string.Join(",", denied.Select(d => d.Role).Distinct()));
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var scene = await GetOrCreateSceneForMutationAsync(room, subRoomId);
|
||||
if (scene is null) return NotFound();
|
||||
|
||||
var existing = await db.RoomRolePermissions
|
||||
.Where(p => p.RoomId == roomId && p.SubRoomId == subRoomId)
|
||||
.ToListAsync();
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
var row = existing.FirstOrDefault(p =>
|
||||
p.Role == cell.Role
|
||||
&& string.Equals(p.Permission, cell.Permission, StringComparison.OrdinalIgnoreCase));
|
||||
if (row is null)
|
||||
{
|
||||
row = new RoomRolePermissionEntity
|
||||
{
|
||||
RoomId = roomId,
|
||||
SubRoomId = subRoomId,
|
||||
Role = cell.Role,
|
||||
Permission = cell.Permission,
|
||||
};
|
||||
db.RoomRolePermissions.Add(row);
|
||||
existing.Add(row);
|
||||
}
|
||||
row.Type = cell.Type;
|
||||
row.Override = cell.Override;
|
||||
row.Value = cell.Value;
|
||||
row.UpdatedByPlayerId = pid;
|
||||
row.UpdatedAt = now;
|
||||
}
|
||||
scene.DataModifiedAt = now;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
logger.LogWarning(
|
||||
"[subroom-permissions] room={Room} sub={Sub} by={By} entries={Count} — accepted but NOT persisted (no permission storage yet)",
|
||||
roomId, subRoomId, pid, req?.Count ?? 0);
|
||||
logger.LogInformation(
|
||||
"[subroom-permissions] room={Room} sub={Sub} by={By} cells={Count}",
|
||||
roomId, subRoomId, pid, cells.Count);
|
||||
|
||||
// Everyone in that sub-room gets the new matrix; the client's
|
||||
// PhotonAccessToken handler re-initialises its permissions from it.
|
||||
var matrix = await RoomPermissionMatrix.LoadAsync(db, roomId, subRoomId);
|
||||
foreach (var (instance, players) in presence.EnumerateActiveInstances(roomId, includePrivate: true))
|
||||
{
|
||||
if (instance.SubRoomId != subRoomId) continue;
|
||||
var tokenBody = RoomPermissionMatrix.AccessTokenBody(instance.RoomInstanceId, matrix);
|
||||
foreach (var other in players)
|
||||
await notifications.NotifyTypedAsync(other, "PhotonAccessToken", tokenBody);
|
||||
}
|
||||
|
||||
return Ok(await BuildRoomServerDetailsWithRolesAsync(roomId));
|
||||
}
|
||||
@@ -3842,10 +3896,11 @@ public class RoomsController(
|
||||
[Authorize]
|
||||
public async Task<IActionResult> PublishSubRoomSave(long roomId, long subRoomId)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
if (room.CreatorPlayerId != pid) return Forbid();
|
||||
if (!await CanEditRoomAsync(room, pid)) return Forbid();
|
||||
|
||||
var scene = await GetOrCreateSceneForMutationAsync(room, subRoomId);
|
||||
if (scene is null) return NotFound();
|
||||
@@ -3921,10 +3976,11 @@ public class RoomsController(
|
||||
[Authorize]
|
||||
public async Task<IActionResult> CloneSubRoom(long roomId, long subRoomId)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
if (room.CreatorPlayerId != pid) return Forbid();
|
||||
if (!await CanEditRoomAsync(room, pid)) return Forbid();
|
||||
var src = await db.RoomScenes
|
||||
.FirstOrDefaultAsync(s => s.RoomId == roomId && s.OrderIndex == subRoomId);
|
||||
if (src is null) return NotFound();
|
||||
@@ -4015,6 +4071,7 @@ public class RoomsController(
|
||||
long subRoomId,
|
||||
[ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] SubRoomMoveRequest req)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
@@ -4105,6 +4162,7 @@ public class RoomsController(
|
||||
[Authorize]
|
||||
public async Task<IActionResult> SubRoomData(long roomId, long subRoomId)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var body = await ReadSaveRoomSceneRequestAsync();
|
||||
body.RoomSceneId = subRoomId;
|
||||
return await SaveDataCore(body, roomId, wrapCreateModifyResponse: true);
|
||||
@@ -4165,10 +4223,11 @@ public class RoomsController(
|
||||
/// successful mutation surface as a client-side parse failure.</summary>
|
||||
private async Task<IActionResult> MutateScene(long roomId, long subRoomId, Action<RoomSceneEntity> mutator)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
if (room.CreatorPlayerId != pid) return Forbid();
|
||||
if (!await CanEditRoomAsync(room, pid)) return Forbid();
|
||||
var scene = await GetOrCreateSceneForMutationAsync(room, subRoomId);
|
||||
if (scene is null) return NotFound();
|
||||
mutator(scene);
|
||||
@@ -4219,12 +4278,7 @@ public class RoomsController(
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
var rows = await db.RoomRoles.Where(r => r.RoomId == roomId).ToListAsync();
|
||||
var list = new List<object>(rows.Count + 1)
|
||||
{
|
||||
BuildRoomAccountRoleWire(room.CreatorPlayerId, 30),
|
||||
};
|
||||
list.AddRange(rows.Select(BuildRoomRoleGrantWire));
|
||||
return Ok(list);
|
||||
return Ok(RoomRoleRules.BuildWires(room, rows));
|
||||
}
|
||||
|
||||
private static string CurrentOrSyntheticDataBlobName(RoomEntity room)
|
||||
@@ -4339,30 +4393,38 @@ public class RoomsController(
|
||||
/// <summary>GET <c>rooms/{id}/roles/{playerId}</c> — one role row
|
||||
/// (<c>NLDBPDCNNCF.PCFOIPELKHN</c> → <c>Task<EFHPLDPNGIM></c>,
|
||||
/// NLDBPDCNNCF.txt:2192). roomserver/ twin required, same as the list
|
||||
/// above.</summary>
|
||||
/// above. The creator answers as Creator (255).</summary>
|
||||
[HttpGet("rooms/{roomId:long}/roles/{playerId:long}")]
|
||||
[HttpGet("roomserver/rooms/{roomId:long}/roles/{playerId:long}")]
|
||||
public async Task<IActionResult> RoleForPlayer(long roomId, long playerId)
|
||||
{
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
if (room.CreatorPlayerId == playerId)
|
||||
return Ok(BuildRoomAccountRoleWire(playerId, 30));
|
||||
var row = await db.RoomRoles
|
||||
.FirstOrDefaultAsync(r => r.RoomId == roomId && r.PlayerId == playerId);
|
||||
if (row is null) return NotFound();
|
||||
return Ok(BuildRoomRoleGrantWire(row));
|
||||
var rows = await db.RoomRoles
|
||||
.Where(r => r.RoomId == roomId && r.PlayerId == playerId)
|
||||
.ToListAsync();
|
||||
var wire = RoomRoleRules.BuildWires(room, rows)
|
||||
.FirstOrDefault(w => w.AccountId == RoomRoleRules.ToClientAccountId(playerId));
|
||||
if (wire is null) return NotFound();
|
||||
return Ok(wire);
|
||||
}
|
||||
|
||||
public sealed class GrantRoleRequest { public int? Role { get; set; } }
|
||||
|
||||
/// <summary>PUT <c>rooms/{id}/roles/{playerId}</c> — owner grants
|
||||
/// a role to a player. Auto-accepted (no invite step).
|
||||
/// <c>NLDBPDCNNCF.LMIEIJMCEAB</c> ("change room account role",
|
||||
/// NLDBPDCNNCF.txt:7380) is verb 3 = PUT, form key <c>role</c> carrying a
|
||||
/// boxed Int32 (BFKNABDHMGG.txt ISIL 015), and returns the full
|
||||
/// <c>FGCPNAACHIK</c> details — which this already does. Only the
|
||||
/// roomserver/ twin was missing.</summary>
|
||||
/// <summary>PUT <c>rooms/{id}/roles/{playerId}</c> — "change room account
|
||||
/// role" (<c>NLDBPDCNNCF.LMIEIJMCEAB</c>, NLDBPDCNNCF.txt:7516, verb 3 =
|
||||
/// PUT, single form key <c>role</c> boxed as Int32 —
|
||||
/// NLDBPDCNNCF_NestedType_BFKNABDHMGG.txt ISIL 015-024), response the full
|
||||
/// <c>FGCPNAACHIK</c> details. Three callers:
|
||||
/// <list type="bullet">
|
||||
/// <item>the owner's role button (AddRemovePlayerRoomRoleConditionalButton
|
||||
/// .EEGJBELCBDI): Host/Moderator are set directly, and "Remove" sends
|
||||
/// role 0;</item>
|
||||
/// <item>the invitee accepting a co-owner invite (message Type 62,
|
||||
/// KPFKPIKDBFJ.ELKAEBMMIGL / KLNLKDJJDLE Accept): their OWN account id
|
||||
/// with role 30;</item>
|
||||
/// <item>a co-owner rejecting a direct add (Type 60, COJCABEJDFN Reject):
|
||||
/// their own id with role 0.</item>
|
||||
/// </list>
|
||||
/// Authorization is <see cref="RoomRoleRules.CheckChange"/>.</summary>
|
||||
[HttpPut("rooms/{roomId:long}/roles/{playerId:long}")]
|
||||
[HttpPost("rooms/{roomId:long}/roles/{playerId:long}")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/roles/{playerId:long}")]
|
||||
@@ -4372,45 +4434,19 @@ public class RoomsController(
|
||||
[HttpPut("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}")]
|
||||
[HttpPost("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GrantRole(long roomId, long playerId)
|
||||
{
|
||||
var me = this.RequireCurrentPlayerId();
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
if (!await CanManageRoomRolesAsync(room, me)) return Forbid();
|
||||
var role = await ReadClientRoomRoleAsync();
|
||||
if (role is null) return BadRequest(new { error = "missing_role" });
|
||||
if (role.Value == 0)
|
||||
return await ClearRoomAccountRoleAsync(room, playerId);
|
||||
public Task<IActionResult> GrantRole(long roomId, long playerId)
|
||||
=> ChangeRoomAccountRoleAsync(roomId, playerId, isInvite: false);
|
||||
|
||||
if (!TryMapClientRoleToRoomRole(role.Value, out var roomRole))
|
||||
return BadRequest(new { error = "invalid_role", role });
|
||||
|
||||
var existing = await db.RoomRoles
|
||||
.FirstOrDefaultAsync(r => r.RoomId == roomId && r.PlayerId == playerId && r.Role == roomRole);
|
||||
if (existing is null)
|
||||
{
|
||||
await RemoveOtherRoomRolesAsync(roomId, playerId, roomRole);
|
||||
db.RoomRoles.Add(new RoomRoleEntity
|
||||
{
|
||||
RoomId = roomId, PlayerId = playerId, Role = roomRole,
|
||||
Accepted = true, GrantedByPlayerId = me,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.Accepted = true;
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(await BuildRoomServerDetailsWithRolesAsync(roomId));
|
||||
}
|
||||
|
||||
/// <summary>POST <c>rooms/{id}/roles/{playerId}/invite</c> — same
|
||||
/// as grant but Accepted=false; the target's accept-invite flow
|
||||
/// flips the flag (separate endpoint, not yet exposed). For now
|
||||
/// invited rows surface in RoomDetails.InvitedCoOwners etc.
|
||||
/// <c>NLDBPDCNNCF.KIJDCLINOIC</c> ("invite account to role",
|
||||
/// NLDBPDCNNCF.txt:7550) is verb 3 = PUT under the roomserver/ prefix.</summary>
|
||||
/// <summary>PUT <c>rooms/{id}/roles/{playerId}/invite</c> — "invite account
|
||||
/// to role" (<c>NLDBPDCNNCF.KIJDCLINOIC</c>, NLDBPDCNNCF.txt:7686, verb 3,
|
||||
/// form key <c>role</c> as Int32 — NLDBPDCNNCF_NestedType_ENHMMCAOING.txt).
|
||||
/// Only Co-Owner goes through an invite in this client: the owner's button
|
||||
/// sends role 30 here, the invitee is told with a Type 62
|
||||
/// (RoomCoOwnerInvited) message carrying RoomId, and either accepts with
|
||||
/// <c>PUT roles/{me}</c> role 30 or rejects by sending role 0 back to THIS
|
||||
/// route on their own id (KPFKPIKDBFJ.OGONAKHNMGN, KLNLKDJJDLE Reject),
|
||||
/// which clears the pending invite but leaves any role they already
|
||||
/// hold.</summary>
|
||||
[HttpPut("rooms/{roomId:long}/roles/{playerId:long}/invite")]
|
||||
[HttpPost("rooms/{roomId:long}/roles/{playerId:long}/invite")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/roles/{playerId:long}/invite")]
|
||||
@@ -4420,62 +4456,167 @@ public class RoomsController(
|
||||
[HttpPut("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}/invite")]
|
||||
[HttpPost("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}/invite")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> InviteRole(long roomId, long playerId)
|
||||
public Task<IActionResult> InviteRole(long roomId, long playerId)
|
||||
=> ChangeRoomAccountRoleAsync(roomId, playerId, isInvite: true);
|
||||
|
||||
private async Task<IActionResult> ChangeRoomAccountRoleAsync(long roomId, long playerId, bool isInvite)
|
||||
{
|
||||
var me = this.RequireCurrentPlayerId();
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
if (!await CanManageRoomRolesAsync(room, me)) return Forbid();
|
||||
var role = await ReadClientRoomRoleAsync();
|
||||
if (role is null) return BadRequest(new { error = "missing_role" });
|
||||
if (role.Value == 0)
|
||||
return await ClearRoomAccountRoleAsync(room, playerId);
|
||||
var requested = await ReadClientRoomRoleAsync();
|
||||
if (requested is null) return BadRequest(new { error = "missing_role" });
|
||||
if (playerId <= 0 || !await db.Players.AnyAsync(p => p.Id == playerId))
|
||||
return NotFound(new { error = "unknown_player" });
|
||||
|
||||
if (!TryMapClientRoleToRoomRole(role.Value, out var roomRole))
|
||||
return BadRequest(new { error = "invalid_role", role });
|
||||
|
||||
var existing = await db.RoomRoles
|
||||
.FirstOrDefaultAsync(r => r.RoomId == roomId && r.PlayerId == playerId && r.Role == roomRole);
|
||||
if (existing is null)
|
||||
var rows = await db.RoomRoles.Where(r => r.RoomId == roomId).ToListAsync();
|
||||
var error = RoomRoleRules.CheckChange(
|
||||
room, rows, me, playerId, requested.Value, isInvite, out var kind);
|
||||
if (error is not null)
|
||||
{
|
||||
await RemoveOtherRoomRolesAsync(roomId, playerId, roomRole);
|
||||
db.RoomRoles.Add(new RoomRoleEntity
|
||||
logger.LogWarning(
|
||||
"[room-roles] denied room={Room} actor={Actor} target={Target} role={Role} invite={Invite} error={Error}",
|
||||
roomId, me, playerId, requested.Value, isInvite, error);
|
||||
return error == "cannot_change_creator_role" || error == "invalid_role"
|
||||
? BadRequest(new { error, role = requested.Value })
|
||||
: Forbid();
|
||||
}
|
||||
|
||||
var targetRows = rows.Where(r => r.PlayerId == playerId).ToList();
|
||||
var wasCoOwner = targetRows.Any(r => r.Accepted && r.Role == RoomRoleRules.StoredCoOwner);
|
||||
var hadCoOwnerInvite = targetRows.Any(r => !r.Accepted && r.Role == RoomRoleRules.StoredCoOwner);
|
||||
RoomRoleRules.TryFromWire(requested.Value, out var storedRole);
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case RoomRoleRules.ChangeKind.Clear when isInvite:
|
||||
// Reject / withdraw an invite: only the pending rows go.
|
||||
db.RoomRoles.RemoveRange(targetRows.Where(r => !r.Accepted));
|
||||
break;
|
||||
|
||||
case RoomRoleRules.ChangeKind.Clear:
|
||||
db.RoomRoles.RemoveRange(targetRows);
|
||||
break;
|
||||
|
||||
case RoomRoleRules.ChangeKind.Invite:
|
||||
{
|
||||
RoomId = roomId, PlayerId = playerId, Role = roomRole,
|
||||
Accepted = false, GrantedByPlayerId = me,
|
||||
});
|
||||
// Already holding the role: nothing to invite to. Otherwise
|
||||
// replace any other pending invite but keep the role the
|
||||
// player holds until they accept.
|
||||
if (targetRows.Any(r => r.Accepted && r.Role == storedRole)) break;
|
||||
db.RoomRoles.RemoveRange(targetRows.Where(r => !r.Accepted && r.Role != storedRole));
|
||||
var pending = targetRows.FirstOrDefault(r => !r.Accepted && r.Role == storedRole);
|
||||
if (pending is null)
|
||||
{
|
||||
db.RoomRoles.Add(new RoomRoleEntity
|
||||
{
|
||||
RoomId = roomId, PlayerId = playerId, Role = storedRole,
|
||||
Accepted = false, GrantedByPlayerId = me, GrantedAt = DateTime.UtcNow,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
pending.GrantedByPlayerId = me;
|
||||
pending.GrantedAt = DateTime.UtcNow;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case RoomRoleRules.ChangeKind.AcceptInvite:
|
||||
case RoomRoleRules.ChangeKind.Grant:
|
||||
{
|
||||
// One role per account: the new one replaces the rest
|
||||
// (an accepted invite replaces the role held meanwhile).
|
||||
var keep = targetRows.FirstOrDefault(r => r.Role == storedRole);
|
||||
db.RoomRoles.RemoveRange(targetRows.Where(r => !ReferenceEquals(r, keep)));
|
||||
if (keep is null)
|
||||
{
|
||||
db.RoomRoles.Add(new RoomRoleEntity
|
||||
{
|
||||
RoomId = roomId, PlayerId = playerId, Role = storedRole,
|
||||
Accepted = true,
|
||||
GrantedByPlayerId = kind == RoomRoleRules.ChangeKind.AcceptInvite
|
||||
? targetRows.FirstOrDefault(r => !r.Accepted)?.GrantedByPlayerId ?? me
|
||||
: me,
|
||||
GrantedAt = DateTime.UtcNow,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
keep.Accepted = true;
|
||||
if (kind == RoomRoleRules.ChangeKind.Grant) keep.GrantedByPlayerId = me;
|
||||
keep.GrantedAt = DateTime.UtcNow;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var isCoOwnerNow = await db.RoomRoles.AnyAsync(r =>
|
||||
r.RoomId == roomId && r.PlayerId == playerId && r.Accepted && r.Role == RoomRoleRules.StoredCoOwner);
|
||||
var hasCoOwnerInviteNow = await db.RoomRoles.AnyAsync(r =>
|
||||
r.RoomId == roomId && r.PlayerId == playerId && !r.Accepted && r.Role == RoomRoleRules.StoredCoOwner);
|
||||
logger.LogInformation(
|
||||
"[room-roles] room={Room} actor={Actor} target={Target} role={Role} invite={Invite} kind={Kind} coOwner={Was}->{Now}",
|
||||
roomId, me, playerId, requested.Value, isInvite, kind, wasCoOwner, isCoOwnerNow);
|
||||
|
||||
// The invitee's notifications are how the role reaches them: the
|
||||
// 2023 client renders message Types 60/61/62 (factory
|
||||
// OCGKOJMFLAI.ALEHDHAEFOK, enum KJECOLODAFA.MEPCALGGMJC) and reads
|
||||
// only RoomId from them. Self-service changes (accept / reject /
|
||||
// leave) notify nobody.
|
||||
if (playerId != me)
|
||||
{
|
||||
existing.Accepted = false;
|
||||
if (!hadCoOwnerInvite && hasCoOwnerInviteNow)
|
||||
await systemNotifications.SendAsync(playerId,
|
||||
SystemNotificationService.MessageType.RoomCoOwnerInvited, me, roomId: roomId);
|
||||
if (!wasCoOwner && isCoOwnerNow)
|
||||
await systemNotifications.SendAsync(playerId,
|
||||
SystemNotificationService.MessageType.RoomCoOwnerAdded, me, roomId: roomId);
|
||||
if (wasCoOwner && !isCoOwnerNow)
|
||||
await systemNotifications.SendAsync(playerId,
|
||||
SystemNotificationService.MessageType.RoomCoOwnerRemoved, me, roomId: roomId);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(await BuildRoomServerDetailsWithRolesAsync(roomId));
|
||||
|
||||
if (hadCoOwnerInvite && !hasCoOwnerInviteNow)
|
||||
await MarkCoOwnerInviteMessagesReadAsync(playerId, roomId);
|
||||
|
||||
var details = await BuildRoomServerDetailsWithRolesAsync(roomId);
|
||||
await PushRoomUpdateAsync(roomId, details, playerId, me);
|
||||
return Ok(details);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ClearRoomAccountRoleAsync(RoomEntity room, long playerId)
|
||||
/// <summary>Once an invite is answered (or withdrawn) its Type 62 message
|
||||
/// must stop offering Accept/Reject in the inbox.</summary>
|
||||
private async Task MarkCoOwnerInviteMessagesReadAsync(long playerId, long roomId)
|
||||
{
|
||||
if (room.CreatorPlayerId == playerId)
|
||||
return BadRequest(new { error = "cannot_change_creator_role" });
|
||||
|
||||
var rows = await db.RoomRoles
|
||||
.Where(r => r.RoomId == room.Id && r.PlayerId == playerId)
|
||||
var stale = await db.Messages
|
||||
.Where(m => m.RecipientPlayerId == playerId
|
||||
&& m.RoomId == roomId
|
||||
&& m.Type == SystemNotificationService.MessageType.RoomCoOwnerInvited
|
||||
&& m.ReadAt == null)
|
||||
.ToListAsync();
|
||||
if (rows.Count > 0)
|
||||
db.RoomRoles.RemoveRange(rows);
|
||||
|
||||
if (stale.Count == 0) return;
|
||||
foreach (var m in stale) m.ReadAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(await BuildRoomServerDetailsWithRolesAsync(room.Id));
|
||||
}
|
||||
|
||||
private async Task<bool> CanManageRoomRolesAsync(RoomEntity room, long playerId)
|
||||
/// <summary>Push the fresh room details as <c>RoomUpdate</c> (handler
|
||||
/// IBEOONPEELF.txt:507, Action<FGCPNAACHIK>) to everyone currently
|
||||
/// in the room plus the players whose view changed, so the role list,
|
||||
/// the role-gated buttons and the local player's own role refresh
|
||||
/// without leaving the room.</summary>
|
||||
private async Task PushRoomUpdateAsync(long roomId, object details, params long[] alsoNotify)
|
||||
{
|
||||
if (room.CreatorPlayerId == playerId) return true;
|
||||
return await db.RoomRoles.AnyAsync(r =>
|
||||
r.RoomId == room.Id
|
||||
&& r.PlayerId == playerId
|
||||
&& r.Accepted
|
||||
&& r.Role == 0);
|
||||
var recipients = presence.EnumerateActiveInstances(roomId, includePrivate: true)
|
||||
.SelectMany(i => i.PlayerIds)
|
||||
.Concat(alsoNotify)
|
||||
.Where(pid => pid > 0)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
foreach (var pid in recipients)
|
||||
await notifications.NotifyAsync(pid, PushNotificationId.SubscriptionUpdateRoom, details);
|
||||
}
|
||||
|
||||
private async Task<int?> ReadClientRoomRoleAsync()
|
||||
@@ -4486,6 +4627,8 @@ public class RoomsController(
|
||||
var form = await Request.ReadFormAsync();
|
||||
if (int.TryParse(form["role"].FirstOrDefault(), out parsed)) return parsed;
|
||||
if (int.TryParse(form["Role"].FirstOrDefault(), out parsed)) return parsed;
|
||||
if (TryParseRoleName(form["role"].FirstOrDefault() ?? form["Role"].FirstOrDefault(), out parsed))
|
||||
return parsed;
|
||||
}
|
||||
|
||||
if (int.TryParse(Request.Query["role"].FirstOrDefault(), out parsed)) return parsed;
|
||||
@@ -4495,10 +4638,20 @@ public class RoomsController(
|
||||
{
|
||||
try
|
||||
{
|
||||
var body = await JsonSerializer.DeserializeAsync<GrantRoleRequest>(
|
||||
Request.Body,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
if (body?.Role is int bodyRole) return bodyRole;
|
||||
using var doc = await JsonDocument.ParseAsync(Request.Body);
|
||||
if (doc.RootElement.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
if (!prop.Name.Equals("role", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (prop.Value.ValueKind == JsonValueKind.Number && prop.Value.TryGetInt32(out parsed))
|
||||
return parsed;
|
||||
if (prop.Value.ValueKind == JsonValueKind.String
|
||||
&& (int.TryParse(prop.Value.GetString(), out parsed)
|
||||
|| TryParseRoleName(prop.Value.GetString(), out parsed)))
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
@@ -4509,64 +4662,34 @@ public class RoomsController(
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task RemoveOtherRoomRolesAsync(long roomId, long playerId, int keepRole)
|
||||
/// <summary>OMMBGJMJJPN member names, for callers that box the enum
|
||||
/// itself (Enum.ToString) instead of an Int32.</summary>
|
||||
internal static bool TryParseRoleName(string? raw, out int role)
|
||||
{
|
||||
var rows = await db.RoomRoles
|
||||
.Where(r => r.RoomId == roomId && r.PlayerId == playerId && r.Role != keepRole)
|
||||
.ToListAsync();
|
||||
if (rows.Count > 0)
|
||||
db.RoomRoles.RemoveRange(rows);
|
||||
}
|
||||
|
||||
private static bool TryMapClientRoleToRoomRole(int clientRole, out int roomRole)
|
||||
{
|
||||
roomRole = clientRole switch
|
||||
role = (raw ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||
{
|
||||
30 => 0, // CoOwner
|
||||
20 => 1, // Moderator
|
||||
10 => 2, // Host
|
||||
"none" => RoomRoleRules.WireNone,
|
||||
"banned" => RoomRoleRules.WireBanned,
|
||||
"host" => RoomRoleRules.WireHost,
|
||||
"moderator" => RoomRoleRules.WireModerator,
|
||||
"coowner" => RoomRoleRules.WireCoOwner,
|
||||
"temporarycoowner" => RoomRoleRules.WireTemporaryCoOwner,
|
||||
"creator" => RoomRoleRules.WireCreator,
|
||||
_ => -1,
|
||||
};
|
||||
return roomRole >= 0;
|
||||
return role >= 0;
|
||||
}
|
||||
|
||||
private static object BuildRoomRoleGrantWire(RoomRoleEntity role)
|
||||
=> BuildRoomAccountRoleWire(
|
||||
role.PlayerId,
|
||||
ToClientRoomRole(role.Role),
|
||||
role.Accepted,
|
||||
role.GrantedByPlayerId);
|
||||
|
||||
private static object BuildRoomAccountRoleWire(
|
||||
long accountId,
|
||||
int role,
|
||||
bool accepted = true,
|
||||
long? lastChangedByAccountId = null)
|
||||
=> new
|
||||
{
|
||||
AccountId = ToClientAccountId(accountId),
|
||||
Role = accepted ? role : 0,
|
||||
LastChangedByAccountId = ToClientAccountId(lastChangedByAccountId),
|
||||
InvitedRole = accepted ? 0 : role,
|
||||
};
|
||||
|
||||
private static int ToClientRoomRole(int roomRole) => roomRole switch
|
||||
/// <summary>Creator or accepted co-owner (see
|
||||
/// <see cref="RoomRoleRules.CanEditRoom"/>).</summary>
|
||||
private async Task<bool> CanEditRoomAsync(RoomEntity room, long playerId)
|
||||
{
|
||||
0 => 30, // CoOwner
|
||||
1 => 20, // Moderator
|
||||
2 => 10, // Host
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private static int ToClientAccountId(long accountId)
|
||||
=> accountId > int.MaxValue
|
||||
? int.MaxValue
|
||||
: accountId < int.MinValue
|
||||
? int.MinValue
|
||||
: (int)accountId;
|
||||
|
||||
private static int? ToClientAccountId(long? accountId)
|
||||
=> accountId.HasValue ? ToClientAccountId(accountId.Value) : null;
|
||||
if (room.CreatorPlayerId == playerId) return true;
|
||||
var rows = await db.RoomRoles
|
||||
.Where(r => r.RoomId == room.Id && r.PlayerId == playerId)
|
||||
.ToListAsync();
|
||||
return RoomRoleRules.CanEditRoom(room, rows, playerId);
|
||||
}
|
||||
|
||||
private async Task<object> BuildRoomServerDetailsWithRolesAsync(long roomId)
|
||||
{
|
||||
|
||||
@@ -1432,6 +1432,13 @@ public class RoomsModerationController(
|
||||
// it editable. Safe for admin: the purge gate and "RR Original"
|
||||
// badge both key on CreatorPlayerId == SystemAccountId, so a
|
||||
// user-owned clone is still shown as Custom and stays purgeable.
|
||||
//
|
||||
// The WIRE IsRRO is a different thing: it is false for every
|
||||
// player-owned room (RoomService.WireIsRRO). The 2023 client's
|
||||
// room-load pipeline never reads IsRRO (the baked scene comes
|
||||
// from the sub-room's UnitySceneId, the blob step from DataBlob),
|
||||
// while its owner UI refuses publish and every room setting on
|
||||
// an RRO, which is what kept cloned rooms from being published.
|
||||
IsAGRoom = source.IsAGRoom,
|
||||
IsDormRoom = false,
|
||||
CloningAllowed = source.CloningAllowed,
|
||||
@@ -1509,6 +1516,18 @@ public class RoomsModerationController(
|
||||
CanMatchmakeInto = s.CanMatchmakeInto,
|
||||
DataModifiedAt = DateTime.UtcNow,
|
||||
}).ToList();
|
||||
// A template with no scene rows still has to give the new room a
|
||||
// real entry sub-room (named "Home", from the clone's own location,
|
||||
// blob and capacity). Without it the details served a synthesised
|
||||
// placeholder and Room Settings > Subrooms showed an empty row.
|
||||
if (clonedScenes.Count == 0)
|
||||
{
|
||||
var home = Services.RoomService.NewHomeScene(clone);
|
||||
home.Id = ++nextSceneId;
|
||||
clonedScenes.Add(home);
|
||||
}
|
||||
foreach (var scene in clonedScenes.Where(s => string.IsNullOrWhiteSpace(s.Name)))
|
||||
scene.Name = Services.RoomService.DefaultSubRoomName(scene.OrderIndex);
|
||||
if (clonedScenes.Count > 0)
|
||||
{
|
||||
db.RoomScenes.AddRange(clonedScenes);
|
||||
@@ -1580,8 +1599,11 @@ public class RoomsModerationController(
|
||||
// The scene was created as a shallow copy, so its DataBlobName is
|
||||
// currently the SOURCE's. Either a copy replaces it or it gets
|
||||
// cleared — it must never survive as-is.
|
||||
// A synthesised Home scene (template had no rows) has no source
|
||||
// scene; it starts from the room-level blob it was built with.
|
||||
var sourceBlob = i < sourceScenes.Count ? sourceScenes[i].DataBlobName : target.DataBlobName;
|
||||
var copied = await CopyBlobForCloneAsync(
|
||||
sourceScenes[i].DataBlobName, clone.Id, target.OrderIndex, callerId);
|
||||
sourceBlob, clone.Id, target.OrderIndex, callerId);
|
||||
|
||||
target.DataBlobName = copied ?? string.Empty;
|
||||
target.DataModifiedAt = DateTime.UtcNow;
|
||||
|
||||
@@ -317,6 +317,10 @@ public class GoToController(
|
||||
[FromForm(Name = "JoinMode")] int joinMode = 0,
|
||||
[FromForm(Name = "AdditionalPlayerIds")] List<long>? additionalPlayerIds = null)
|
||||
{
|
||||
// The client sends the wire SubRoomId it read from the room details
|
||||
// (OrderIndex + 1, see RoomSceneIds); everything below works in
|
||||
// OrderIndex.
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId);
|
||||
var room = await rooms.GetByIdAsync(roomId);
|
||||
if (room is null)
|
||||
{
|
||||
@@ -1552,9 +1556,25 @@ public class RoomInstanceDto
|
||||
[JsonPropertyName("roomId")]
|
||||
public long RoomId { get; set; }
|
||||
|
||||
[JsonPropertyName("subRoomId")]
|
||||
/// <summary>The sub-room's <see cref="Data.Entities.RoomSceneEntity.OrderIndex"/>
|
||||
/// (0 = the room's first sub-room). Server code reads and writes this;
|
||||
/// the wire carries <see cref="WireSubRoomId"/>.</summary>
|
||||
[JsonIgnore]
|
||||
public long SubRoomId { get; set; }
|
||||
|
||||
/// <summary>The 2023 client's SubRoomId: OrderIndex + 1 (see
|
||||
/// <see cref="Services.RoomSceneIds"/>). It must equal the
|
||||
/// <c>SubRooms[].SubRoomId</c> the room details advertise — the client
|
||||
/// compares the two (<c>SubroomModel.get_SubroomIsLocal</c>) and looks the
|
||||
/// current sub-room up by it. Presence round-trips through this property
|
||||
/// too, so stored JSON stays consistent.</summary>
|
||||
[JsonPropertyName("subRoomId")]
|
||||
public long WireSubRoomId
|
||||
{
|
||||
get => Services.RoomSceneIds.ToWire(SubRoomId);
|
||||
set => SubRoomId = Services.RoomSceneIds.ToOrderIndex(value);
|
||||
}
|
||||
|
||||
// STRING RoomSceneLocationId GUID — foreign-keys to
|
||||
// AGRoomRuntimeConfig.Locations[].ReplicationId. Resolved via
|
||||
// AGRoomSettings.TryGetRoomSceneLocationById (RVA 0x10F57D0). Default
|
||||
|
||||
@@ -140,66 +140,48 @@ public class MatchController(
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>GET <c>photon_access_token</c> — read by the room-permissions
|
||||
/// runtime as <c>ALIIIAGAPND {RoomInstanceId, PhotonAccessToken,
|
||||
/// Permissions}</c> (NLDBPDCNNCF.CEOPCANHPPH, verb 0, issued from the
|
||||
/// rooms client, hence the <c>roomserver/</c> twin).
|
||||
/// <c>Permissions</c> is the role-permission matrix of the sub-room the
|
||||
/// player is in: the defaults plus whatever the room's editors saved with
|
||||
/// <c>PUT rooms/{id}/subrooms/{sub}/permissions</c>
|
||||
/// (<see cref="RoomPermissionMatrix"/>). It must not be empty: with no
|
||||
/// Permissions the client falls into its persisted-role migration path,
|
||||
/// which NREs during first spawn.</summary>
|
||||
[HttpGet("/photon_access_token")]
|
||||
public IActionResult PhotonAccessToken()
|
||||
[HttpGet("/roomserver/photon_access_token")]
|
||||
public async Task<IActionResult> PhotonAccessToken()
|
||||
{
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var appId = config["Photon:AppId"] ?? string.Empty;
|
||||
var voiceAppId = config["Photon:VoiceAppId"] ?? string.Empty;
|
||||
var region = (config["Photon:CloudRegion"] ?? "us").ToLowerInvariant();
|
||||
var currentRoom = presence.GetRoom(pid);
|
||||
var permissions = BuildPhotonPermissionSeed();
|
||||
var permissions = currentRoom is null
|
||||
? RoomPermissionMatrix.Merge(Array.Empty<RoomRolePermissionEntity>())
|
||||
: await RoomPermissionMatrix.LoadAsync(db, currentRoom.RoomId, currentRoom.SubRoomId);
|
||||
logger.LogInformation(
|
||||
"[photon-token] player={PlayerId} roomInstance={RoomInstanceId} permissions={PermissionCount}",
|
||||
pid, currentRoom?.RoomInstanceId ?? 0L, permissions.Length);
|
||||
return Ok(new Dictionary<string, object>
|
||||
{
|
||||
// 2023 deserializes this endpoint as ALIIIAGAPND before room
|
||||
// permissions initialization. If Permissions is missing/empty,
|
||||
// OMJKHJLFOCO falls into the persisted-role migration path that
|
||||
// currently NREs during first spawn.
|
||||
["RoomInstanceId"] = currentRoom?.RoomInstanceId ?? 0L,
|
||||
["PhotonAccessToken"] = string.Empty,
|
||||
["Permissions"] = permissions,
|
||||
"[photon-token] player={PlayerId} room={RoomId} sub={SubRoomId} roomInstance={RoomInstanceId} permissions={PermissionCount}",
|
||||
pid, currentRoom?.RoomId ?? 0L, currentRoom?.SubRoomId ?? 0L, currentRoom?.RoomInstanceId ?? 0L, permissions.Count);
|
||||
|
||||
// Existing custom-auth/app-id shape kept for Photon auth callers.
|
||||
["ResultCode"] = 1,
|
||||
["Message"] = string.Empty,
|
||||
["UserId"] = pid.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
["PhotonAppId"] = appId,
|
||||
["PhotonVoiceAppId"] = voiceAppId,
|
||||
["AppId"] = appId,
|
||||
["VoiceAppId"] = voiceAppId,
|
||||
["CloudRegion"] = region,
|
||||
["Region"] = region,
|
||||
["Token"] = string.Empty,
|
||||
});
|
||||
var body = RoomPermissionMatrix.AccessTokenBody(currentRoom?.RoomInstanceId ?? 0L, permissions);
|
||||
// Existing custom-auth/app-id shape kept for Photon auth callers; the
|
||||
// ALIIIAGAPND reader ignores keys it does not register.
|
||||
body["ResultCode"] = 1;
|
||||
body["Message"] = string.Empty;
|
||||
body["UserId"] = pid.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
body["PhotonAppId"] = appId;
|
||||
body["PhotonVoiceAppId"] = voiceAppId;
|
||||
body["AppId"] = appId;
|
||||
body["VoiceAppId"] = voiceAppId;
|
||||
body["CloudRegion"] = region;
|
||||
body["Region"] = region;
|
||||
body["Token"] = string.Empty;
|
||||
return Ok(body);
|
||||
}
|
||||
|
||||
private static object[] BuildPhotonPermissionSeed()
|
||||
=>
|
||||
[
|
||||
BuildPhotonPermission(13, 30), // Can Use Maker Pen
|
||||
BuildPhotonPermission(20, 30), // Can Spawn Inventions
|
||||
BuildPhotonPermission(21, 30), // Can Spawn Consumables
|
||||
BuildPhotonPermission(22, 30), // Can Use Room Reset Button
|
||||
BuildPhotonPermission(23, 30), // Can Use Play Gizmos Toggle
|
||||
BuildPhotonPermission(13, 255), // Creator alias
|
||||
BuildPhotonPermission(20, 255),
|
||||
BuildPhotonPermission(21, 255),
|
||||
BuildPhotonPermission(22, 255),
|
||||
BuildPhotonPermission(23, 255),
|
||||
];
|
||||
|
||||
private static object BuildPhotonPermission(int permission, int role) => new
|
||||
{
|
||||
Permission = permission.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
Role = role,
|
||||
Override = true,
|
||||
Type = 0,
|
||||
Value = "true",
|
||||
};
|
||||
|
||||
[HttpPost("/matchmake/dorm")]
|
||||
public async Task<IActionResult> MatchmakeDorm()
|
||||
{
|
||||
@@ -490,97 +472,125 @@ public class MatchController(
|
||||
return Ok(session);
|
||||
}
|
||||
|
||||
/// <summary>GET <c>match.*/room/{id}/instances</c> — the
|
||||
/// "RoomInstanceBrowser" the watch shows when you tap a room tile.
|
||||
/// Returns the currently-active instances so the player can see
|
||||
/// which one their friends are in (and join it) instead of being
|
||||
/// matchmade into a fresh empty room. Verified URL at
|
||||
/// Matchmaking.txt:6286; response is a
|
||||
/// <c>List<SimpleRoomInstance></c> per
|
||||
/// <c>SimpleRoomInstance.cs</c>: RoomInstanceId/RoomId/SubRoomId,
|
||||
/// IsFull, CreatedAt, PlayerIds, SubroomName, PlayerCount,
|
||||
/// HasModPresent, HashedInstanceId.
|
||||
/// <summary>GET <c>match.*/room/{id}/instances</c> — the room instance
|
||||
/// browser.
|
||||
///
|
||||
/// We derive instances from PlayerPresenceService's per-player
|
||||
/// RoomInstanceDto: every distinct PhotonRoomId in the same RoomId
|
||||
/// is one instance, with the players currently presenced there.
|
||||
/// PrivateInstances are surfaced as well so the inviter and
|
||||
/// invitee see the same room in the browser.</summary>
|
||||
/// <para><b>Client contract.</b> <c>RecNet.Matchmaking.KAIPGDGACGG(long)</c>
|
||||
/// (RecNet.Runtime/RecNet/Matchmaking.txt:13966, route at :14237, verb 0 =
|
||||
/// GET on the matchmaking host) returns <c>List<PNDCMIMEJLD></c>, a bare
|
||||
/// array. The reader (JPNMBICAMEE.txt) takes exactly RoomInstanceId,
|
||||
/// RoomId, SubRoomId, IsFull, CreatedAt and PlayerIds; the post-parse hook
|
||||
/// (PNDCMIMEJLD.FKDDCNLJOLF) derives PlayerCount from PlayerIds.Count,
|
||||
/// SubroomName from the cached room details' SubRooms by SubRoomId, and
|
||||
/// HasModPresent from the room's Roles. The screen
|
||||
/// (RoomInstanceBrowserScreen / RoomInstancePagedGrid) shows every row it
|
||||
/// is given, current instance first then oldest CreatedAt first, and
|
||||
/// renders the age as UtcNow - CreatedAt, so CreatedAt must be UTC.</para>
|
||||
///
|
||||
/// <para><b>What is listed.</b> Every live instance of the room: each
|
||||
/// distinct Photon room in which at least one player is presenced with a
|
||||
/// live activity marker (<see cref="PlayerPresenceService.EnumerateActiveInstances"/>).
|
||||
/// Private instances are included only for callers allowed into them —
|
||||
/// the room creator, accepted role holders, the instance owner, its
|
||||
/// invitees, and players already inside — so another room's invite-only
|
||||
/// match never leaks. Empty instances are not listed: there is nothing to
|
||||
/// join and they are what made the old list stale.</para>
|
||||
///
|
||||
/// <para><b>Fixed defects.</b> The Redis presence path returned nothing at
|
||||
/// all, so the browser was empty on every multi-replica deployment;
|
||||
/// private instances (every user room is born private and has one
|
||||
/// instance per sub-room) were dropped from presence and re-added only
|
||||
/// for their owner with an empty PlayerIds, so they showed 0 players or
|
||||
/// nothing; expired or crashed presences still counted; IsFull was always
|
||||
/// false; and SubRoomId used the OrderIndex, which does not match the
|
||||
/// room details' wire ids.</para></summary>
|
||||
[HttpGet("/room/{roomId:long}/instances")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RoomInstances(long roomId)
|
||||
{
|
||||
var presenced = presence.EnumerateActiveInstances(roomId).ToList();
|
||||
// Only surface the caller's OWN private instances — we never
|
||||
// leak someone else's PhotonRoomId into the browser. The Photon
|
||||
// join check still requires CanJoinAsync, but redacting at the
|
||||
// list level matches the existing RoomInstanceDto.Redact policy.
|
||||
var pid = this.CurrentPlayerId() ?? 0;
|
||||
var privates = pid == 0
|
||||
? new List<Data.Entities.PrivateInstanceEntity>()
|
||||
: await db.PrivateInstances
|
||||
.Where(p => p.RoomId == roomId && p.OwnerPlayerId == pid)
|
||||
.ToListAsync();
|
||||
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return Ok(Array.Empty<object>());
|
||||
|
||||
// Merge: presence rows are authoritative (player currently
|
||||
// there); add any owned private instance the inviter created
|
||||
// but nobody's joined yet so the inviter can still see it.
|
||||
var byKey = new Dictionary<string, SimpleInstance>(StringComparer.Ordinal);
|
||||
foreach (var (room, playerIds) in presenced)
|
||||
var live = presence.EnumerateActiveInstances(roomId, includePrivate: true).ToList();
|
||||
|
||||
var privateIds = live.Where(l => l.Room.IsPrivate).Select(l => l.Room.RoomInstanceId).Distinct().ToList();
|
||||
var privateRows = privateIds.Count == 0
|
||||
? new Dictionary<long, PrivateInstanceEntity>()
|
||||
: await db.PrivateInstances.AsNoTracking()
|
||||
.Where(p => privateIds.Contains(p.Id))
|
||||
.ToDictionaryAsync(p => p.Id);
|
||||
|
||||
var callerHasRole = pid != 0 && (room.CreatorPlayerId == pid
|
||||
|| await db.RoomRoles.AsNoTracking().AnyAsync(r =>
|
||||
r.RoomId == roomId && r.PlayerId == pid && r.Accepted));
|
||||
var invitedTo = pid == 0 || privateIds.Count == 0
|
||||
? new HashSet<long>()
|
||||
: (await db.PrivateInstanceInvitees.AsNoTracking()
|
||||
.Where(i => i.PlayerId == pid && privateIds.Contains(i.PrivateInstanceId))
|
||||
.Select(i => i.PrivateInstanceId)
|
||||
.ToListAsync()).ToHashSet();
|
||||
|
||||
var rows = new List<RoomInstanceRow>();
|
||||
foreach (var (inst, playerIds) in live)
|
||||
{
|
||||
if (string.IsNullOrEmpty(room.PhotonRoomId)) continue;
|
||||
byKey[room.PhotonRoomId] = new SimpleInstance(
|
||||
RoomInstanceId: room.RoomInstanceId,
|
||||
RoomId: room.RoomId,
|
||||
SubRoomId: room.SubRoomId,
|
||||
IsFull: false,
|
||||
CreatedAt: DateTime.UtcNow,
|
||||
PlayerIds: playerIds.Select(p => (int)p).ToList(),
|
||||
SubroomName: room.Name ?? string.Empty,
|
||||
PlayerCount: playerIds.Count,
|
||||
HasModPresent: false,
|
||||
HashedInstanceId: room.PhotonRoomId);
|
||||
}
|
||||
foreach (var p in privates)
|
||||
{
|
||||
if (string.IsNullOrEmpty(p.PhotonRoomId) || byKey.ContainsKey(p.PhotonRoomId)) continue;
|
||||
byKey[p.PhotonRoomId] = new SimpleInstance(
|
||||
RoomInstanceId: p.Id,
|
||||
RoomId: p.RoomId,
|
||||
SubRoomId: p.SubRoomId,
|
||||
IsFull: false,
|
||||
CreatedAt: DateTime.SpecifyKind(p.CreatedAt, DateTimeKind.Utc),
|
||||
PlayerIds: new List<int>(),
|
||||
SubroomName: p.Name,
|
||||
PlayerCount: 0,
|
||||
HasModPresent: false,
|
||||
HashedInstanceId: p.PhotonRoomId);
|
||||
privateRows.TryGetValue(inst.RoomInstanceId, out var privateRow);
|
||||
if (inst.IsPrivate && !CanSeePrivateInstance(
|
||||
pid, callerHasRole, playerIds, privateRow, invitedTo.Contains(inst.RoomInstanceId)))
|
||||
continue;
|
||||
|
||||
var capacity = inst.MaxCapacity > 0 ? inst.MaxCapacity : privateRow?.MaxCapacity ?? 0;
|
||||
rows.Add(new RoomInstanceRow(
|
||||
RoomInstanceId: inst.RoomInstanceId,
|
||||
RoomId: inst.RoomId,
|
||||
SubRoomId: inst.SubRoomId,
|
||||
IsFull: capacity > 0 && playerIds.Count >= capacity,
|
||||
CreatedAt: privateRow is not null
|
||||
? DateTime.SpecifyKind(privateRow.CreatedAt, DateTimeKind.Utc)
|
||||
: DateTime.UtcNow,
|
||||
PlayerIds: playerIds.Distinct().OrderBy(p => p).ToList()));
|
||||
}
|
||||
|
||||
// Wire shape: SimpleRoomInstance.Deserialize reads CAMELCASE
|
||||
// keys — verified at Cpp2IL_ISIL/.../RecNet/SimpleRoomInstance.txt
|
||||
// lines 765-808 ({roomInstanceId, roomId, subRoomId, isFull,
|
||||
// createdAt, playerIds}). SubroomName / PlayerCount /
|
||||
// HasModPresent / HashedInstanceId are populated locally on the
|
||||
// watch (PlayerIds.Count, local hash, etc.) — not read from
|
||||
// JSON, so we don't send them. Earlier PascalCase response made
|
||||
// every field default to 0/null on the watch, breaking the
|
||||
// browser.
|
||||
return Ok(byKey.Values.Select(i => new
|
||||
{
|
||||
roomInstanceId = i.RoomInstanceId,
|
||||
roomId = i.RoomId,
|
||||
subRoomId = i.SubRoomId,
|
||||
isFull = i.IsFull,
|
||||
createdAt = i.CreatedAt,
|
||||
playerIds = i.PlayerIds.ToArray(),
|
||||
}));
|
||||
logger.LogInformation(
|
||||
"[room-instances] room={Room} caller={Caller} live={Live} listed={Listed}",
|
||||
roomId, pid, live.Count, rows.Count);
|
||||
return Ok(rows.OrderBy(r => r.CreatedAt).Select(ToWire));
|
||||
}
|
||||
|
||||
private sealed record SimpleInstance(
|
||||
/// <summary>Who may see (and so be offered a join into) a private
|
||||
/// instance in the browser.</summary>
|
||||
public static bool CanSeePrivateInstance(
|
||||
long callerId,
|
||||
bool callerHasRoomRole,
|
||||
IReadOnlyCollection<long> playersInside,
|
||||
PrivateInstanceEntity? privateRow,
|
||||
bool callerInvited)
|
||||
{
|
||||
if (callerId == 0) return false;
|
||||
if (callerHasRoomRole) return true;
|
||||
if (playersInside.Contains(callerId)) return true;
|
||||
if (privateRow is not null && privateRow.OwnerPlayerId == callerId) return true;
|
||||
return callerInvited;
|
||||
}
|
||||
|
||||
internal sealed record RoomInstanceRow(
|
||||
long RoomInstanceId, long RoomId, long SubRoomId, bool IsFull,
|
||||
DateTime CreatedAt, List<int> PlayerIds, string SubroomName,
|
||||
int PlayerCount, bool HasModPresent, string HashedInstanceId);
|
||||
DateTime CreatedAt, List<long> PlayerIds);
|
||||
|
||||
/// <summary>PNDCMIMEJLD wire row. Account ids are Int32 on this DTO;
|
||||
/// SubRoomId is the wire id the room details advertise
|
||||
/// (<see cref="RoomSceneIds"/>), which the client uses to name the row.
|
||||
/// The reader matches PascalCase, camelCase and lowercase keys; camelCase
|
||||
/// is what this route has always sent.</summary>
|
||||
internal static object ToWire(RoomInstanceRow r) => new
|
||||
{
|
||||
roomInstanceId = r.RoomInstanceId,
|
||||
roomId = r.RoomId,
|
||||
subRoomId = RoomSceneIds.ToWire(r.SubRoomId),
|
||||
isFull = r.IsFull,
|
||||
createdAt = r.CreatedAt,
|
||||
playerIds = r.PlayerIds.Select(p => (int)p).ToArray(),
|
||||
};
|
||||
|
||||
/// <summary>DELETE <c>match.*/invite/{id}</c> — Matchmaking.cs
|
||||
/// DeclineRoomInvite. Verified URL at Matchmaking.txt:6505 (method
|
||||
|
||||
@@ -11,6 +11,7 @@ public class DorkNetDbContext(DbContextOptions<DorkNetDbContext> options) : DbCo
|
||||
public DbSet<RelationshipEntity> Relationships => Set<RelationshipEntity>();
|
||||
public DbSet<RoomEntity> Rooms => Set<RoomEntity>();
|
||||
public DbSet<RoomRoleEntity> RoomRoles => Set<RoomRoleEntity>();
|
||||
public DbSet<RoomRolePermissionEntity> RoomRolePermissions => Set<RoomRolePermissionEntity>();
|
||||
public DbSet<LeaderboardChannelMetaEntity> LeaderboardChannelMeta => Set<LeaderboardChannelMetaEntity>();
|
||||
public DbSet<RoomBookmarkEntity> RoomBookmarks => Set<RoomBookmarkEntity>();
|
||||
public DbSet<RoomDataBlobEntity> RoomDataBlobs => Set<RoomDataBlobEntity>();
|
||||
@@ -134,6 +135,14 @@ public class DorkNetDbContext(DbContextOptions<DorkNetDbContext> options) : DbCo
|
||||
e.HasIndex(p => new { p.ClubId, p.MembershipType }).IsUnique();
|
||||
});
|
||||
|
||||
// One cell per (room, sub-room, role, permission): the sub-room
|
||||
// permissions PUT upserts the cells it was sent.
|
||||
modelBuilder.Entity<RoomRolePermissionEntity>(e =>
|
||||
{
|
||||
e.HasKey(p => p.Id);
|
||||
e.HasIndex(p => new { p.RoomId, p.SubRoomId, p.Role, p.Permission }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ClubAdditionalImageEntity>(e =>
|
||||
{
|
||||
e.HasKey(i => i.Id);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DorkNet.Server.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// One cell of a sub-room's role-permission matrix, as written by
|
||||
/// <c>PUT rooms/{roomId}/subrooms/{subRoomId}/permissions</c> and served back
|
||||
/// in the <c>Permissions</c> list of <c>GET photon_access_token</c>.
|
||||
///
|
||||
/// The 2023-03-21 client models a cell as <c>BNLPBMJJOMM</c>
|
||||
/// (formatter <c>RecNet.Runtime/HDOJMEBCAGP.txt</c>, keys
|
||||
/// <c>Permission</c> / <c>Role</c> / <c>Override</c> / <c>Type</c> /
|
||||
/// <c>Value</c>): Permission and Value are strings, Role is the room-role
|
||||
/// enum <c>OMMBGJMJJPN</c> (None 0, Host 10, Moderator 20, CoOwner 30,
|
||||
/// TemporaryCoOwner 31, Creator 255), Type is <c>FEOJFGECKGH</c>
|
||||
/// (Bool 0, Int 1, StringList 2) and Override is a bool.
|
||||
///
|
||||
/// The editor sends only the cells it changed (the room-permissions runtime
|
||||
/// builds a one-element <c>BNLPBMJJOMM[]</c> per edit,
|
||||
/// <c>OMJKHJLFOCO.AFHJLBBFAGP.<LocalRequestModifyRolePermission>g__ChangeAndRefresh</c>),
|
||||
/// so the write is an upsert per (room, sub-room, role, permission) and never
|
||||
/// a replace of the whole matrix.
|
||||
/// </summary>
|
||||
public class RoomRolePermissionEntity
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long RoomId { get; set; }
|
||||
|
||||
/// <summary>The wire sub-room id from the route (the same value the room
|
||||
/// details advertise as <c>SubRooms[].SubRoomId</c>).</summary>
|
||||
public long SubRoomId { get; set; }
|
||||
|
||||
/// <summary>Wire room-role value (OMMBGJMJJPN), e.g. 30 = CoOwner, 255 = Creator.</summary>
|
||||
public int Role { get; set; }
|
||||
|
||||
/// <summary>The permission id exactly as the client sent it.</summary>
|
||||
[MaxLength(128)]
|
||||
public string Permission { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>FEOJFGECKGH: 0 = Bool, 1 = Int, 2 = StringList.</summary>
|
||||
public int Type { get; set; }
|
||||
|
||||
public bool Override { get; set; }
|
||||
|
||||
/// <summary>The value exactly as the client sent it (a string on the wire).</summary>
|
||||
[MaxLength(4096)]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
public long UpdatedByPlayerId { get; set; }
|
||||
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,3718 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DorkNet.Server.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DorkNet.Server.Migrations
|
||||
{
|
||||
[DbContext(typeof(DorkNetDbContext))]
|
||||
[Migration("20261024201104_RoomRolePermissions")]
|
||||
partial class RoomRolePermissions
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.4");
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.AdminActionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("AdminPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("TargetId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TargetType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AdminPlayerId");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.HasIndex("TargetType", "TargetId");
|
||||
|
||||
b.ToTable("AdminActions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.AvatarEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CustomAvatarItemsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EquippedItemsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FaceFeatures")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HairColor")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("InventoryJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OutfitSelections")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OutfitSelectionsV2")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SavedOutfitsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SkinColor")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Avatars");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.BugReportEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Body")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClientVersion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("GameSessionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ReadAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("ReporterPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ReadAt");
|
||||
|
||||
b.HasIndex("ReporterPlayerId");
|
||||
|
||||
b.ToTable("BugReports");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.CardEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ActionUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Body")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ExpiresAt");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.ToTable("Cards");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.CharadesWordListEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsBuiltIn")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("WordsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("CharadesWordLists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ChatMessageEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Body")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ModerationState")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("SenderPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SentAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ThreadKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ThreadKey", "SentAt");
|
||||
|
||||
b.ToTable("ChatMessages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ChatThreadEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ThreadKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(96)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ThreadKey")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ChatThreads");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ChatThreadMemberEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("LastReadMessageId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("SnoozeUntil")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ThreadKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.HasIndex("ThreadKey", "PlayerId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ChatThreadMembers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.CheerEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CheeredAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("FromPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TargetInventionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TargetPhotoId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TargetPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TargetRoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TargetPhotoId");
|
||||
|
||||
b.HasIndex("TargetPlayerId");
|
||||
|
||||
b.HasIndex("TargetRoomId");
|
||||
|
||||
b.HasIndex("FromPlayerId", "TargetPlayerId", "TargetRoomId", "TargetPhotoId", "Type")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Cheers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubAdditionalImageEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("ClubId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Slot")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClubId", "Slot")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ClubAdditionalImages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubAnnouncementEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("AuthorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Body")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("ClubId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClubId", "CreatedAt");
|
||||
|
||||
b.ToTable("ClubAnnouncements");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubAnnouncementReadEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("AnnouncementId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.HasIndex("AnnouncementId", "PlayerId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ClubAnnouncementReads");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubCategoryAssignmentEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("CategoryTagId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("ClubId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryTagId");
|
||||
|
||||
b.HasIndex("ClubId", "CategoryTagId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ClubCategoryAssignments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubCategoryTagEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("OrderIndex")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("ClubCategoryTags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowJuniors")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BanStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("ClubChatEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ClubType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("ClubhouseRoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsRRO")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Joinability")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MinLevel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT")
|
||||
.UseCollation("NOCASE");
|
||||
|
||||
b.Property<int>("State")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Visibility")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorPlayerId");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Clubs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubMembershipEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("ClubId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Permissions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.HasIndex("ClubId", "PlayerId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ClubMemberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubRolePermissionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ApproveMember")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("BanUnban")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("ClubId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("CreateEvent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EditDetails")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EditPermissionSettings")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MembershipType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("PostAnnouncement")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClubId", "MembershipType")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ClubRolePermissions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubSubscriptionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("ClubId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SubscribedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClubId");
|
||||
|
||||
b.HasIndex("PlayerId", "ClubId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ClubSubscriptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.CohortAssignmentEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("AssignedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CohortKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Variant")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId", "CohortKey")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CohortAssignments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.CommunityBoardEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Json")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("CommunityBoardRows");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.CouponEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxRedemptions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RedemptionCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RewardAmount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RewardCurrencyType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RewardItemSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RewardType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Coupons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.CouponRedemptionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("CouponId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("RedeemedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CouponId", "PlayerId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CouponRedemptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.CurrencyBalanceEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Balance")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CurrencyType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId", "CurrencyType")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CurrencyBalances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.CustomAvatarItemEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AssetName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("BaseAvatarItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("CheerCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Color")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsFeatured")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsPublic")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Price")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("PublicId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PurchaseCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorPlayerId");
|
||||
|
||||
b.HasIndex("IsFeatured");
|
||||
|
||||
b.HasIndex("IsPublic");
|
||||
|
||||
b.HasIndex("PublicId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CustomAvatarItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.CustomAvatarItemOwnershipEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("AcquiredAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CustomAvatarItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CustomAvatarItemId");
|
||||
|
||||
b.HasIndex("PlayerId", "CustomAvatarItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CustomAvatarItemOwnership");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.DormStateEntity", b =>
|
||||
{
|
||||
b.Property<long>("PlayerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CurrentDataBlobName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("PlayerId");
|
||||
|
||||
b.ToTable("DormStates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.GameRewardSelectionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("GiftContext")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("GrantedItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("Offer1ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Offer2ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Offer3ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RewardType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("SelectedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SelectedGiftDropId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId", "SelectedAt");
|
||||
|
||||
b.ToTable("GameRewardSelections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.GameSessionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ActivityLevelId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MaxCapacity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PhotonRoomName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PlayerCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Region")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoomId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoomId", "Region");
|
||||
|
||||
b.ToTable("GameSessions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.GiftPackageEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AvatarItemDescOrHairDyeDesc")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("AvatarItemType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConsumableItemDesc")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Consumed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ConsumedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Currency")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CurrencyType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("EquipmentModificationGuid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EquipmentPrefabName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("FromPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("GiftContext")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("GiftRarity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsGifted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsValid")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PackageMaterial")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PackageVariant")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RecipientPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("SourceStoreItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsCurrentPlatform")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Xp")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RecipientPlayerId", "Consumed");
|
||||
|
||||
b.ToTable("GiftPackages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.InventionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CheerCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("CreationRoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CreatorPermission")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CurrentBlobName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CurrentVersionNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("FirstPublishedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("GeneralPermission")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAgInvention")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsPublished")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("NumPlayersHaveUsedInRoom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Permission")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Price")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ReplicationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SpawnCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TagsCsv")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CheerCount");
|
||||
|
||||
b.HasIndex("CreatorPlayerId");
|
||||
|
||||
b.ToTable("Inventions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.InventionVersionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BlobName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("InstantiationCost")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("InventionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LightsCost")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ReplicationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("VersionNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InventionId", "VersionNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InventionVersions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.IpBanEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("BannedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("BannedByAdminId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Cidr")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Until")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Cidr");
|
||||
|
||||
b.ToTable("IpBans");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ItemWishlistEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId", "ItemKey", "ItemType")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ItemWishlists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.KeepsakeEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("EarnedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EventKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.HasIndex("PlayerId", "EventKey");
|
||||
|
||||
b.ToTable("Keepsakes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.LeaderboardChannelMetaEntity", b =>
|
||||
{
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Channel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("LowerIsBetter")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ValueFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("RoomId", "Channel");
|
||||
|
||||
b.HasIndex("RoomId");
|
||||
|
||||
b.ToTable("LeaderboardChannelMeta");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.LeaderboardStatEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("StatChannel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("Value")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoomId", "PlayerId", "StatChannel")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RoomId", "StatChannel", "Value");
|
||||
|
||||
b.ToTable("LeaderboardStats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.LoadingScreenTipEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Context")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PlatformMask")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RoomNamesCsv")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("LoadingScreenTips");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.MessageEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Body")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ReadAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RecipientPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("SenderPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SentAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RecipientPlayerId", "SentAt");
|
||||
|
||||
b.HasIndex("SenderPlayerId", "SentAt");
|
||||
|
||||
b.ToTable("Messages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.NotificationPrefsEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowAll")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowAnnouncements")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowEventInvite")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowFriendRequest")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowMessage")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId", "Platform")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("NotificationPrefs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ObjectiveProgressEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ClearedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId", "Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ObjectiveProgress");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PendingDeviceEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("FirstSeenAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LastIp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastSeenAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PlatformId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingDevices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PhotoEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BlobName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Caption")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CheerCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsPublic")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TaggedPlayerIdsCsv")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("UploaderPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ViewCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlobName");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("RoomId");
|
||||
|
||||
b.HasIndex("UploaderPlayerId");
|
||||
|
||||
b.ToTable("Photos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlatformIgnoreEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PlatformUserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId", "Platform", "PlatformUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PlatformIgnores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerDeviceEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("FirstSeen")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsBanned")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastSeen")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.ToTable("PlayerDevices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerEloEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Elo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("GameMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Losses")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MatchesPlayed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Wins")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GameMode", "Elo");
|
||||
|
||||
b.HasIndex("PlayerId", "GameMode")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PlayerElo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("BannedUntil")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("BannerImageName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Birthday")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("CanReceiveInvites")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedFromDeviceId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayEmoji")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("IdentityFlags")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsCommunityTeam")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsDeveloper")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsJunior")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsVerified")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LastIp")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LastPlatform")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LastPlatformId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastSeenAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PersonalPronouns")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProfileImageName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Reputation")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("XP")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Players");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerEventEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Capacity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("EndsAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("StartsAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorPlayerId");
|
||||
|
||||
b.HasIndex("StartsAt");
|
||||
|
||||
b.ToTable("PlayerEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerEventResponseEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("EventId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Response")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.HasIndex("EventId", "PlayerId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PlayerEventResponses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerInventoryEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("AcquiredAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ItemSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.HasIndex("PlayerId", "ItemSlug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PlayerInventory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerSettingEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId", "Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PlayerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlaylistEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Accessibility")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CheerCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CustomWarning")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("FavoriteCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsCurated")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT")
|
||||
.UseCollation("NOCASE");
|
||||
|
||||
b.Property<int>("OrderIndex")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsJuniors")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsLevelVoting")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsScreens")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsTeleportVR")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsWalkVR")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TagsCsv")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("VisitCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VisitorCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("WarningMask")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorPlayerId");
|
||||
|
||||
b.HasIndex("IsCurated");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Playlists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlaylistInteractionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Cheered")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Favorited")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlaylistId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.HasIndex("PlaylistId", "PlayerId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PlaylistInteractions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlaylistRoomEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("OrderIndex")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlaylistId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaylistId");
|
||||
|
||||
b.HasIndex("RoomId");
|
||||
|
||||
b.HasIndex("PlaylistId", "OrderIndex");
|
||||
|
||||
b.ToTable("PlaylistRooms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PrivateInstanceEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DataBlob")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MaxCapacity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("OwnerPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PhotonRegion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhotonRoomId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoomCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("SubRoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OwnerPlayerId");
|
||||
|
||||
b.HasIndex("RoomId", "SubRoomId");
|
||||
|
||||
b.ToTable("PrivateInstances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PrivateInstanceInviteeEntity", b =>
|
||||
{
|
||||
b.Property<long>("PrivateInstanceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("InvitedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("LatestInviteMessageId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("PrivateInstanceId", "PlayerId");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.ToTable("PrivateInstanceInvitees");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PushTokenEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token");
|
||||
|
||||
b.HasIndex("PlayerId", "Platform")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PushTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RelationshipEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Favorited")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Ignored")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Muted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RequesterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TargetId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RequesterId", "TargetId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Relationships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ReportEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Category")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("GameSessionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("ReporterPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ResolutionNote")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ResolvedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("ResolverAdminId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TargetEventId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TargetInventionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TargetPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TargetRoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ReporterPlayerId");
|
||||
|
||||
b.HasIndex("ResolvedAt");
|
||||
|
||||
b.HasIndex("TargetPlayerId");
|
||||
|
||||
b.ToTable("Reports");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomBanEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BanType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("BannedByPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("BannedPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("Until")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BannedPlayerId");
|
||||
|
||||
b.HasIndex("RoomId", "BannedPlayerId");
|
||||
|
||||
b.ToTable("RoomBans");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomBookmarkEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.HasIndex("PlayerId", "RoomId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RoomBookmarks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomConsumableEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("CurrencyId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("Price")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("PublicId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorPlayerId");
|
||||
|
||||
b.HasIndex("PublicId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RoomId");
|
||||
|
||||
b.ToTable("RoomConsumables");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomConsumableOwnershipEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ConcurrencyCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("ModifiedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomConsumableId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoomConsumableId");
|
||||
|
||||
b.HasIndex("PlayerId", "RoomConsumableId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RoomConsumableOwnership");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomCurrencyBalanceEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Balance")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomCurrencyId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoomCurrencyId");
|
||||
|
||||
b.HasIndex("PlayerId", "RoomCurrencyId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RoomCurrencyBalances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomCurrencyEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DailyLimit")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("PublicId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorPlayerId");
|
||||
|
||||
b.HasIndex("PublicId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RoomId");
|
||||
|
||||
b.ToTable("RoomCurrencies");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomCurrencyPurchaseOfferEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CurrencyType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Price")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("PublicId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomCurrencyId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PublicId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RoomCurrencyId");
|
||||
|
||||
b.ToTable("RoomCurrencyPurchaseOffers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomDataBlobEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BlobName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Bytes")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("ReferencedFilenamesCsv")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("SubRoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UploadedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("UploadedByPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlobName")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RoomId");
|
||||
|
||||
b.ToTable("RoomDataBlobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Accessibility")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowNewUsers")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowsJuniors")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CheerCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("CloningAllowed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CurrentDataBlobName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomRoomWarning")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DisableMicAutoMute")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("FavoriteCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("HiddenFromBrowse")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("HotScore")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAGRoom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsDormRoom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsRoomLinkedToRecRoomStudio")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsStudioRoom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LoadScreensJson")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LocationReplicationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MaxCapacity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxPlayerCalculationMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MinLevel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT")
|
||||
.UseCollation("NOCASE");
|
||||
|
||||
b.Property<string>("PromoExternalContentJson")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PromoImagesJson")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RoomWarningMask")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("State")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StudioSessionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("SupportsLevelVoting")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsMobile")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsScreens")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsTeleportVR")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsVRLow")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsWalkVR")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TagsCsv")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("VisitCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VisitorCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorPlayerId");
|
||||
|
||||
b.HasIndex("HotScore");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Rooms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomKeyEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(174)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Price")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ReplicationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorPlayerId");
|
||||
|
||||
b.HasIndex("RoomId");
|
||||
|
||||
b.HasIndex("RoomId", "Name");
|
||||
|
||||
b.ToTable("RoomKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomKeyPurchaseEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PaidPrice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("PurchasedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomKeyId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.HasIndex("RoomKeyId", "PlayerId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RoomKeyPurchases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomRoleEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Accepted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("GrantedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("GrantedByPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoomId");
|
||||
|
||||
b.HasIndex("RoomId", "PlayerId", "Role")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RoomRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomRolePermissionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Override")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Permission")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("SubRoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("UpdatedByPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoomId", "SubRoomId", "Role", "Permission")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RoomRolePermissions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomSceneEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("CanMatchmakeInto")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataBlobName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DataModifiedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSandbox")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxPlayers")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT")
|
||||
.UseCollation("NOCASE");
|
||||
|
||||
b.Property<int>("OrderIndex")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RoomSceneLocationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StudioAssetBundleNamesCsv")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("StudioSubRoomDataSaveId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StudioUnityAssetId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoomId", "Name");
|
||||
|
||||
b.HasIndex("RoomId", "OrderIndex")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RoomScenes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomVisitEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("FirstVisitAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastVisitAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VisitCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.HasIndex("RoomId", "LastVisitAt");
|
||||
|
||||
b.HasIndex("RoomId", "PlayerId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RoomVisits");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoyaleMatchEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChestsOpened")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CompletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("CustomGame")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("HealthPotionsConsumed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("NumEliminations")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Rank")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondsAlive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondsInAir")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ShieldPotionsConsumed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WalkGame")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CompletedAt");
|
||||
|
||||
b.ToTable("RoyaleMatches");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoyaleMatchPlayerEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("MatchId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("NumEliminations")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Rank")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondsAlive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId");
|
||||
|
||||
b.HasIndex("MatchId", "PlayerId");
|
||||
|
||||
b.ToTable("RoyaleMatchPlayers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoyalePlayerProgressEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("CurrentLevelXPThreshold")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("NextLevelAcornReward")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("NextLevelXPThreshold")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RankIdx")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RankName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("TotalXP")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlayerId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RoyalePlayerProgress");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.ServerSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllAvatarItemsOwned")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BaseRoomNamesJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CharadesSlotBindingsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CircuitsV2LimitsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DiscoveredGameConfigsJson")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<bool>("GlobalFriendsEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PlayMenuTagsJson")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<bool>("ProfanityFilterDisabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RecCenterDoorsJson")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<bool>("RoomBlobVersionClampDisabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SignupsDisabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("WeeklyChallengeRewardJson")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<bool>("WeeklyChallengesCompletedRequired")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("WeeklyChallengesJson")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ServerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.SignupCodeEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatedByPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Descriptor")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("RedeemedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("RedeemedByPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Revoked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SignupCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.StoreItemEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("AvailableUntil")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CurrencyType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsLimitedTime")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Price")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Storefront")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Storefront");
|
||||
|
||||
b.HasIndex("IsActive", "Category");
|
||||
|
||||
b.ToTable("StoreItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.SubscriptionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("SubscriberPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TargetPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SubscriberPlayerId");
|
||||
|
||||
b.HasIndex("TargetPlayerId");
|
||||
|
||||
b.HasIndex("SubscriberPlayerId", "TargetPlayerId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Subscriptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.TestCaseEntity", b =>
|
||||
{
|
||||
b.Property<long>("Pk")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AssignedPlayerIdsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AssignedPlayerNamesJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CommentsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("JiraBugUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("JiraUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MinNumAssignedPlayers")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RoomName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TagsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint?>("TestPassId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Pk");
|
||||
|
||||
b.HasIndex("Id")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("TestPassId");
|
||||
|
||||
b.ToTable("TestCases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.TestPassEntity", b =>
|
||||
{
|
||||
b.Property<uint>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("EndDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TagsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("WasManuallyClosed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TestPasses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.UgcPurchasableEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CurrencyType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsFeatured")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Price")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("PublicId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorPlayerId");
|
||||
|
||||
b.HasIndex("PublicId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RoomId");
|
||||
|
||||
b.ToTable("UgcPurchasables");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.AvatarEntity", b =>
|
||||
{
|
||||
b.HasOne("DorkNet.Server.Data.Entities.PlayerEntity", "Player")
|
||||
.WithOne("Avatar")
|
||||
.HasForeignKey("DorkNet.Server.Data.Entities.AvatarEntity", "PlayerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Player");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerSettingEntity", b =>
|
||||
{
|
||||
b.HasOne("DorkNet.Server.Data.Entities.PlayerEntity", "Player")
|
||||
.WithMany("Settings")
|
||||
.HasForeignKey("PlayerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Player");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RelationshipEntity", b =>
|
||||
{
|
||||
b.HasOne("DorkNet.Server.Data.Entities.PlayerEntity", "Requester")
|
||||
.WithMany("Relationships")
|
||||
.HasForeignKey("RequesterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Requester");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerEntity", b =>
|
||||
{
|
||||
b.Navigation("Avatar");
|
||||
|
||||
b.Navigation("Relationships");
|
||||
|
||||
b.Navigation("Settings");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DorkNet.Server.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RoomRolePermissions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RoomRolePermissions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
RoomId = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
SubRoomId = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
Role = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Permission = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Override = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 4096, nullable: false),
|
||||
UpdatedByPlayerId = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RoomRolePermissions", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RoomRolePermissions_RoomId_SubRoomId_Role_Permission",
|
||||
table: "RoomRolePermissions",
|
||||
columns: new[] { "RoomId", "SubRoomId", "Role", "Permission" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "RoomRolePermissions");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3021,6 +3021,51 @@ namespace DorkNet.Server.Migrations
|
||||
b.ToTable("RoomRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomRolePermissionEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Override")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Permission")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("SubRoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("UpdatedByPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoomId", "SubRoomId", "Role", "Permission")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RoomRolePermissions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomSceneEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
||||
@@ -311,7 +311,7 @@ public class NotificationService(
|
||||
{
|
||||
["roomInstanceId"] = redacted.RoomInstanceId,
|
||||
["roomId"] = redacted.RoomId,
|
||||
["subRoomId"] = redacted.SubRoomId,
|
||||
["subRoomId"] = redacted.WireSubRoomId,
|
||||
["roomInstanceType"] = redacted.RoomInstanceType,
|
||||
["location"] = redacted.Location,
|
||||
["dataBlob"] = redacted.DataBlob,
|
||||
|
||||
@@ -105,6 +105,15 @@ public class PlayerPresenceService
|
||||
var db = mux.GetDatabase();
|
||||
db.StringSet(Key(playerId), json, PresenceTtl);
|
||||
db.KeyDelete(NoRoomKey(playerId));
|
||||
// Per-room membership index for the instance browser
|
||||
// (EnumerateActiveInstances). Members are validated against
|
||||
// the player's live presence on read, so a player who moved
|
||||
// on is pruned lazily instead of needing the old room here.
|
||||
if (room is not null && room.RoomId > 0)
|
||||
{
|
||||
db.SetAdd(RoomIndexKey(room.RoomId), playerId);
|
||||
db.KeyExpire(RoomIndexKey(room.RoomId), PresenceTtl + PresenceTtl);
|
||||
}
|
||||
_log?.LogDebug("[presence] set redis player={PlayerId} room={Room} bytes={Bytes}",
|
||||
playerId, room?.Name ?? "<null>", json.Length);
|
||||
}
|
||||
@@ -459,45 +468,97 @@ public class PlayerPresenceService
|
||||
_localNoRoomExpires[playerId] = DateTimeOffset.UtcNow.Add(PresenceTtl);
|
||||
}
|
||||
|
||||
/// <summary>List the currently-active instances of a given room,
|
||||
/// grouped by PhotonRoomId, with the player ids currently presenced
|
||||
/// in each. Drives the watch's RoomInstanceBrowser
|
||||
/// (<c>GET match.*/room/{id}/instances</c>) so players can see
|
||||
/// which instance their friends are in.
|
||||
/// <summary>List the live instances of a given room, grouped by
|
||||
/// PhotonRoomId (or RoomInstanceId when a row has none), with the player
|
||||
/// ids currently presenced in each. Drives the room instance browser
|
||||
/// (<c>GET match.*/room/{id}/instances</c>) and the event instance list.
|
||||
///
|
||||
/// Redis caveat: without a secondary index per room we'd need a
|
||||
/// SCAN over every presence key — too expensive to do on each
|
||||
/// request. For now the Redis path returns empty and the local
|
||||
/// in-process dictionary is the source of truth. Single-instance
|
||||
/// deployments work; multi-replica Redis deployments need a
|
||||
/// per-room set index to be added before the browser is reliable
|
||||
/// there.</summary>
|
||||
public IEnumerable<(RoomInstanceDto Room, List<long> PlayerIds)> EnumerateActiveInstances(long roomId)
|
||||
/// <para>Only players whose presence is still live count: an expired
|
||||
/// room presence (<see cref="PresenceTtl"/>) is dropped, and so is a
|
||||
/// player whose activity marker (<see cref="ActivityTtl"/>, refreshed by
|
||||
/// every heartbeat and join) has lapsed. A client that crashed keeps its
|
||||
/// room presence for up to ten minutes, but it is not in the instance and
|
||||
/// must not inflate the player count or keep an empty instance listed.</para>
|
||||
///
|
||||
/// <para><b>Redis.</b> <see cref="SetRoom"/> records each player in a
|
||||
/// per-room set (<c>presence-room:{roomId}</c>); this reads that set and
|
||||
/// validates every member against their current presence, pruning the
|
||||
/// ones that have moved on or expired. Without the index the Redis path
|
||||
/// had nothing to enumerate and returned an empty list on every
|
||||
/// multi-replica deployment.</para>
|
||||
///
|
||||
/// <para>Private instances are skipped unless
|
||||
/// <paramref name="includePrivate"/> is set; callers that include them
|
||||
/// must apply their own visibility rule.</para></summary>
|
||||
public IEnumerable<(RoomInstanceDto Room, List<long> PlayerIds)> EnumerateActiveInstances(
|
||||
long roomId, bool includePrivate = false)
|
||||
{
|
||||
if (_redis is not null) return Array.Empty<(RoomInstanceDto, List<long>)>();
|
||||
var groups = new Dictionary<string, (RoomInstanceDto Room, List<long> PlayerIds)>(StringComparer.Ordinal);
|
||||
foreach (var kv in _local)
|
||||
var members = new List<(long PlayerId, RoomInstanceDto Room)>();
|
||||
if (_redis is { } mux)
|
||||
{
|
||||
var room = kv.Value;
|
||||
if (room is null || room.RoomId != roomId) continue;
|
||||
// Skip private instances — those are surfaced separately
|
||||
// in the controller via PrivateInstanceService so we never
|
||||
// leak a photonRoomId for an invite-only match into the
|
||||
// public browser.
|
||||
if (room.IsPrivate) continue;
|
||||
var key = string.IsNullOrEmpty(room.PhotonRoomId)
|
||||
? $"<{room.RoomInstanceId}>"
|
||||
: room.PhotonRoomId;
|
||||
try
|
||||
{
|
||||
var db = mux.GetDatabase();
|
||||
var stale = new List<RedisValue>();
|
||||
foreach (var member in db.SetMembers(RoomIndexKey(roomId)))
|
||||
{
|
||||
if (!long.TryParse(member.ToString(), out var playerId))
|
||||
{
|
||||
stale.Add(member);
|
||||
continue;
|
||||
}
|
||||
var room = GetRoom(playerId);
|
||||
if (room is null || room.RoomId != roomId)
|
||||
{
|
||||
stale.Add(member);
|
||||
continue;
|
||||
}
|
||||
members.Add((playerId, room));
|
||||
}
|
||||
if (stale.Count > 0)
|
||||
db.SetRemove(RoomIndexKey(roomId), stale.ToArray());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log?.LogWarning(ex, "[presence] redis room index scan failed room={RoomId}", roomId);
|
||||
return Array.Empty<(RoomInstanceDto, List<long>)>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var playerId in _local.Keys.ToList())
|
||||
{
|
||||
var room = GetRoom(playerId); // honours the room TTL
|
||||
if (room is not null && room.RoomId == roomId)
|
||||
members.Add((playerId, room));
|
||||
}
|
||||
}
|
||||
|
||||
var groups = new Dictionary<string, (RoomInstanceDto Room, List<long> PlayerIds)>(StringComparer.Ordinal);
|
||||
foreach (var (playerId, room) in members)
|
||||
{
|
||||
if (room.IsPrivate && !includePrivate) continue;
|
||||
if (!IsRecentlyActive(playerId)) continue;
|
||||
var key = InstanceKey(room);
|
||||
if (!groups.TryGetValue(key, out var entry))
|
||||
{
|
||||
entry = (room, new List<long>());
|
||||
groups[key] = entry;
|
||||
}
|
||||
entry.PlayerIds.Add(kv.Key);
|
||||
entry.PlayerIds.Add(playerId);
|
||||
}
|
||||
return groups.Values;
|
||||
}
|
||||
|
||||
/// <summary>The identity two presence rows share when they are in the
|
||||
/// same live instance: the Photon room, else the instance id.</summary>
|
||||
public static string InstanceKey(RoomInstanceDto room)
|
||||
=> string.IsNullOrEmpty(room.PhotonRoomId)
|
||||
? "<" + room.RoomInstanceId + ">"
|
||||
: room.PhotonRoomId;
|
||||
|
||||
private static string RoomIndexKey(long roomId) => "presence-room:" + roomId;
|
||||
|
||||
private static string Key(long playerId) => $"presence:{playerId}";
|
||||
private static string ActiveKey(long playerId) => $"presence-active:{playerId}";
|
||||
private static string BootDormGraceKey(long playerId) => $"presence-boot-dorm-grace:{playerId}";
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
using System.Text.Json;
|
||||
using DorkNet.Server.Data;
|
||||
using DorkNet.Server.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DorkNet.Server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A sub-room's role-permission matrix: the cells the 2023 client reads from
|
||||
/// <c>GET photon_access_token</c> (<c>ALIIIAGAPND.Permissions</c>, a list of
|
||||
/// <c>BNLPBMJJOMM</c>) and writes with
|
||||
/// <c>PUT rooms/{id}/subrooms/{sub}/permissions</c>.
|
||||
///
|
||||
/// <para>The served list is the built-in defaults with every stored cell for
|
||||
/// the room's sub-room applied on top: a stored cell replaces the default
|
||||
/// with the same (Role, Permission), and cells with no default are appended.
|
||||
/// The list must never be empty — with no Permissions the room-permissions
|
||||
/// runtime (OMJKHJLFOCO) falls into its persisted-role migration path, which
|
||||
/// NREs during first spawn.</para>
|
||||
///
|
||||
/// <para>Cell keys (formatter RecNet.Runtime/HDOJMEBCAGP.txt):
|
||||
/// <c>Permission</c> (string), <c>Role</c> (OMMBGJMJJPN), <c>Override</c>
|
||||
/// (bool), <c>Type</c> (FEOJFGECKGH: Bool 0 / Int 1 / StringList 2),
|
||||
/// <c>Value</c> (string). Role and Type go out as numbers; requests are
|
||||
/// accepted with numbers, numeric strings or enum names.</para>
|
||||
/// </summary>
|
||||
public static class RoomPermissionMatrix
|
||||
{
|
||||
/// <summary>One matrix cell.</summary>
|
||||
public sealed record Cell(string Permission, int Role, bool Override, int Type, string Value);
|
||||
|
||||
/// <summary>The defaults every sub-room starts from: Co-Owner and Creator
|
||||
/// may use the maker pen, spawn inventions and consumables, reset the
|
||||
/// room and toggle play gizmos.</summary>
|
||||
public static IReadOnlyList<Cell> Defaults { get; } = BuildDefaults();
|
||||
|
||||
private static IReadOnlyList<Cell> BuildDefaults()
|
||||
{
|
||||
var permissions = new[]
|
||||
{
|
||||
"13", // Can Use Maker Pen
|
||||
"20", // Can Spawn Inventions
|
||||
"21", // Can Spawn Consumables
|
||||
"22", // Can Use Room Reset Button
|
||||
"23", // Can Use Play Gizmos Toggle
|
||||
};
|
||||
var list = new List<Cell>();
|
||||
foreach (var role in new[] { RoomRoleRules.WireCoOwner, RoomRoleRules.WireCreator })
|
||||
foreach (var permission in permissions)
|
||||
list.Add(new Cell(permission, role, true, 0, "true"));
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>Defaults merged with the stored cells.</summary>
|
||||
public static List<Cell> Merge(IEnumerable<RoomRolePermissionEntity> stored)
|
||||
{
|
||||
var cells = Defaults.ToList();
|
||||
foreach (var row in stored.OrderBy(r => r.Id))
|
||||
{
|
||||
var cell = new Cell(row.Permission, row.Role, row.Override, row.Type, row.Value);
|
||||
var at = cells.FindIndex(c =>
|
||||
c.Role == row.Role
|
||||
&& string.Equals(c.Permission, row.Permission, StringComparison.OrdinalIgnoreCase));
|
||||
if (at >= 0) cells[at] = cell;
|
||||
else cells.Add(cell);
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
/// <summary>The served matrix for one sub-room (by OrderIndex).</summary>
|
||||
public static async Task<List<Cell>> LoadAsync(DorkNetDbContext db, long roomId, long subRoomOrderIndex)
|
||||
{
|
||||
var stored = await db.RoomRolePermissions.AsNoTracking()
|
||||
.Where(p => p.RoomId == roomId && p.SubRoomId == subRoomOrderIndex)
|
||||
.ToListAsync();
|
||||
return Merge(stored);
|
||||
}
|
||||
|
||||
/// <summary>The <c>ALIIIAGAPND</c> body: <c>{RoomInstanceId,
|
||||
/// PhotonAccessToken, Permissions}</c>. Used both by
|
||||
/// <c>GET photon_access_token</c> and the <c>PhotonAccessToken</c> hub
|
||||
/// push (IBEOONPEELF.txt:519 registers it as Action<ALIIIAGAPND>).</summary>
|
||||
public static Dictionary<string, object> AccessTokenBody(long roomInstanceId, IEnumerable<Cell> cells)
|
||||
=> new()
|
||||
{
|
||||
["RoomInstanceId"] = roomInstanceId,
|
||||
["PhotonAccessToken"] = string.Empty,
|
||||
["Permissions"] = cells.Select(ToWire).ToList(),
|
||||
};
|
||||
|
||||
public static object ToWire(Cell c) => new
|
||||
{
|
||||
c.Permission,
|
||||
c.Role,
|
||||
c.Override,
|
||||
c.Type,
|
||||
c.Value,
|
||||
};
|
||||
|
||||
/// <summary>Parse the PUT body: a JSON array of cells (an object wrapping
|
||||
/// a <c>Permissions</c> array is accepted too). Returns null when the body
|
||||
/// is not JSON or holds no array.</summary>
|
||||
public static List<Cell>? Parse(string body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body)) return null;
|
||||
JsonDocument doc;
|
||||
try { doc = JsonDocument.Parse(body); }
|
||||
catch (JsonException) { return null; }
|
||||
using (doc)
|
||||
{
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
root = root.EnumerateObject()
|
||||
.Where(p => p.Name.Equals("Permissions", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(p => p.Value)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
if (root.ValueKind != JsonValueKind.Array) return null;
|
||||
|
||||
var cells = new List<Cell>();
|
||||
foreach (var item in root.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind != JsonValueKind.Object) continue;
|
||||
string? permission = null;
|
||||
int? role = null;
|
||||
bool @override = false;
|
||||
int type = 0;
|
||||
string value = string.Empty;
|
||||
foreach (var prop in item.EnumerateObject())
|
||||
{
|
||||
switch (prop.Name.ToLowerInvariant())
|
||||
{
|
||||
case "permission":
|
||||
permission = Scalar(prop.Value)?.Trim();
|
||||
break;
|
||||
case "role":
|
||||
role = EnumValue(prop.Value, RoleName);
|
||||
break;
|
||||
case "type":
|
||||
type = EnumValue(prop.Value, TypeName) ?? 0;
|
||||
break;
|
||||
case "override":
|
||||
@override = prop.Value.ValueKind == JsonValueKind.True
|
||||
|| (prop.Value.ValueKind == JsonValueKind.String
|
||||
&& bool.TryParse(prop.Value.GetString(), out var b) && b);
|
||||
break;
|
||||
case "value":
|
||||
value = Scalar(prop.Value) ?? string.Empty;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrEmpty(permission) || role is null) continue;
|
||||
if (permission.Length > 128 || value.Length > 4096) continue;
|
||||
cells.Add(new Cell(permission, role.Value, @override, type, value));
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? Scalar(JsonElement e) => e.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => e.GetString(),
|
||||
JsonValueKind.Number => e.GetRawText(),
|
||||
JsonValueKind.True => "true",
|
||||
JsonValueKind.False => "false",
|
||||
JsonValueKind.Null or JsonValueKind.Undefined => null,
|
||||
_ => e.GetRawText(),
|
||||
};
|
||||
|
||||
private static int? EnumValue(JsonElement e, Func<string, int?> byName)
|
||||
{
|
||||
if (e.ValueKind == JsonValueKind.Number && e.TryGetInt32(out var n)) return n;
|
||||
if (e.ValueKind != JsonValueKind.String) return null;
|
||||
var s = e.GetString() ?? string.Empty;
|
||||
return int.TryParse(s, out n) ? n : byName(s);
|
||||
}
|
||||
|
||||
private static int? RoleName(string s) => s.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"none" => RoomRoleRules.WireNone,
|
||||
"banned" => RoomRoleRules.WireBanned,
|
||||
"host" => RoomRoleRules.WireHost,
|
||||
"moderator" => RoomRoleRules.WireModerator,
|
||||
"coowner" => RoomRoleRules.WireCoOwner,
|
||||
"temporarycoowner" => RoomRoleRules.WireTemporaryCoOwner,
|
||||
"creator" => RoomRoleRules.WireCreator,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static int? TypeName(string s) => s.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"bool" => 0,
|
||||
"int" => 1,
|
||||
"stringlist" => 2,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
using DorkNet.Server.Data.Entities;
|
||||
|
||||
namespace DorkNet.Server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// The 2023 room-role ladder and who may change whose role.
|
||||
///
|
||||
/// <para><b>Wire enum.</b> The client's room role is <c>OMMBGJMJJPN : byte</c>
|
||||
/// (recnet-runtime-decomp/OMMBGJMJJPN.cs): None 0, Banned 1, Host 10,
|
||||
/// Moderator 20, CoOwner 30, TemporaryCoOwner 31, Creator 255. It is
|
||||
/// gap-skipping, like the club MembershipType enum, so never treat it as an
|
||||
/// index. <see cref="RoomRoleEntity.Role"/> stores a compact kind instead
|
||||
/// (0 = CoOwner, 1 = Moderator, 2 = Host); <see cref="ToWire"/> and
|
||||
/// <see cref="TryFromWire"/> convert.</para>
|
||||
///
|
||||
/// <para><b>The creator is 255, not 30.</b> The client reads the local
|
||||
/// player's role straight out of the room details <c>Roles</c> list
|
||||
/// (<c>FGCPNAACHIK.ONECLLALJEO</c>: first row whose AccountId matches), and
|
||||
/// the room-permissions runtime only lets a player edit a role strictly
|
||||
/// below their own, with Creator (255) the one unconditional pass
|
||||
/// (<c>OMJKHJLFOCO.DIIMLMKEJLC</c>,
|
||||
/// IsilDump/RecRoom.RoomPermissions.Runtime/OMJKHJLFOCO.txt:2367:
|
||||
/// <c>if role == 255 return true; if target >= role return false</c>).
|
||||
/// Advertising the creator as CoOwner (30) therefore locked the room's own
|
||||
/// creator out of the Co-Owner and Creator permission rows.</para>
|
||||
///
|
||||
/// <para><b>Server rule</b> (<see cref="CheckChange"/>): the creator may set
|
||||
/// any role on anyone but themselves. Otherwise the actor must hold an
|
||||
/// accepted CoOwner role, and both the target's current role (accepted or
|
||||
/// still invited) and the requested role must be strictly below the
|
||||
/// actor's. A player may always drop their own role or decline their own
|
||||
/// invite (role 0), and may accept exactly the role they were invited to.</para>
|
||||
/// </summary>
|
||||
public static class RoomRoleRules
|
||||
{
|
||||
public const int WireNone = 0;
|
||||
public const int WireBanned = 1;
|
||||
public const int WireHost = 10;
|
||||
public const int WireModerator = 20;
|
||||
public const int WireCoOwner = 30;
|
||||
public const int WireTemporaryCoOwner = 31;
|
||||
public const int WireCreator = 255;
|
||||
|
||||
public const int StoredCoOwner = 0;
|
||||
public const int StoredModerator = 1;
|
||||
public const int StoredHost = 2;
|
||||
|
||||
/// <summary>Stored kind → wire role.</summary>
|
||||
public static int ToWire(int storedRole) => storedRole switch
|
||||
{
|
||||
StoredCoOwner => WireCoOwner,
|
||||
StoredModerator => WireModerator,
|
||||
StoredHost => WireHost,
|
||||
_ => WireNone,
|
||||
};
|
||||
|
||||
/// <summary>Wire role → stored kind. Only the three persistable roles
|
||||
/// map; None, Banned, TemporaryCoOwner (an in-instance grant) and Creator
|
||||
/// (ownership, changed through <c>rooms/{id}/creator</c>) do not.</summary>
|
||||
public static bool TryFromWire(int wireRole, out int storedRole)
|
||||
{
|
||||
storedRole = wireRole switch
|
||||
{
|
||||
WireCoOwner => StoredCoOwner,
|
||||
WireModerator => StoredModerator,
|
||||
WireHost => StoredHost,
|
||||
_ => -1,
|
||||
};
|
||||
return storedRole >= 0;
|
||||
}
|
||||
|
||||
/// <summary>The player's accepted wire role in the room: 255 for the
|
||||
/// creator, the highest accepted grant otherwise, 0 for none.</summary>
|
||||
public static int AcceptedRank(RoomEntity room, IEnumerable<RoomRoleEntity> rows, long playerId)
|
||||
{
|
||||
if (room.CreatorPlayerId == playerId) return WireCreator;
|
||||
return rows
|
||||
.Where(r => r.RoomId == room.Id && r.PlayerId == playerId && r.Accepted)
|
||||
.Select(r => ToWire(r.Role))
|
||||
.DefaultIfEmpty(WireNone)
|
||||
.Max();
|
||||
}
|
||||
|
||||
/// <summary>The highest role the player has been invited to and not yet
|
||||
/// accepted, 0 for none.</summary>
|
||||
public static int InvitedRank(RoomEntity room, IEnumerable<RoomRoleEntity> rows, long playerId)
|
||||
=> rows
|
||||
.Where(r => r.RoomId == room.Id && r.PlayerId == playerId && !r.Accepted)
|
||||
.Select(r => ToWire(r.Role))
|
||||
.DefaultIfEmpty(WireNone)
|
||||
.Max();
|
||||
|
||||
/// <summary>True when the player may change room-level settings and the
|
||||
/// sub-room settings that hang off them: the creator or an accepted
|
||||
/// co-owner. This is the same gate as the bare room mutations
|
||||
/// (RoomsModerationController.RequireOwnedRoomAsync) and room saves.</summary>
|
||||
public static bool CanEditRoom(RoomEntity room, IEnumerable<RoomRoleEntity> rows, long playerId)
|
||||
=> AcceptedRank(room, rows, playerId) >= WireCoOwner;
|
||||
|
||||
/// <summary>What a role write will do once <see cref="CheckChange"/> allows it.</summary>
|
||||
public enum ChangeKind
|
||||
{
|
||||
/// <summary>Grant or replace the target's role (accepted at once).</summary>
|
||||
Grant,
|
||||
/// <summary>Record a pending invite the target must accept.</summary>
|
||||
Invite,
|
||||
/// <summary>The target accepts their own pending invite.</summary>
|
||||
AcceptInvite,
|
||||
/// <summary>Remove the target's role and any pending invite.</summary>
|
||||
Clear,
|
||||
}
|
||||
|
||||
/// <summary>Decide whether <paramref name="actorId"/> may set
|
||||
/// <paramref name="targetId"/>'s role to <paramref name="requestedWireRole"/>.
|
||||
/// Returns null when allowed, otherwise an error code for the response.
|
||||
/// <paramref name="kind"/> is what the caller should then do.</summary>
|
||||
public static string? CheckChange(
|
||||
RoomEntity room,
|
||||
IReadOnlyCollection<RoomRoleEntity> rows,
|
||||
long actorId,
|
||||
long targetId,
|
||||
int requestedWireRole,
|
||||
bool isInvite,
|
||||
out ChangeKind kind)
|
||||
{
|
||||
kind = requestedWireRole == WireNone
|
||||
? ChangeKind.Clear
|
||||
: isInvite ? ChangeKind.Invite : ChangeKind.Grant;
|
||||
|
||||
if (targetId == room.CreatorPlayerId)
|
||||
return "cannot_change_creator_role";
|
||||
|
||||
if (requestedWireRole != WireNone && !TryFromWire(requestedWireRole, out _))
|
||||
return "invalid_role";
|
||||
|
||||
var actorRank = AcceptedRank(room, rows, actorId);
|
||||
|
||||
// Self-service: anyone may give up their own role or decline their
|
||||
// own invite, and accept the exact role they were invited to.
|
||||
if (actorId == targetId && actorRank != WireCreator)
|
||||
{
|
||||
if (requestedWireRole == WireNone) return null;
|
||||
if (!isInvite && requestedWireRole == InvitedRank(room, rows, targetId))
|
||||
{
|
||||
kind = ChangeKind.AcceptInvite;
|
||||
return null;
|
||||
}
|
||||
return "forbidden";
|
||||
}
|
||||
|
||||
if (actorRank == WireCreator) return null;
|
||||
if (actorRank < WireCoOwner) return "forbidden";
|
||||
|
||||
var targetRank = Math.Max(
|
||||
AcceptedRank(room, rows, targetId),
|
||||
InvitedRank(room, rows, targetId));
|
||||
if (targetRank >= actorRank) return "forbidden";
|
||||
if (requestedWireRole >= actorRank) return "forbidden";
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>True when <paramref name="playerId"/> may write the
|
||||
/// permission row for <paramref name="wireRole"/>: the creator may edit
|
||||
/// every role (including CoOwner and Creator); a co-owner only the roles
|
||||
/// below their own. Mirrors the client's OMJKHJLFOCO.DIIMLMKEJLC.</summary>
|
||||
public static bool CanEditRolePermissions(
|
||||
RoomEntity room, IEnumerable<RoomRoleEntity> rows, long playerId, int wireRole)
|
||||
{
|
||||
var rank = AcceptedRank(room, rows, playerId);
|
||||
if (rank == WireCreator) return true;
|
||||
if (rank < WireCoOwner) return false;
|
||||
// TemporaryCoOwner collapses to CoOwner on the client (ONECLLALJEO).
|
||||
var target = wireRole == WireTemporaryCoOwner ? WireCoOwner : wireRole;
|
||||
return target < rank;
|
||||
}
|
||||
|
||||
/// <summary>The room details / roles-list rows: one per account, with
|
||||
/// the creator first as 255 and every other account's accepted role in
|
||||
/// <c>Role</c> and pending invite in <c>InvitedRole</c>. Rows the
|
||||
/// creator happens to own in RoomRoles are ignored so the creator is
|
||||
/// never listed twice (the client takes the first match).</summary>
|
||||
public static List<RoomRoleWire> BuildWires(RoomEntity room, IEnumerable<RoomRoleEntity> rows)
|
||||
{
|
||||
var list = new List<RoomRoleWire>
|
||||
{
|
||||
new(ToClientAccountId(room.CreatorPlayerId), WireCreator, WireNone, null),
|
||||
};
|
||||
foreach (var group in rows
|
||||
.Where(r => r.RoomId == room.Id && r.PlayerId != room.CreatorPlayerId)
|
||||
.GroupBy(r => r.PlayerId)
|
||||
.OrderBy(g => g.Key))
|
||||
{
|
||||
var accepted = group.Where(r => r.Accepted).ToList();
|
||||
var invited = group.Where(r => !r.Accepted).ToList();
|
||||
var role = accepted.Select(r => ToWire(r.Role)).DefaultIfEmpty(WireNone).Max();
|
||||
var invitedRole = invited.Select(r => ToWire(r.Role)).DefaultIfEmpty(WireNone).Max();
|
||||
var lastChangedBy = group
|
||||
.OrderByDescending(r => r.GrantedAt)
|
||||
.Select(r => r.GrantedByPlayerId)
|
||||
.FirstOrDefault();
|
||||
list.Add(new RoomRoleWire(
|
||||
ToClientAccountId(group.Key),
|
||||
role,
|
||||
invitedRole,
|
||||
lastChangedBy is long by ? ToClientAccountId(by) : null));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>One <c>EFHPLDPNGIM</c> row: <c>{AccountId: Int32, Role,
|
||||
/// InvitedRole, LastChangedByAccountId: Int32?}</c>.</summary>
|
||||
public sealed record RoomRoleWire(int AccountId, int Role, int InvitedRole, int? LastChangedByAccountId);
|
||||
|
||||
/// <summary>Account ids are Int32 in the 2023 role DTO. Clamp rather than
|
||||
/// wrap so an out-of-range id is obviously wrong.</summary>
|
||||
public static int ToClientAccountId(long accountId)
|
||||
=> accountId > int.MaxValue
|
||||
? int.MaxValue
|
||||
: accountId < int.MinValue
|
||||
? int.MinValue
|
||||
: (int)accountId;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace DorkNet.Server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Converts between a sub-room's position in the room
|
||||
/// (<see cref="Data.Entities.RoomSceneEntity.OrderIndex"/>, 0-based, what the
|
||||
/// server stores and indexes on) and the <c>SubRoomId</c> the 2023 client
|
||||
/// sees on the wire.
|
||||
///
|
||||
/// <para><b>Why they differ.</b> The client refuses to load a sub-room whose
|
||||
/// id is not positive: <c>SubroomModel.DOKCPLJOKIE</c>
|
||||
/// (IsilDump/Assembly-CSharp/RRUI/Data/SubroomModel.txt:3) is
|
||||
/// <c>RoomId > 0 && SubroomId > 0</c>, and
|
||||
/// <c>Model`1.LNPEMMMGCPC</c> skips the load when it is false. With the
|
||||
/// first sub-room advertised as id 0, its Room Settings > Subrooms row
|
||||
/// never fetched its details (<c>SubroomModel.KBOJBJNCOGK</c> →
|
||||
/// <c>FGCPNAACHIK.OBFPEGLLELP(long?)</c>), so the row showed no name, and
|
||||
/// every per-row action (rename, max players, permissions, duplicate) ran
|
||||
/// against a null sub-room. Rec Room's own ids were large unique numbers;
|
||||
/// the client needs only "positive and stable per room".</para>
|
||||
///
|
||||
/// <para><b>Mapping.</b> Wire id = OrderIndex + 1, so a room's first
|
||||
/// sub-room is 1. Incoming ids go back through <see cref="ToOrderIndex"/>;
|
||||
/// 0 (and anything negative) is accepted as an alias of the first sub-room,
|
||||
/// which keeps cached client state, old presence rows and bare
|
||||
/// <c>matchmake/room/{id}</c> joins working.</para>
|
||||
///
|
||||
/// <para><b>Where the conversion happens.</b> Only at the wire: the room
|
||||
/// details <c>SubRooms[]</c>, the sub-room routes
|
||||
/// (<c>rooms/{id}/subrooms/{sub}/*</c>), <c>matchmake/room/{id}/{sub}</c>,
|
||||
/// the room-instance JSON (<c>RoomInstanceDto.subRoomId</c>), the instance
|
||||
/// browser, sub-room save DTOs and permission rows. Everything stored
|
||||
/// (RoomScenes, PrivateInstances, RoomDataBlobs, presence) keeps the
|
||||
/// OrderIndex.</para>
|
||||
/// </summary>
|
||||
public static class RoomSceneIds
|
||||
{
|
||||
/// <summary>OrderIndex → wire SubRoomId (always ≥ 1).</summary>
|
||||
public static long ToWire(long orderIndex) => Math.Max(0, orderIndex) + 1;
|
||||
|
||||
/// <summary>Wire SubRoomId → OrderIndex. 0 and negatives map to the first
|
||||
/// sub-room.</summary>
|
||||
public static int ToOrderIndex(long wireSubRoomId)
|
||||
=> wireSubRoomId <= 1 ? 0 : (int)Math.Min(int.MaxValue, wireSubRoomId - 1);
|
||||
}
|
||||
@@ -546,6 +546,71 @@ public class RoomService(DorkNetDbContext db)
|
||||
return existing;
|
||||
}
|
||||
|
||||
/// <summary>The name a sub-room gets when it has none: "Home" for the
|
||||
/// entry sub-room, "Subroom N" (1-based, matching its wire id) for the
|
||||
/// rest.</summary>
|
||||
public static string DefaultSubRoomName(int orderIndex)
|
||||
=> orderIndex <= 0 ? "Home" : $"Subroom {orderIndex + 1}";
|
||||
|
||||
/// <summary>A real entry sub-room row for a room that has none, built
|
||||
/// from the room's own location, data blob and capacity. Callers add it
|
||||
/// to the context.</summary>
|
||||
public static RoomSceneEntity NewHomeScene(RoomEntity room) => new()
|
||||
{
|
||||
RoomId = room.Id,
|
||||
OrderIndex = 0,
|
||||
Name = DefaultSubRoomName(0),
|
||||
RoomSceneLocationId = string.IsNullOrWhiteSpace(room.LocationReplicationId)
|
||||
? new RoomSceneEntity().RoomSceneLocationId
|
||||
: room.LocationReplicationId,
|
||||
DataBlobName = room.CurrentDataBlobName ?? string.Empty,
|
||||
MaxPlayers = room.MaxCapacity > 0 ? room.MaxCapacity : 8,
|
||||
IsSandbox = false,
|
||||
CanMatchmakeInto = true,
|
||||
DataModifiedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
/// <summary>Startup self-heal for sub-rooms (idempotent).
|
||||
///
|
||||
/// <para>Every non-dorm room must own at least one real RoomScenes row,
|
||||
/// and every row must have a name. A room created by cloning a template
|
||||
/// that had no scene rows (the 2023 "create room" flow clones a base room
|
||||
/// such as MakerRoom) ended up with none, so the details builder served a
|
||||
/// synthesised fallback and every sub-room mutation had to create the row
|
||||
/// on the fly; a blank Name renders as an empty row in Room Settings >
|
||||
/// Subrooms. This inserts the missing entry sub-room (OrderIndex 0, the
|
||||
/// room's own location / blob / capacity) and names unnamed rows. Dorms
|
||||
/// are left alone: their details are shaped per player and their single
|
||||
/// scene is synthesised on purpose.</para>
|
||||
///
|
||||
/// <para>Scene ids are assigned explicitly (max + 1) for the same reason
|
||||
/// the clone handler does: seeded rows were inserted with manual ids
|
||||
/// without advancing the Postgres identity sequence.</para>
|
||||
/// Returns (rooms given a scene, scenes renamed).</summary>
|
||||
public async Task<(int ScenesAdded, int ScenesRenamed)> RepairRoomScenesAsync()
|
||||
{
|
||||
var roomsWithoutScenes = await db.Rooms
|
||||
.Where(r => !r.IsDormRoom && !db.RoomScenes.Any(s => s.RoomId == r.Id))
|
||||
.ToListAsync();
|
||||
var nextSceneId = await db.RoomScenes.MaxAsync(s => (long?)s.Id) ?? 0L;
|
||||
foreach (var room in roomsWithoutScenes)
|
||||
{
|
||||
var scene = NewHomeScene(room);
|
||||
scene.Id = ++nextSceneId;
|
||||
db.RoomScenes.Add(scene);
|
||||
}
|
||||
|
||||
var unnamed = await db.RoomScenes
|
||||
.Where(s => s.Name == null || s.Name.Trim() == string.Empty)
|
||||
.ToListAsync();
|
||||
foreach (var scene in unnamed)
|
||||
scene.Name = DefaultSubRoomName(scene.OrderIndex);
|
||||
|
||||
if (roomsWithoutScenes.Count > 0 || unnamed.Count > 0)
|
||||
await db.SaveChangesAsync();
|
||||
return (roomsWithoutScenes.Count, unnamed.Count);
|
||||
}
|
||||
|
||||
/// <summary>Run once at startup to backfill <see cref="RoomEntity"/>
|
||||
/// + <see cref="DormStateEntity"/> rows for every account that
|
||||
/// pre-dates the auto-create-on-signup work. Idempotent — calls
|
||||
@@ -1068,6 +1133,10 @@ public class RoomService(DorkNetDbContext db)
|
||||
};
|
||||
|
||||
db.Rooms.Add(clone);
|
||||
// Every room owns a real entry sub-room (see RepairRoomScenesAsync).
|
||||
var home = NewHomeScene(clone);
|
||||
home.Id = (await db.RoomScenes.MaxAsync(s => (long?)s.Id) ?? 0L) + 1;
|
||||
db.RoomScenes.Add(home);
|
||||
await db.SaveChangesAsync();
|
||||
return new CloneResult(0 /* Success */, clone);
|
||||
}
|
||||
@@ -1081,6 +1150,25 @@ public class RoomService(DorkNetDbContext db)
|
||||
/// AllowsJuniors, RoomWarningMask, CustomRoomWarning
|
||||
/// Optional: IsDormRoom, SupportsVRLow, SupportsMobile, DisableMicAutoMute.
|
||||
/// </summary>
|
||||
/// <summary>The <c>IsRRO</c> the 2023 client is told: only a first-party
|
||||
/// room (baked/AG and still owned by the system account) is a Rec Room
|
||||
/// Original. A player's room is never one, even when it was cloned from an
|
||||
/// RRO template and keeps <see cref="RoomEntity.IsAGRoom"/> for the baked
|
||||
/// blob handling.
|
||||
///
|
||||
/// <para>The client gates the whole owner UI on it: RoomModel
|
||||
/// get_CanPublishRoom, CanUnpublishRoom, CanDeleteRoom,
|
||||
/// CanChangeRoomCreator, CanChangeRoomSetting, CanChangeCustomWarning,
|
||||
/// CanChangeLoadingScreen and CanCreateNewSubroom all require
|
||||
/// <c>!IsRRO</c> (IsilDump/Assembly-CSharp/RRUI/Data/RoomModel.txt:275,
|
||||
/// 429, 560, 682, 1002, 1473, 1680, 1816), so a clone advertised as RRO
|
||||
/// could never be published ("Cannot publish room") or have its settings
|
||||
/// changed. The room-load pipeline (RecRoom.RoomLoading.Runtime) never
|
||||
/// reads the flag: the scene comes from the sub-room's UnitySceneId and
|
||||
/// the blob step from whether DataBlob is set.</para></summary>
|
||||
public static bool WireIsRRO(RoomEntity r)
|
||||
=> r.IsAGRoom && r.CreatorPlayerId == PlayerService.SystemAccountId;
|
||||
|
||||
public static object ToWireRoom(RoomEntity r) => new
|
||||
{
|
||||
RoomId = r.Id,
|
||||
@@ -1093,7 +1181,7 @@ public class RoomService(DorkNetDbContext db)
|
||||
Accessibility = r.Accessibility,
|
||||
SupportsLevelVoting = r.SupportsLevelVoting,
|
||||
IsAGRoom = r.IsAGRoom,
|
||||
IsRRO = r.IsAGRoom,
|
||||
IsRRO = WireIsRRO(r),
|
||||
// 2020.12 Room.Deserialize (KLCOGEIGEBJ.PPGFHEDFBEA, the Room base)
|
||||
// reads required keys: RoomId, IsDorm, CloningAllowed, DisableMicAutoMute,
|
||||
// DisableRoomComments, EncryptVoiceChat. The 2020.03 key was IsDormRoom;
|
||||
|
||||
@@ -164,6 +164,11 @@ public static class DatabaseBootstrap
|
||||
await roomService.ApplyCanonicalOverridesAsync(http, imagesDir);
|
||||
}
|
||||
|
||||
// Every non-dorm room needs a real, named entry sub-room (see
|
||||
// RoomService.RepairRoomScenesAsync). Runs after the seed and the
|
||||
// canonical overrides so it only fills what they left missing.
|
||||
await roomService.RepairRoomScenesAsync();
|
||||
|
||||
// Seed default curated playlists, club category tags, store catalog.
|
||||
// Must run after SeedAsync so they can reference RR-Originals.
|
||||
var playlistService = scope.ServiceProvider.GetRequiredService<PlaylistService>();
|
||||
@@ -899,6 +904,23 @@ public static class DatabaseBootstrap
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_ClubAdditionalImages_ClubId_Slot"" ON ""ClubAdditionalImages"" (""ClubId"", ""Slot"");",
|
||||
// 2023 sub-room role-permission matrix
|
||||
// (PUT rooms/{id}/subrooms/{sub}/permissions → photon_access_token).
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "RoomRolePermissions" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"RoomId" bigint NOT NULL,
|
||||
"SubRoomId" bigint NOT NULL,
|
||||
"Role" integer NOT NULL,
|
||||
"Permission" character varying(128) NOT NULL DEFAULT '',
|
||||
"Type" integer NOT NULL DEFAULT 0,
|
||||
"Override" boolean NOT NULL DEFAULT false,
|
||||
"Value" character varying(4096) NOT NULL DEFAULT '',
|
||||
"UpdatedByPlayerId" bigint NOT NULL DEFAULT 0,
|
||||
"UpdatedAt" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_RoomRolePermissions_RoomId_SubRoomId_Role_Permission"" ON ""RoomRolePermissions"" (""RoomId"", ""SubRoomId"", ""Role"", ""Permission"");",
|
||||
};
|
||||
foreach (var sql in statements)
|
||||
await db.Database.ExecuteSqlRawAsync(sql);
|
||||
@@ -976,6 +998,21 @@ public static class DatabaseBootstrap
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_ClubAdditionalImages_ClubId_Slot"" ON ""ClubAdditionalImages"" (""ClubId"", ""Slot"");",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "RoomRolePermissions" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_RoomRolePermissions" PRIMARY KEY AUTOINCREMENT,
|
||||
"RoomId" INTEGER NOT NULL,
|
||||
"SubRoomId" INTEGER NOT NULL,
|
||||
"Role" INTEGER NOT NULL,
|
||||
"Permission" TEXT NOT NULL DEFAULT '',
|
||||
"Type" INTEGER NOT NULL DEFAULT 0,
|
||||
"Override" INTEGER NOT NULL DEFAULT 0,
|
||||
"Value" TEXT NOT NULL DEFAULT '',
|
||||
"UpdatedByPlayerId" INTEGER NOT NULL DEFAULT 0,
|
||||
"UpdatedAt" TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_RoomRolePermissions_RoomId_SubRoomId_Role_Permission"" ON ""RoomRolePermissions"" (""RoomId"", ""SubRoomId"", ""Role"", ""Permission"");",
|
||||
};
|
||||
// CREATE ... IF NOT EXISTS is already idempotent; a failure here is a
|
||||
// real problem and must fail startup.
|
||||
|
||||
Reference in New Issue
Block a user