mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 15:38:45 -07:00
fix: correct 2023-03-21 client wire contracts across the API and notify surfaces
Audits every route the March-2023 client can call against what this server
actually serves, and fixes the defects that were breaking gameplay. Adds
docs/recroom-2023-client-api-complete.md as the reference: ~500 route
literals across 13 subsystems, with verb, request/response shape, and a
per-subsystem diff. 315 defects catalogued; the ones fixed here are listed
in its "Already fixed" table.
Path coverage was never the problem — 465 of 506 routes already had
handlers. The failures were in shape, and they cluster into four causes:
- Wrong verb. The verb is a BestHTTP.HTTPMethods ordinal (GET=0 .. PATCH=5),
confirmed from that enum's static constructor. Routes registered POST-only
while the client PUTs returned 405, which made room consumables neither
buyable nor consumable and blocked every room-key edit.
- Bare scalar wrapped in an object. The client reads `1000`, not
`{"Limit":1000}`; the issuing method signature says which. Wrapping makes
the deserializer throw and the feature dies silently.
- [FromBody] vs form-urlencoded. The client sends nearly every write as form
data, which an [ApiController] action bound [FromBody] rejects with 415
before the handler runs. This alone broke all 13 club member-management
endpoints. Adds Binding/FormOrJsonModelBinder so this is fixed once.
- Notify dispatch keys. Both enum overloads of the client's RegisterHandler
lower the id via Int32.ToString() and register only that numeric key, and
lookup is a case-sensitive TryGetValue that returns silently on a miss. We
were sending the enum NAME for most ids, so friend-request updates, forced
logout, maintenance broadcasts, live balance and consumable updates, and
invention moderation pushes all evaporated with no error anywhere.
Notable behavioural bugs fixed:
- Editing a club announcement deleted it: PUT was a third binding on the
delete handler. Split apart, and the missing create route added.
- leaderboard/CheckAndSetStat did not exist. The 2023 client has no SetStat
literal at all, so every leaderboard score it wrote was discarded.
- Setting a club clubhouse cleared it: the handler read roomId from the query
while the client sends it in the body, then wrote the resulting null.
- Club join requests were inverted (the enum is Open=0, InviteOnly=1,
AskToJoin=2), so ask-to-join clubs rejected everyone and invite-only clubs
quietly admitted them.
- Room bans were pushed as id 24, for which the client registers no handler;
they now go out as ModerationKick with IsBan, so the ban takes effect
immediately instead of at next login.
- Room-key ownership queries answered for the wrong player, and "has anyone
bought this key" asked "did I buy it" — always false for the creator.
Also persists settings that were previously acknowledged and dropped
(playlist accessibility/restrictions/level-voting/warning, per-player status
visibility, VR movement mode and avoid-juniors, custom room codes), which is
what the new columns in the migration are for.
One test asserted the old nested PriceAndCurrency response shape; the
client's consumable formatter has no such key and reads Price and
PurchaseCurrencyId flat, so the test was corrected alongside the handler.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace DorkNet.Server.Binding;
|
||||
|
||||
/// <summary>
|
||||
/// Binds an action's request DTO from either a form-urlencoded body or a JSON
|
||||
/// body, whichever the caller actually sent.
|
||||
///
|
||||
/// The Rec Room client's HTTP layer (<c>BNDIAONDFFF</c> in RecNet.Runtime)
|
||||
/// sends nearly every write as <c>application/x-www-form-urlencoded</c>, adding
|
||||
/// fields one at a time. Handlers written against <c>[FromBody]</c> only accept
|
||||
/// JSON, so with <c>[ApiController]</c> those requests were rejected with 415
|
||||
/// Unsupported Media Type before the action body ever ran — this is why every
|
||||
/// club member-management action (invite, ban, kick, promote, approve, modify)
|
||||
/// failed on the 2023 client.
|
||||
///
|
||||
/// Rather than hand-rolling a form reader per handler, apply this binder to the
|
||||
/// DTO parameter:
|
||||
///
|
||||
/// <code>
|
||||
/// public Task<IActionResult> MemberBan(
|
||||
/// long clubId,
|
||||
/// [ModelBinder(typeof(FormOrJsonModelBinder))] MemberTargetRequest req)
|
||||
/// </code>
|
||||
///
|
||||
/// The explicit <c>[ModelBinder]</c> is required: without it <c>[ApiController]</c>
|
||||
/// infers <c>[FromBody]</c> for complex types and this binder never runs.
|
||||
///
|
||||
/// Matching is case-insensitive on both paths, and a scalar property also
|
||||
/// accepts the singular/plural spellings the client uses interchangeably. A
|
||||
/// missing or unparseable body yields a default-constructed DTO rather than a
|
||||
/// 400, matching how the client treats these calls as fire-and-forget.
|
||||
/// </summary>
|
||||
public sealed class FormOrJsonModelBinder : IModelBinder
|
||||
{
|
||||
public async Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bindingContext);
|
||||
|
||||
var type = bindingContext.ModelType;
|
||||
var request = bindingContext.HttpContext.Request;
|
||||
var model = Activator.CreateInstance(type)!;
|
||||
|
||||
if (request.HasFormContentType)
|
||||
{
|
||||
var form = await request.ReadFormAsync(bindingContext.HttpContext.RequestAborted);
|
||||
foreach (var prop in Writable(type))
|
||||
{
|
||||
var values = Lookup(form, prop.Name);
|
||||
if (values.Count == 0) continue;
|
||||
var converted = Convert(values, prop.PropertyType);
|
||||
if (converted is not null) prop.SetValue(model, converted);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
request.EnableBuffering();
|
||||
request.Body.Position = 0;
|
||||
var parsed = await JsonSerializer.DeserializeAsync(
|
||||
request.Body, type,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true },
|
||||
bindingContext.HttpContext.RequestAborted);
|
||||
request.Body.Position = 0;
|
||||
if (parsed is not null) model = parsed;
|
||||
}
|
||||
catch (JsonException) { /* empty or non-JSON body → defaults */ }
|
||||
catch (NotSupportedException) { /* unreadable body → defaults */ }
|
||||
}
|
||||
|
||||
bindingContext.Result = ModelBindingResult.Success(model);
|
||||
}
|
||||
|
||||
private static IEnumerable<PropertyInfo> Writable(Type type) =>
|
||||
type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanWrite);
|
||||
|
||||
/// <summary>Form keys, case-insensitively, also accepting the other
|
||||
/// number of the name — the client says <c>accountId</c> where the DTO says
|
||||
/// <c>AccountIds</c> and vice versa depending on the endpoint.</summary>
|
||||
private static List<string> Lookup(IFormCollection form, string propertyName)
|
||||
{
|
||||
var candidates = new List<string> { propertyName };
|
||||
if (propertyName.EndsWith('s')) candidates.Add(propertyName[..^1]);
|
||||
else candidates.Add(propertyName + "s");
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
foreach (var key in form.Keys)
|
||||
{
|
||||
if (!string.Equals(key, candidate, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
var values = form[key]
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v))
|
||||
.Select(v => v!)
|
||||
.ToList();
|
||||
if (values.Count > 0) return values;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private static object? Convert(List<string> values, Type target)
|
||||
{
|
||||
var underlying = Nullable.GetUnderlyingType(target) ?? target;
|
||||
|
||||
if (underlying.IsGenericType && underlying.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
var element = underlying.GetGenericArguments()[0];
|
||||
var list = (System.Collections.IList)Activator.CreateInstance(underlying)!;
|
||||
// A repeated field arrives as multiple values; a single field may
|
||||
// still carry a comma-separated list.
|
||||
foreach (var raw in values.SelectMany(v => v.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)))
|
||||
{
|
||||
var item = ConvertScalar(raw, element);
|
||||
if (item is not null) list.Add(item);
|
||||
}
|
||||
return list.Count > 0 ? list : null;
|
||||
}
|
||||
|
||||
return ConvertScalar(values[0], underlying);
|
||||
}
|
||||
|
||||
private static object? ConvertScalar(string raw, Type target)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (target == typeof(string)) return raw;
|
||||
if (target == typeof(bool)) return bool.TryParse(raw, out var b) ? b : raw == "1";
|
||||
if (target.IsEnum) return Enum.TryParse(target, raw, ignoreCase: true, out var e) ? e : null;
|
||||
if (target == typeof(Guid)) return Guid.TryParse(raw, out var g) ? g : null;
|
||||
if (target == typeof(DateTime)) return DateTime.TryParse(raw, out var d) ? d : null;
|
||||
return System.Convert.ChangeType(raw, target, System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using DorkNet.Server.Auth;
|
||||
using DorkNet.Server.Data;
|
||||
using DorkNet.Server.Data.Entities;
|
||||
@@ -13,6 +14,13 @@ namespace DorkNet.Server.Controllers.API.AgeVerification;
|
||||
[Authorize]
|
||||
public class AgeVerificationController(DorkNetDbContext db) : ControllerBase
|
||||
{
|
||||
/// <summary>The 2023-03-21 client POSTs this (verb register <c>rdx=2</c>
|
||||
/// at RecNet.Runtime/ALLHCFFLMLD.txt:216-222) and deserialises the body as
|
||||
/// a bare <c>String</c>: the issuing method is
|
||||
/// <c>FGLDKEJLAKB<IPCJLCNIBEG<System.String>> GKEBPOPAEJC()</c>
|
||||
/// (:150). Returning a JSON object here made Json.NET throw, so the
|
||||
/// promise rejected and the code was never shown in the watch.
|
||||
/// GET stays registered because it costs nothing.</summary>
|
||||
[HttpPost("generateCode")]
|
||||
[HttpGet("generateCode")]
|
||||
public async Task<IActionResult> GenerateCode()
|
||||
@@ -26,6 +34,6 @@ public class AgeVerificationController(DorkNetDbContext db) : ControllerBase
|
||||
Value = code,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(new { Code = code, VerificationCode = code, ExpiresAt = DateTime.UtcNow.AddMinutes(15) });
|
||||
return Content(JsonSerializer.Serialize(code), "application/json");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,31 @@ namespace DorkNet.Server.Controllers.API.Comments;
|
||||
|
||||
/// <summary>
|
||||
/// Room-comment unread counts. Client contract (RecNet.Runtime
|
||||
/// <c>DJLNMANKMAA(IReadOnlyCollection<long>)</c>): the caller sends a
|
||||
/// collection of roomIds and expects a top-level JSON OBJECT
|
||||
/// <c>Dictionary<long, uint></c> mapping roomId → unread count.
|
||||
/// We don't track per-player comment read-state yet, so every requested
|
||||
/// room maps to 0 — a correct, non-crashing "nothing unread" answer that
|
||||
/// matches the wire shape (an object, never an array).
|
||||
/// <c>OPELMBNJHNO.DJLNMANKMAA(IReadOnlyCollection<long>)</c>).
|
||||
///
|
||||
/// The response is a JSON <b>ARRAY</b>, not an object. The method's return
|
||||
/// type is <c>Dictionary<long,uint></c>, but that dictionary is built
|
||||
/// client-side: OPELMBNJHNO.txt:889 constructs a
|
||||
/// <c>Func<List<UnreadRoomComments>, Dictionary<long,uint>></c>
|
||||
/// projection that runs over the deserialised payload. So the wire type is
|
||||
/// <c>List<RecNet.UnreadRoomComments></c> — each element an object with a
|
||||
/// room id (Int64) and a count (UInt32), per the two properties on
|
||||
/// RecNet/UnreadRoomComments.txt.
|
||||
///
|
||||
/// This previously returned an object map keyed by room id. Json.NET cannot
|
||||
/// read a JSON object into a <c>List<T></c>, so every call threw and the
|
||||
/// client logged "Failed to get unread room comment counts"
|
||||
/// (OPELMBNJHNO.txt:937) and fell back to an empty dictionary — the watch's
|
||||
/// room-comment unread badge never appeared.
|
||||
///
|
||||
/// The element's exact property names are attribute-driven and are not
|
||||
/// recoverable from the ISIL (see the note on key aliasing below), so each
|
||||
/// element carries the plausible spellings side by side. Json.NET matches
|
||||
/// case-insensitively and ignores unknown members, so the extra aliases are
|
||||
/// inert whichever name the DTO actually declares.
|
||||
///
|
||||
/// Counts are all zero until per-player comment read-state is tracked; the
|
||||
/// shape is what matters for the client not to throw.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
@@ -24,10 +43,14 @@ public class CommentsController : ControllerBase
|
||||
public async Task<IActionResult> UnreadCounts()
|
||||
{
|
||||
var roomIds = await ReadRoomIdsAsync();
|
||||
// Keys must be strings in JSON; values are the (currently always 0)
|
||||
// unread counts.
|
||||
var result = new Dictionary<string, uint>();
|
||||
foreach (var id in roomIds) result[id.ToString()] = 0;
|
||||
var result = roomIds
|
||||
.Select(id => new Dictionary<string, object>
|
||||
{
|
||||
["RoomId"] = id,
|
||||
["UnreadCount"] = 0u,
|
||||
["Count"] = 0u,
|
||||
})
|
||||
.ToList();
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,19 @@ namespace DorkNet.Server.Controllers.API.Compatibility;
|
||||
[ApiController]
|
||||
public class CompatibilityFeatureController(DorkNetDbContext db) : ControllerBase
|
||||
{
|
||||
/// <summary>The 2023-03-21 client GETs this (verb register <c>rdx=0</c>
|
||||
/// at RecNet.Runtime/FPIBGPIAOBI.txt:2984, immediately before the
|
||||
/// <c>"api/banappeal/generateCode"</c> literal on :2980) and deserialises
|
||||
/// the response as a bare <c>String</c> — the issuing method is
|
||||
/// <c>FGLDKEJLAKB<System.String> HBEPHMAKCDN()</c> (:2759) and the
|
||||
/// continuation is <c>Func<IPCJLCNIBEG<String>,
|
||||
/// FGLDKEJLAKB<String>></c> (:3019).
|
||||
///
|
||||
/// It used to be POST-only returning <c>{value: code}</c>, so the client
|
||||
/// got a 405 and, had it reached the handler, would have thrown parsing a
|
||||
/// JSON object into a string. Ban-appeal code generation never worked.
|
||||
/// The body is now the bare JSON string (<c>"BA-123456"</c>).</summary>
|
||||
[HttpGet("api/banappeal/generateCode")]
|
||||
[HttpPost("api/banappeal/generateCode")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GenerateBanAppealCode()
|
||||
@@ -25,7 +38,7 @@ public class CompatibilityFeatureController(DorkNetDbContext db) : ControllerBas
|
||||
Value = code,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(new { value = code });
|
||||
return Content(JsonSerializer.Serialize(code), "application/json");
|
||||
}
|
||||
|
||||
[HttpPost("api/CampusCard/v1/UpdateAndGetSubscription")]
|
||||
@@ -110,7 +123,7 @@ public class CompatibilityFeatureController(DorkNetDbContext db) : ControllerBas
|
||||
|
||||
// Prefix with "[club {id}]" so admins can tell club reports apart
|
||||
// from player reports in the shared ReportEntity queue.
|
||||
var message = $"[club {club.Id}] {req.Message ?? string.Empty}".Trim();
|
||||
var message = $"[club {club.Id}] {req.Details ?? req.Message ?? string.Empty}".Trim();
|
||||
if (message.Length > 1000) message = message[..1000];
|
||||
db.Reports.Add(new ReportEntity
|
||||
{
|
||||
@@ -155,7 +168,13 @@ public class CompatibilityFeatureController(DorkNetDbContext db) : ControllerBas
|
||||
?? form["ClubId"].FirstOrDefault(), out var clubId) ? clubId : 0,
|
||||
ReportCategory = int.TryParse(form["reportCategory"].FirstOrDefault()
|
||||
?? form["ReportCategory"].FirstOrDefault(), out var cat) ? cat : 5,
|
||||
Message = form["message"].FirstOrDefault() ?? form["Message"].FirstOrDefault(),
|
||||
// The 2023 client names the free-text field "details"
|
||||
// (IKMMOCKDKAF.txt:25303-25322), not "message" — reading only
|
||||
// the latter stored every club report with an empty body.
|
||||
Message = form["details"].FirstOrDefault()
|
||||
?? form["Details"].FirstOrDefault()
|
||||
?? form["message"].FirstOrDefault()
|
||||
?? form["Message"].FirstOrDefault(),
|
||||
};
|
||||
}
|
||||
try
|
||||
@@ -178,5 +197,7 @@ public class CompatibilityFeatureController(DorkNetDbContext db) : ControllerBas
|
||||
public long ClubId { get; set; }
|
||||
public int ReportCategory { get; set; } = 5;
|
||||
public string? Message { get; set; }
|
||||
/// <summary>The client's spelling of <see cref="Message"/>.</summary>
|
||||
public string? Details { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,14 +121,18 @@ public class CustomAvatarItemsController(
|
||||
skip = Math.Max(0, skip);
|
||||
take = Math.Clamp(take, 1, 100);
|
||||
var currentPlayerId = this.CurrentPlayerId();
|
||||
var rows = await db.CustomAvatarItems
|
||||
var query = db.CustomAvatarItems
|
||||
.Where(i => i.CreatorPlayerId == creatorId
|
||||
&& (i.IsPublic || i.CreatorPlayerId == currentPlayerId))
|
||||
&& (i.IsPublic || i.CreatorPlayerId == currentPlayerId));
|
||||
var total = await query.CountAsync();
|
||||
var rows = await query
|
||||
.OrderByDescending(i => i.UpdatedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
return Ok(rows.Select(ToWire));
|
||||
// Paged container, not a bare array — the client deserializes this into
|
||||
// CKFLFJCNEPH {Results, TotalResults}, same as Owned() below.
|
||||
return Ok(new { Results = rows.Select(ToWire), TotalResults = total });
|
||||
}
|
||||
|
||||
[HttpGet("me")]
|
||||
@@ -203,7 +207,11 @@ public class CustomAvatarItemsController(
|
||||
var allowed = player is not null
|
||||
&& !player.IsJunior
|
||||
&& (player.BannedUntil is null || player.BannedUntil <= DateTime.UtcNow);
|
||||
return Ok(new { IsCreationAllowed = allowed, Allowed = allowed });
|
||||
// Bare JSON boolean, like the two sibling gates below: the client's
|
||||
// issuing method is FGLDKEJLAKB<System.Boolean> GGMDLMLJDDK()
|
||||
// (RecNet.Runtime/MPBLNLMCEDL.txt:6048, route literal on :6205), so an
|
||||
// object body threw and the can-create gate blocked shirt creation.
|
||||
return Content(allowed ? "true" : "false", "application/json");
|
||||
}
|
||||
|
||||
[HttpGet("isCreationEnabled")]
|
||||
@@ -262,6 +270,13 @@ public class CustomAvatarItemsController(
|
||||
return Ok(ToWire(item));
|
||||
}
|
||||
|
||||
/// <summary>The 2023-03-21 client buys custom items through the
|
||||
/// <c>econ/</c> host prefix — <c>econ/customAvatarItems/v1/{id}/purchase</c>
|
||||
/// — which was never registered, so every custom-shirt purchase 404'd. The
|
||||
/// rooted route below is an alias onto the same handler. <c>requestedPrice</c>
|
||||
/// arrives as a query param and is validated against the item's real price
|
||||
/// so a tampered client can't buy at its own number.</summary>
|
||||
[HttpPost("/econ/customAvatarItems/v1/{id:guid}/purchase")]
|
||||
[HttpPost("{id:guid}/purchase")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Purchase(Guid id)
|
||||
|
||||
+26
-20
@@ -43,37 +43,43 @@ public class ExternalFriendInviteController(DorkNetDbContext db) : ControllerBas
|
||||
return Ok(new { Success = true, InviteCode = code, PhoneNumber = phoneNumber ?? string.Empty });
|
||||
}
|
||||
|
||||
/// <summary>Accounts who joined via this player's platform invites.
|
||||
///
|
||||
/// The response is a bare array of account ids — the issuing method is
|
||||
/// <c>FGLDKEJLAKB<List<System.Int32>> POJMMCCLNCB()</c>
|
||||
/// (RecNet.Runtime/OMHDDLFIPLP.txt:923). Returning objects
|
||||
/// (<c>{InviteCode, Kind, Value, CreatedAt}</c>) made Json.NET throw as soon
|
||||
/// as any referrer existed, so the referral screen broke for exactly the
|
||||
/// players who had referred someone.</summary>
|
||||
[HttpGet("api/externalfriendinvite/v1/getplatformreferrers")]
|
||||
[HttpPost("api/externalfriendinvite/v1/getplatformreferrers")]
|
||||
public async Task<IActionResult> GetPlatformReferrers()
|
||||
{
|
||||
var rows = await db.PlayerSettings
|
||||
.Where(s => s.PlayerId == Me && s.Key.StartsWith("externalinvite:platform:"))
|
||||
.OrderByDescending(s => s.Id)
|
||||
.ToListAsync();
|
||||
return Ok(rows.Select(ToInviteWire));
|
||||
}
|
||||
=> Ok(await ReferrerIdsAsync("externalinvite:platform:"));
|
||||
|
||||
/// <summary>Same bare-id-array contract as
|
||||
/// <see cref="GetPlatformReferrers"/>, for text-message invites.</summary>
|
||||
[HttpGet("api/externalfriendinvite/v1/gettextmessagereferrers")]
|
||||
[HttpPost("api/externalfriendinvite/v1/gettextmessagereferrers")]
|
||||
public async Task<IActionResult> GetTextMessageReferrers()
|
||||
=> Ok(await ReferrerIdsAsync("externalinvite:text:"));
|
||||
|
||||
/// <summary>Account ids recorded against the caller's invites of the given
|
||||
/// kind. The redeeming account id is stored as the second pipe-delimited
|
||||
/// field once an invite is claimed; unclaimed invites contribute nothing.</summary>
|
||||
private async Task<List<int>> ReferrerIdsAsync(string keyPrefix)
|
||||
{
|
||||
var rows = await db.PlayerSettings
|
||||
.Where(s => s.PlayerId == Me && s.Key.StartsWith("externalinvite:text:"))
|
||||
.Where(s => s.PlayerId == Me && s.Key.StartsWith(keyPrefix))
|
||||
.OrderByDescending(s => s.Id)
|
||||
.Select(s => s.Value)
|
||||
.ToListAsync();
|
||||
return Ok(rows.Select(ToInviteWire));
|
||||
|
||||
return rows
|
||||
.Select(v => v.Split('|'))
|
||||
.Select(parts => int.TryParse(parts.ElementAtOrDefault(1), out var id) ? id : 0)
|
||||
.Where(id => id > 0)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static object ToInviteWire(PlayerSettingEntity row)
|
||||
{
|
||||
var parts = row.Value.Split('|');
|
||||
return new
|
||||
{
|
||||
InviteCode = row.Key.Split(':').Last(),
|
||||
Kind = row.Key.Contains(":text:") ? "text" : "platform",
|
||||
Value = parts.FirstOrDefault() ?? string.Empty,
|
||||
CreatedAt = parts.LastOrDefault() ?? string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,6 +618,16 @@ public class ImagesController(
|
||||
public long ImageId { get; set; }
|
||||
public string? ImageName { get; set; }
|
||||
public bool IsPublic { get; set; }
|
||||
|
||||
/// <summary>What the 2023-03-21 client actually sends:
|
||||
/// 0 = Private, 1 = Public, 2 = FriendsOnly. It never sends
|
||||
/// <see cref="IsPublic"/>, so reading only that flag left every photo
|
||||
/// private no matter what the player picked.</summary>
|
||||
public int? Accessibility { get; set; }
|
||||
|
||||
/// <summary>Resolved visibility: the enum when present, else the
|
||||
/// legacy boolean.</summary>
|
||||
public bool ResolvedIsPublic => Accessibility is int a ? a == 1 : IsPublic;
|
||||
}
|
||||
|
||||
/// <summary>POST <c>api/images/v1/modifyaccessibility</c> — flip
|
||||
@@ -630,7 +640,8 @@ public class ImagesController(
|
||||
[FromForm(Name = "PhotoId")] long? photoIdForm,
|
||||
[FromForm(Name = "ImageId")] long? imageIdForm,
|
||||
[FromForm(Name = "ImageName")] string? imageNameForm,
|
||||
[FromForm(Name = "IsPublic")] bool? isPublicForm)
|
||||
[FromForm(Name = "IsPublic")] bool? isPublicForm,
|
||||
[FromForm(Name = "Accessibility")] int? accessibilityForm)
|
||||
{
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var photoId = body?.PhotoId ?? body?.ImageId
|
||||
@@ -638,7 +649,12 @@ public class ImagesController(
|
||||
var imageName = body?.ImageName ?? imageNameForm;
|
||||
if (photoId <= 0 && string.IsNullOrWhiteSpace(imageName))
|
||||
return BadRequest(new { error = "missing photoId" });
|
||||
var isPublic = body?.IsPublic ?? isPublicForm ?? true;
|
||||
// Accessibility (0=Private, 1=Public, 2=FriendsOnly) is what the 2023
|
||||
// client sends; IsPublic is the older boolean.
|
||||
var accessibility = body?.Accessibility ?? accessibilityForm;
|
||||
var isPublic = accessibility is int a
|
||||
? a == 1
|
||||
: body?.IsPublic ?? isPublicForm ?? true;
|
||||
|
||||
var photo = photoId > 0
|
||||
? await db.Photos.FirstOrDefaultAsync(p => p.Id == photoId)
|
||||
|
||||
@@ -504,9 +504,25 @@ public class InventionsController(
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>The 2023-03-21 client names the blob field
|
||||
/// <c>inventionDataFilename</c> and sends four extra cost fields plus the
|
||||
/// creation room and referenced inventions. The record only had
|
||||
/// <c>BlobName</c>, which the client never sends, so the required-parameter
|
||||
/// check rejected every publish with 400 and no new invention version could
|
||||
/// ever be saved.</summary>
|
||||
public sealed record AddVersionRequest(
|
||||
long InventionId, string BlobName,
|
||||
int? InstantiationCost, int? LightsCost);
|
||||
long InventionId,
|
||||
string? BlobName,
|
||||
string? InventionDataFilename,
|
||||
int? InstantiationCost, int? LightsCost,
|
||||
int? ChipsCost, int? CloudVariablesCost, int? AiCost,
|
||||
long? CreationRoomId,
|
||||
List<long>? ReferencedInventions)
|
||||
{
|
||||
public string? Blob => !string.IsNullOrWhiteSpace(InventionDataFilename)
|
||||
? InventionDataFilename
|
||||
: BlobName;
|
||||
}
|
||||
|
||||
[HttpPost("api/inventions/v3/addversion")]
|
||||
[HttpPost("api/inventions/v4/addversion")]
|
||||
@@ -517,10 +533,11 @@ public class InventionsController(
|
||||
var inv = await db.Inventions.FirstOrDefaultAsync(x => x.Id == req.InventionId && !x.IsDeleted);
|
||||
if (inv is null) return NotFound();
|
||||
if (inv.CreatorPlayerId != pid) return Forbid();
|
||||
if (string.IsNullOrWhiteSpace(req.BlobName)) return BadRequest("missing BlobName");
|
||||
var blobName = req.Blob;
|
||||
if (string.IsNullOrWhiteSpace(blobName)) return BadRequest("missing inventionDataFilename");
|
||||
|
||||
var nextVer = inv.CurrentVersionNumber + 1;
|
||||
inv.CurrentBlobName = req.BlobName;
|
||||
inv.CurrentBlobName = blobName;
|
||||
inv.CurrentVersionNumber = nextVer;
|
||||
inv.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
@@ -529,7 +546,7 @@ public class InventionsController(
|
||||
InventionId = inv.Id,
|
||||
ReplicationId = Guid.NewGuid().ToString("D"),
|
||||
VersionNumber = nextVer,
|
||||
BlobName = req.BlobName,
|
||||
BlobName = blobName,
|
||||
InstantiationCost = req.InstantiationCost ?? 0,
|
||||
LightsCost = req.LightsCost ?? 0,
|
||||
});
|
||||
@@ -664,7 +681,15 @@ public class InventionsController(
|
||||
|
||||
// ── POST mutations ───────────────────────────────────────────────────
|
||||
|
||||
public sealed record SetTagsRequest(long InventionId, string? AutoTags, string? PlayerAddedTags);
|
||||
/// <summary>The client sends <c>AutoTags</c> and <c>CustomTags</c> as JSON
|
||||
/// ARRAYS of strings. Binding them to <c>string</c> made deserialization
|
||||
/// throw, so every tag edit 400'd. <c>PlayerAddedTags</c> is kept as an
|
||||
/// alias for older callers.</summary>
|
||||
public sealed record SetTagsRequest(
|
||||
long InventionId,
|
||||
List<string>? AutoTags,
|
||||
List<string>? CustomTags,
|
||||
List<string>? PlayerAddedTags);
|
||||
|
||||
[HttpPost("api/inventions/v1/settags")]
|
||||
[Authorize]
|
||||
@@ -674,8 +699,12 @@ public class InventionsController(
|
||||
var inv = await db.Inventions.FirstOrDefaultAsync(x => x.Id == req.InventionId && !x.IsDeleted);
|
||||
if (inv is null) return NotFound();
|
||||
if (inv.CreatorPlayerId != pid) return Forbid();
|
||||
var combined = string.Join(',', new[] { req.AutoTags, req.PlayerAddedTags }
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))).Trim(',');
|
||||
var combined = string.Join(',', new[] { req.AutoTags, req.CustomTags, req.PlayerAddedTags }
|
||||
.Where(list => list is not null)
|
||||
.SelectMany(list => list!)
|
||||
.Select(t => t?.Trim())
|
||||
.Where(t => !string.IsNullOrWhiteSpace(t))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
inv.TagsCsv = combined;
|
||||
inv.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -78,8 +79,85 @@ public class InventoryController(
|
||||
/// <summary>POST <c>api/equipment/v1/update</c> — toggle the
|
||||
/// "favorited" flag on a piece of equipment (the watch lets the
|
||||
/// player pin commonly-used tools).</summary>
|
||||
/// <remarks>The 2023-03-21 client sends a JSON <b>array</b> of equipment
|
||||
/// updates here, not a single object (it serialises a
|
||||
/// <c>List<BCINJINBBHG></c>). Binding <c>[FromBody]</c> onto one
|
||||
/// object failed model binding and returned an automatic 400, so equip and
|
||||
/// favourite changes never persisted for that build. The body is now read
|
||||
/// leniently and both shapes are applied through the same per-item path.
|
||||
/// </remarks>
|
||||
[HttpPost("api/equipment/v1/update")]
|
||||
public async Task<IActionResult> UpdateEquipment([FromBody] EquipmentUpdateRequest req)
|
||||
[Consumes("application/json", "application/x-www-form-urlencoded", "multipart/form-data")]
|
||||
public async Task<IActionResult> UpdateEquipment()
|
||||
{
|
||||
var requests = await ReadEquipmentUpdatesAsync();
|
||||
if (requests.Count == 0) return BadRequest("missing prefab/mod identifier");
|
||||
|
||||
IActionResult last = Ok();
|
||||
foreach (var req in requests)
|
||||
{
|
||||
last = await ApplyEquipmentUpdateAsync(req);
|
||||
// Surface the first rejection rather than pretending the whole
|
||||
// batch succeeded.
|
||||
if (last is not OkObjectResult and not OkResult) return last;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
/// <summary>Accepts a single object, an array of objects, or a form post.</summary>
|
||||
private async Task<List<EquipmentUpdateRequest>> ReadEquipmentUpdatesAsync()
|
||||
{
|
||||
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
var result = new List<EquipmentUpdateRequest>();
|
||||
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
string? S(params string[] keys)
|
||||
{
|
||||
foreach (var k in keys)
|
||||
if (form.TryGetValue(k, out var v) && !string.IsNullOrEmpty(v)) return v.ToString();
|
||||
return null;
|
||||
}
|
||||
bool? B(params string[] keys)
|
||||
=> bool.TryParse(S(keys), out var b) ? b : null;
|
||||
result.Add(new EquipmentUpdateRequest
|
||||
{
|
||||
PrefabName = S("PrefabName", "prefabName"),
|
||||
ModificationGuid = S("ModificationGuid", "modificationGuid"),
|
||||
Favorited = B("Favorited", "favorited") ?? false,
|
||||
Equipped = B("Equipped", "equipped"),
|
||||
IsEquipped = B("IsEquipped", "isEquipped"),
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Request.EnableBuffering();
|
||||
Request.Body.Position = 0;
|
||||
using var doc = await JsonDocument.ParseAsync(Request.Body);
|
||||
Request.Body.Position = 0;
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var el in root.EnumerateArray())
|
||||
{
|
||||
var one = el.Deserialize<EquipmentUpdateRequest>(opts);
|
||||
if (one is not null) result.Add(one);
|
||||
}
|
||||
}
|
||||
else if (root.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var one = root.Deserialize<EquipmentUpdateRequest>(opts);
|
||||
if (one is not null) result.Add(one);
|
||||
}
|
||||
}
|
||||
catch (JsonException) { /* unparseable body → treated as empty */ }
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ApplyEquipmentUpdateAsync(EquipmentUpdateRequest req)
|
||||
{
|
||||
var pid = Me;
|
||||
// Catalog-backed skins use a compound slug so we can round-trip
|
||||
@@ -209,16 +287,28 @@ public class InventoryController(
|
||||
return Ok(new { row.Id, Count = row.Quantity });
|
||||
}
|
||||
|
||||
/// <remarks>The 2023-03-21 client serialises a <c>TransferConsumableRequest</c>
|
||||
/// with <c>JsonUtility.ToJson</c> and posts it as a raw JSON body. With
|
||||
/// <c>[ApiController]</c> plus <c>[FromForm]</c> parameters, an
|
||||
/// <c>application/json</c> request bound nothing and the transfer was
|
||||
/// rejected as invalid, so consumable gifting never worked on that build.
|
||||
/// The JSON body is now read alongside the original form/query paths.</remarks>
|
||||
[HttpPost("api/consumables/v1/transfer")]
|
||||
[Consumes("application/json", "application/x-www-form-urlencoded", "multipart/form-data")]
|
||||
public async Task<IActionResult> TransferConsumable(
|
||||
[FromForm] long? id,
|
||||
[FromForm] long? recipientPlayerId,
|
||||
[FromForm] int quantity = 1)
|
||||
{
|
||||
var pid = Me;
|
||||
var inventoryId = id ?? (long.TryParse(Request.Query["id"], out var qId) ? qId : 0);
|
||||
var recipient = recipientPlayerId ?? (long.TryParse(Request.Query["recipientPlayerId"], out var qRecipient) ? qRecipient : 0);
|
||||
var amount = Math.Max(1, quantity);
|
||||
var body = await ReadTransferBodyAsync();
|
||||
var inventoryId = id
|
||||
?? body.Id
|
||||
?? (long.TryParse(Request.Query["id"], out var qId) ? qId : 0);
|
||||
var recipient = recipientPlayerId
|
||||
?? body.RecipientPlayerId
|
||||
?? (long.TryParse(Request.Query["recipientPlayerId"], out var qRecipient) ? qRecipient : 0);
|
||||
var amount = Math.Max(1, body.Quantity ?? quantity);
|
||||
if (inventoryId <= 0 || recipient <= 0 || recipient == pid)
|
||||
return BadRequest("invalid_transfer");
|
||||
|
||||
@@ -247,6 +337,69 @@ public class InventoryController(
|
||||
return Ok(new { Success = true, SourceCount = Math.Max(0, source.Quantity), RecipientCount = target.Quantity });
|
||||
}
|
||||
|
||||
private readonly record struct TransferBody(long? Id, long? RecipientPlayerId, int? Quantity);
|
||||
|
||||
/// <summary>Reads the 2023 client's raw JSON transfer body. JsonUtility
|
||||
/// emits field names verbatim, and the exact casing of that DTO is not
|
||||
/// recoverable from the ISIL, so both conventions are accepted.</summary>
|
||||
private async Task<TransferBody> ReadTransferBodyAsync()
|
||||
{
|
||||
if (Request.HasFormContentType) return default;
|
||||
try
|
||||
{
|
||||
Request.EnableBuffering();
|
||||
Request.Body.Position = 0;
|
||||
using var doc = await JsonDocument.ParseAsync(Request.Body);
|
||||
Request.Body.Position = 0;
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return default;
|
||||
|
||||
long? L(params string[] keys)
|
||||
{
|
||||
foreach (var k in keys)
|
||||
if (doc.RootElement.TryGetProperty(k, out var e) &&
|
||||
e.ValueKind == JsonValueKind.Number && e.TryGetInt64(out var v)) return v;
|
||||
return null;
|
||||
}
|
||||
int? I(params string[] keys)
|
||||
{
|
||||
foreach (var k in keys)
|
||||
if (doc.RootElement.TryGetProperty(k, out var e) &&
|
||||
e.ValueKind == JsonValueKind.Number && e.TryGetInt32(out var v)) return v;
|
||||
return null;
|
||||
}
|
||||
return new TransferBody(
|
||||
L("Id", "id", "InventoryId", "inventoryId"),
|
||||
L("RecipientPlayerId", "recipientPlayerId"),
|
||||
I("Quantity", "quantity", "Count", "count"));
|
||||
}
|
||||
catch (JsonException) { return default; }
|
||||
}
|
||||
|
||||
/// <summary>GET <c>api/consumables/v1/getTransferable/{playerId}</c> — the
|
||||
/// consumables the caller may gift to <c>playerId</c>. No handler existed,
|
||||
/// so the transfer UI 404'd and could never list anything to send.
|
||||
/// Returns the same grouped shape as <c>v2/getUnlocked</c>.</summary>
|
||||
[HttpGet("api/consumables/v1/getTransferable/{playerId:long}")]
|
||||
public async Task<IActionResult> GetTransferableConsumables(long playerId)
|
||||
{
|
||||
var pid = Me;
|
||||
var rows = await db.PlayerInventory
|
||||
.Where(p => p.PlayerId == pid && p.Quantity > 0)
|
||||
.ToListAsync();
|
||||
|
||||
// Can't gift to yourself; everything else the caller holds is fair game.
|
||||
var transferable = playerId == pid ? [] : rows;
|
||||
return Ok(transferable
|
||||
.GroupBy(r => r.ItemSlug)
|
||||
.Select(g => new
|
||||
{
|
||||
Consumable = g.Key,
|
||||
Count = g.Sum(r => r.Quantity),
|
||||
Id = g.Min(r => r.Id),
|
||||
})
|
||||
.ToList());
|
||||
}
|
||||
|
||||
public sealed class ActivateConsumableRequest
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
@@ -131,6 +131,106 @@ public class LeaderboardController(
|
||||
return Ok(new { success = true, error = "" });
|
||||
}
|
||||
|
||||
/// <summary>POST <c>/leaderboard/CheckAndSetStat</c> — the compare-and-set
|
||||
/// form of <see cref="SetStat"/>, and the ONLY stat-write route the
|
||||
/// 2023-03-21 client has: that build contains no <c>SetStat</c> literal at
|
||||
/// all. Nothing registered this route, so every leaderboard score written
|
||||
/// by a 2023 client fell through to the catch-all and was lost.
|
||||
///
|
||||
/// The request is <c>RecNet.SetStatRequestDTO</c>, whose property names are
|
||||
/// NOT obfuscated in the dump: <c>StatChannel</c> (Int32), <c>RoomId</c>
|
||||
/// (Int64), <c>StatValue</c> (Int32) and the extra
|
||||
/// <c>CurrentStatValue</c> (Int32?) that distinguishes this route from
|
||||
/// plain SetStat.
|
||||
///
|
||||
/// <c>CurrentStatValue</c> is the value the client believes is currently
|
||||
/// stored. It is an optimistic-concurrency guard: the write applies only if
|
||||
/// the server agrees, which stops a stale session from clobbering a newer
|
||||
/// score. A null means "no expectation" and writes unconditionally, and a
|
||||
/// mismatch is reported rather than silently dropped so the client can
|
||||
/// re-read.
|
||||
///
|
||||
/// As with SetStat, the client has already decided the new value should win
|
||||
/// (it applies the per-channel higher/lower-is-better rule locally), so the
|
||||
/// server does not second-guess the direction.</summary>
|
||||
[HttpPost("/leaderboard/CheckAndSetStat")]
|
||||
[HttpPost("/api/leaderboard/CheckAndSetStat")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> CheckAndSetStat([FromBody] CheckAndSetStatRequest req)
|
||||
{
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var roomId = ResolveRoomId(req.RoomId, pid);
|
||||
var row = await db.LeaderboardStats.FirstOrDefaultAsync(s =>
|
||||
s.RoomId == roomId && s.PlayerId == pid && s.StatChannel == req.StatChannel);
|
||||
|
||||
if (req.CurrentStatValue is int expected && row is not null && row.Value != expected)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = false,
|
||||
error = "stat_value_mismatch",
|
||||
StatValue = row.Value,
|
||||
CurrentStatValue = row.Value,
|
||||
});
|
||||
}
|
||||
|
||||
if (row is null)
|
||||
{
|
||||
row = new LeaderboardStatEntity
|
||||
{
|
||||
PlayerId = pid,
|
||||
RoomId = roomId,
|
||||
StatChannel = req.StatChannel,
|
||||
Value = req.StatValue,
|
||||
};
|
||||
db.LeaderboardStats.Add(row);
|
||||
}
|
||||
else
|
||||
{
|
||||
row.Value = req.StatValue;
|
||||
row.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
await EnsureChannelMetaAsync(roomId, req.StatChannel);
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
error = "",
|
||||
StatValue = row.Value,
|
||||
CurrentStatValue = row.Value,
|
||||
});
|
||||
}
|
||||
|
||||
public sealed class CheckAndSetStatRequest
|
||||
{
|
||||
public int StatChannel { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public int StatValue { get; set; }
|
||||
|
||||
/// <summary>The value the client believes is stored. Null = write
|
||||
/// unconditionally.</summary>
|
||||
public int? CurrentStatValue { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Stamp a placeholder channel-meta row the first time a channel
|
||||
/// reports, so the admin SPA's per-room Leaderboards tab surfaces it.</summary>
|
||||
private async Task EnsureChannelMetaAsync(long roomId, int statChannel)
|
||||
{
|
||||
if (roomId <= 0) return;
|
||||
var meta = await db.LeaderboardChannelMeta
|
||||
.FirstOrDefaultAsync(c => c.RoomId == roomId && c.Channel == statChannel);
|
||||
if (meta is not null) return;
|
||||
db.LeaderboardChannelMeta.Add(new LeaderboardChannelMetaEntity
|
||||
{
|
||||
Channel = statChannel,
|
||||
RoomId = roomId,
|
||||
Name = $"Channel {statChannel}",
|
||||
LowerIsBetter = false,
|
||||
ValueFormat = "count",
|
||||
});
|
||||
}
|
||||
|
||||
public sealed class GetPlayerRankRequest
|
||||
{
|
||||
public int PlayerId { get; set; }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -105,9 +106,23 @@ public class InGameModerationController(
|
||||
|
||||
// ── /v1/instantKick — admin one-shot ─────────────────────────────
|
||||
/// <summary>
|
||||
/// Admin/dev one-shot kick. The watch posts either
|
||||
/// Admin/dev one-shot kick. The 2020.12 watch posts either
|
||||
/// <c>?playerId=N</c> for a single target (PlayerReporting.txt:3417)
|
||||
/// or a list body for bulk kicks (3590). Either form lands here.
|
||||
/// or a list body for bulk kicks (3590).
|
||||
///
|
||||
/// The 2023-03-21 client posts neither: it serialises a
|
||||
/// <c>RecNet.KickPlayerDTO</c> with <c>JsonUtility.ToJson</c> and sends it
|
||||
/// as a RAW JSON body (RecNet.Runtime/FPIBGPIAOBI.txt:5012 JsonUtility.ToJson,
|
||||
/// :5018 BNDIAONDFFF.FJLLPHFOOJJ raw-body setter, :5034 verb rdx=2).
|
||||
/// KickPlayerDTO's fields are not obfuscated — <c>public long GameSessionId</c>
|
||||
/// and <c>public List<int> PlayerIds</c> — so the body is exactly
|
||||
/// <c>{"GameSessionId":123,"PlayerIds":[456]}</c>. Unity's JsonUtility emits
|
||||
/// field names verbatim.
|
||||
///
|
||||
/// None of the query/form parameters bound against that body, so the target
|
||||
/// list came back empty and every 2023 instant-kick returned
|
||||
/// "missing_target" and kicked nobody.
|
||||
///
|
||||
/// Gated to <see cref="PlayerEntity.IsAdmin"/> /
|
||||
/// <see cref="PlayerEntity.IsDeveloper"/>.
|
||||
/// </summary>
|
||||
@@ -130,6 +145,7 @@ public class InGameModerationController(
|
||||
foreach (var part in formIds.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
if (long.TryParse(part, out var v) && v > 0) ids.Add(v);
|
||||
}
|
||||
ids.AddRange(await ReadKickPlayerDtoIdsAsync());
|
||||
ids = ids.Distinct().ToList();
|
||||
if (ids.Count == 0) return Ok(new { success = false, error = "missing_target" });
|
||||
|
||||
@@ -143,6 +159,47 @@ public class InGameModerationController(
|
||||
return Ok(new { success = true, error = "" });
|
||||
}
|
||||
|
||||
/// <summary>Read the 2023 client's raw <c>KickPlayerDTO</c> JSON body.
|
||||
/// Returns an empty list for a form post or an unparseable body so the
|
||||
/// older query/form paths are unaffected.</summary>
|
||||
private async Task<List<long>> ReadKickPlayerDtoIdsAsync()
|
||||
{
|
||||
var ids = new List<long>();
|
||||
if (Request.HasFormContentType) return ids;
|
||||
|
||||
try
|
||||
{
|
||||
Request.EnableBuffering();
|
||||
Request.Body.Position = 0;
|
||||
using var doc = await JsonDocument.ParseAsync(Request.Body);
|
||||
Request.Body.Position = 0;
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return ids;
|
||||
|
||||
foreach (var name in new[] { "PlayerIds", "playerIds" })
|
||||
{
|
||||
if (!doc.RootElement.TryGetProperty(name, out var arr) ||
|
||||
arr.ValueKind != JsonValueKind.Array) continue;
|
||||
foreach (var el in arr.EnumerateArray())
|
||||
if (el.ValueKind == JsonValueKind.Number && el.TryGetInt64(out var v) && v > 0)
|
||||
ids.Add(v);
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var name in new[] { "PlayerId", "playerId" })
|
||||
{
|
||||
if (doc.RootElement.TryGetProperty(name, out var one) &&
|
||||
one.ValueKind == JsonValueKind.Number &&
|
||||
one.TryGetInt64(out var v) && v > 0)
|
||||
{
|
||||
ids.Add(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException) { /* not a JSON body */ }
|
||||
return ids;
|
||||
}
|
||||
|
||||
// ── /v1/deviceId — log mismatched device id ──────────────────────
|
||||
[HttpPost("api/PlayerReporting/v1/deviceId")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
@@ -192,11 +249,21 @@ public class InGameModerationController(
|
||||
"[mod] ban by {Caller}: target={Target} reason={Reason} until={Until} display={Display}",
|
||||
caller, target, reason, p.BannedUntil, displayReason);
|
||||
|
||||
// Push a ModerationRoomBan notification so the target watch
|
||||
// shows the ban dialog and disconnects (PlayerReporting.txt:2253).
|
||||
await notifications.NotifyAsync(
|
||||
// Push the ban as a ModerationKick (22) with IsBan set, NOT as
|
||||
// ModerationRoomBan (24): the 2023-03-21 client's moderation manager
|
||||
// registers only 22, "ModerationUnkick" and 23
|
||||
// (RecNet.Runtime/FPIBGPIAOBI.txt:310, :323, :335) — there is no handler
|
||||
// for 24, so that push landed nowhere and the banned player kept playing
|
||||
// until their next login. The 22 handler reads Msg.Reason as a
|
||||
// ModerationBlockDetail, which ModerationKickPayload already produces.
|
||||
await notifications.KickPlayerAsync(
|
||||
target,
|
||||
Models.Notification.PushNotificationId.ModerationRoomBan);
|
||||
// `reason` is the numeric report category; DisplayReason is the
|
||||
// moderator's text and is what the ban dialog shows.
|
||||
string.IsNullOrWhiteSpace(displayReason)
|
||||
? "You have been banned from this room."
|
||||
: displayReason.Trim(),
|
||||
isBan: true);
|
||||
return Ok(new { success = true, error = "" });
|
||||
}
|
||||
|
||||
|
||||
@@ -24,19 +24,99 @@ public class PlayerWarningsController(DorkNetDbContext db) : ControllerBase
|
||||
.ToList());
|
||||
}
|
||||
|
||||
/// <summary>Moderator issues a warning against a player. The 2023-03-21
|
||||
/// client POSTs this (verb register <c>rdx=2</c> at
|
||||
/// RecNet.Runtime/FPIBGPIAOBI.txt:3455) with the form keys
|
||||
/// <c>WarnedPlayerId</c>, <c>ReportCategory</c>, <c>DisplayReason</c> and
|
||||
/// <c>ModeratorNote</c> (:3468-3520), matching the issuing method
|
||||
/// <c>FGLDKEJLAKB<DJMHAFPGLLN> OEJIMKKDMBI(Int32, BDOGOIGCKMK,
|
||||
/// String, String)</c> (:3102).
|
||||
///
|
||||
/// Only GET was registered here, so every warning the moderator tools
|
||||
/// tried to create came back 405 and silently failed.</summary>
|
||||
[HttpPost("api/playerwarnings")]
|
||||
[Consumes("application/json", "application/x-www-form-urlencoded", "multipart/form-data")]
|
||||
public async Task<IActionResult> CreateWarning(
|
||||
[FromForm(Name = "WarnedPlayerId")] long? warnedPlayerId,
|
||||
[FromForm(Name = "ReportCategory")] int? reportCategory,
|
||||
[FromForm(Name = "DisplayReason")] string? displayReason,
|
||||
[FromForm(Name = "ModeratorNote")] string? moderatorNote)
|
||||
{
|
||||
var me = this.RequireCurrentPlayerId();
|
||||
|
||||
var moderator = await db.Players
|
||||
.Where(p => p.Id == me)
|
||||
.Select(p => (bool?)(p.IsAdmin || p.IsDeveloper || p.IsCommunityTeam))
|
||||
.FirstOrDefaultAsync();
|
||||
if (moderator != true)
|
||||
return Ok(new { Success = false, Message = "not_authorized" });
|
||||
|
||||
var target = warnedPlayerId
|
||||
?? (long.TryParse(Request.Query["WarnedPlayerId"].FirstOrDefault(), out var q) ? q : 0);
|
||||
if (target <= 0) return Ok(new { Success = false, Message = "missing_player" });
|
||||
if (!await db.Players.AnyAsync(p => p.Id == target))
|
||||
return Ok(new { Success = false, Message = "player_not_found" });
|
||||
|
||||
// Same "warning:{id}|message|category|createdAt|acknowledged" layout the
|
||||
// GET list handler parses in ToWarningWire.
|
||||
var reason = (displayReason ?? string.Empty).Trim();
|
||||
var note = (moderatorNote ?? string.Empty).Trim();
|
||||
var message = string.IsNullOrEmpty(note) ? reason : $"{reason} ({note})";
|
||||
var id = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff");
|
||||
|
||||
db.PlayerSettings.Add(new DorkNet.Server.Data.Entities.PlayerSettingEntity
|
||||
{
|
||||
PlayerId = target,
|
||||
Key = $"warning:{id}",
|
||||
Value = string.Join('|',
|
||||
message.Replace('|', '/'),
|
||||
(reportCategory ?? 0).ToString(),
|
||||
DateTime.UtcNow.ToString("O"),
|
||||
"false"),
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(new { Success = true, Message = string.Empty, WarningId = id });
|
||||
}
|
||||
|
||||
/// <summary>The 2023-03-21 client's acknowledge call takes NO arguments —
|
||||
/// the issuing method is <c>FGLDKEJLAKB<DJMHAFPGLLN> DOFADJPFBMK()</c>
|
||||
/// (FPIBGPIAOBI.txt:3524) and it POSTs an empty body (:3590-3592). It means
|
||||
/// "acknowledge my pending warning".
|
||||
///
|
||||
/// Demanding a warningId made every call 400, so the modal was never marked
|
||||
/// read and re-appeared on each login. With no id supplied we now
|
||||
/// acknowledge the newest unacknowledged warning, and acknowledging nothing
|
||||
/// is a success rather than an error.</summary>
|
||||
[HttpPost("api/playerwarnings/acknowledge")]
|
||||
[Consumes("application/json", "application/x-www-form-urlencoded", "multipart/form-data")]
|
||||
public async Task<IActionResult> Acknowledge([FromForm] string? warningId)
|
||||
{
|
||||
var me = this.RequireCurrentPlayerId();
|
||||
var id = warningId ?? Request.Query["warningId"].FirstOrDefault() ?? Request.Query["id"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(id)) return BadRequest("missing_warning");
|
||||
var row = await db.PlayerSettings
|
||||
.FirstOrDefaultAsync(s => s.PlayerId == me && s.Key == $"warning:{id}");
|
||||
if (row is null) return NotFound();
|
||||
|
||||
DorkNet.Server.Data.Entities.PlayerSettingEntity? row;
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
// Acknowledged state lives inside the packed Value string, so the
|
||||
// filter has to run client-side after materialising.
|
||||
var rows = await db.PlayerSettings
|
||||
.Where(s => s.PlayerId == me && s.Key.StartsWith("warning:"))
|
||||
.OrderByDescending(s => s.Id)
|
||||
.ToListAsync();
|
||||
row = rows.FirstOrDefault(s => !ToWarningWire(s).Acknowledged);
|
||||
if (row is null) return Ok(new { Success = true, Message = string.Empty });
|
||||
}
|
||||
else
|
||||
{
|
||||
row = await db.PlayerSettings
|
||||
.FirstOrDefaultAsync(s => s.PlayerId == me && s.Key == $"warning:{id}");
|
||||
if (row is null) return Ok(new { Success = true, Message = string.Empty });
|
||||
}
|
||||
|
||||
var parts = row.Value.Split('|');
|
||||
row.Value = string.Join('|', parts.Take(3).Append("true"));
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(new { Success = true, WarningId = id });
|
||||
return Ok(new { Success = true, Message = string.Empty, WarningId = row.Key["warning:".Length..] });
|
||||
}
|
||||
|
||||
private static WarningWire ToWarningWire(DorkNet.Server.Data.Entities.PlayerSettingEntity row)
|
||||
|
||||
@@ -21,6 +21,9 @@ public class PlayersController(PlayerService playerService, NotificationService
|
||||
return Ok(PlayerService.ToProfile(player));
|
||||
}
|
||||
|
||||
// The 2023 client PUTs this right after setting an email; GET-only meant a
|
||||
// 405 on every contact-preferences write.
|
||||
[HttpPut("/api/players/v4/current/contact")]
|
||||
[HttpGet("/api/players/v4/current/contact")]
|
||||
public async Task<IActionResult> CurrentContact()
|
||||
{
|
||||
|
||||
@@ -73,56 +73,180 @@ public class PlaylistsController(
|
||||
public sealed class BoolFieldRequest { public bool? Value { get; set; } }
|
||||
public sealed class TagsRequest { public string? Tags { get; set; } public List<string>? TagsList { get; set; } }
|
||||
|
||||
// The 2023-03-21 client sends these as form-urlencoded PUTs. A
|
||||
// non-optional [FromBody] parameter made [ApiController] demand JSON and
|
||||
// reject every one with 415 before the action ran, so renaming, re-imaging,
|
||||
// re-tagging and publishing a playlist all failed silently. Values are now
|
||||
// read from form, query or JSON body, whichever arrived.
|
||||
|
||||
[HttpPost("/playlists/{playlistId:long}/name")]
|
||||
[HttpPut("/playlists/{playlistId:long}/name")]
|
||||
[Authorize]
|
||||
public Task<IActionResult> PlaylistName(long playlistId, [FromBody] StringFieldRequest req,
|
||||
[FromForm(Name = "Name")] string? formValue) =>
|
||||
ApplyMutation(playlistId, p => p.Name = (req?.Value ?? formValue ?? p.Name).Trim());
|
||||
public async Task<IActionResult> PlaylistName(long playlistId)
|
||||
{
|
||||
var value = await ReadFieldAsync("name", "Name", "value", "Value");
|
||||
return await ApplyMutation(playlistId, p => p.Name = (value ?? p.Name).Trim());
|
||||
}
|
||||
|
||||
[HttpPost("/playlists/{playlistId:long}/description")]
|
||||
[HttpPut("/playlists/{playlistId:long}/description")]
|
||||
[Authorize]
|
||||
public Task<IActionResult> PlaylistDescription(long playlistId, [FromBody] StringFieldRequest req,
|
||||
[FromForm(Name = "Description")] string? formValue) =>
|
||||
ApplyMutation(playlistId, p => p.Description = req?.Value ?? formValue ?? p.Description);
|
||||
public async Task<IActionResult> PlaylistDescription(long playlistId)
|
||||
{
|
||||
var value = await ReadFieldAsync("description", "Description", "value", "Value");
|
||||
return await ApplyMutation(playlistId, p => p.Description = value ?? p.Description);
|
||||
}
|
||||
|
||||
[HttpPost("/playlists/{playlistId:long}/image")]
|
||||
[HttpPut("/playlists/{playlistId:long}/image")]
|
||||
[Authorize]
|
||||
public Task<IActionResult> PlaylistImage(long playlistId, [FromBody] StringFieldRequest req,
|
||||
[FromForm(Name = "ImageName")] string? formValue) =>
|
||||
ApplyMutation(playlistId, p => p.ImageName = req?.Value ?? formValue ?? p.ImageName);
|
||||
public async Task<IActionResult> PlaylistImage(long playlistId)
|
||||
{
|
||||
var value = await ReadFieldAsync("imageName", "ImageName", "value", "Value");
|
||||
return await ApplyMutation(playlistId, p => p.ImageName = value ?? p.ImageName);
|
||||
}
|
||||
|
||||
/// <summary>The client sends REPEATED form fields <c>tag</c> and
|
||||
/// <c>autoTag</c>, not a single CSV <c>Tags</c> value.</summary>
|
||||
[HttpPost("/playlists/{playlistId:long}/tags")]
|
||||
[HttpPut("/playlists/{playlistId:long}/tags")]
|
||||
[Authorize]
|
||||
public Task<IActionResult> PlaylistTags(long playlistId, [FromBody] TagsRequest? req,
|
||||
[FromForm(Name = "Tags")] string? formValue) =>
|
||||
ApplyMutation(playlistId, p =>
|
||||
public async Task<IActionResult> PlaylistTags(long playlistId)
|
||||
{
|
||||
var tags = await ReadFieldsAsync("tag", "Tag", "autoTag", "AutoTag", "tags", "Tags");
|
||||
return await ApplyMutation(playlistId, p =>
|
||||
{
|
||||
var csv = req?.Tags
|
||||
?? (req?.TagsList is { Count: > 0 } list ? string.Join(',', list) : null)
|
||||
?? formValue
|
||||
?? p.TagsCsv;
|
||||
p.TagsCsv = csv;
|
||||
if (tags.Count > 0)
|
||||
p.TagsCsv = string.Join(',', tags.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Accessibility / visibility / level-voting / restrictions /
|
||||
/// warning are persisted on the playlist row even though the 2020
|
||||
/// PlaylistEntity doesn't have dedicated columns for each — the wire
|
||||
/// shape always reports the union-entry defaults, so for now the
|
||||
/// mutation is a no-op acknowledgement that returns the unchanged
|
||||
/// playlist. Real per-field columns can be added when we have a
|
||||
/// playlist-settings UI to drive them.</summary>
|
||||
[HttpPost("/playlists/{playlistId:long}/accessibility")]
|
||||
[HttpPut("/playlists/{playlistId:long}/accessibility")]
|
||||
[HttpPost("/playlists/{playlistId:long}/visibility")]
|
||||
[HttpPost("/playlists/{playlistId:long}/levelvoting")]
|
||||
[HttpPost("/playlists/{playlistId:long}/restrictions")]
|
||||
[HttpPost("/playlists/{playlistId:long}/warning")]
|
||||
[HttpPut("/playlists/{playlistId:long}/visibility")]
|
||||
[Authorize]
|
||||
public Task<IActionResult> PlaylistAck(long playlistId) =>
|
||||
ApplyMutation(playlistId, _ => { /* no-op: field not persisted yet */ });
|
||||
public async Task<IActionResult> PlaylistAccessibility(long playlistId)
|
||||
{
|
||||
var value = await ReadFieldAsync(
|
||||
"accessibility", "Accessibility", "visibility", "Visibility", "value", "Value");
|
||||
return await ApplyMutation(playlistId, p =>
|
||||
{
|
||||
if (int.TryParse(value, out var v)) p.Accessibility = v;
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("/playlists/{playlistId:long}/levelvoting")]
|
||||
[HttpPut("/playlists/{playlistId:long}/levelvoting")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> PlaylistLevelVoting(long playlistId)
|
||||
{
|
||||
var value = await ReadFieldAsync("supportsLevelVoting", "SupportsLevelVoting", "value", "Value");
|
||||
return await ApplyMutation(playlistId, p =>
|
||||
{
|
||||
if (bool.TryParse(value, out var v)) p.SupportsLevelVoting = v;
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("/playlists/{playlistId:long}/restrictions")]
|
||||
[HttpPut("/playlists/{playlistId:long}/restrictions")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> PlaylistRestrictions(long playlistId)
|
||||
{
|
||||
var juniors = await ReadFieldAsync("supportsJuniors", "SupportsJuniors");
|
||||
var screens = await ReadFieldAsync("supportsScreens", "SupportsScreens");
|
||||
var teleport = await ReadFieldAsync("supportsTeleportVR", "SupportsTeleportVR");
|
||||
var walk = await ReadFieldAsync("supportsWalkVR", "SupportsWalkVR");
|
||||
return await ApplyMutation(playlistId, p =>
|
||||
{
|
||||
if (bool.TryParse(juniors, out var j)) p.SupportsJuniors = j;
|
||||
if (bool.TryParse(screens, out var c)) p.SupportsScreens = c;
|
||||
if (bool.TryParse(teleport, out var t)) p.SupportsTeleportVR = t;
|
||||
if (bool.TryParse(walk, out var w)) p.SupportsWalkVR = w;
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("/playlists/{playlistId:long}/warning")]
|
||||
[HttpPut("/playlists/{playlistId:long}/warning")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> PlaylistWarning(long playlistId)
|
||||
{
|
||||
var mask = await ReadFieldAsync("warningMask", "WarningMask");
|
||||
var custom = await ReadFieldAsync("customWarning", "CustomWarning");
|
||||
return await ApplyMutation(playlistId, p =>
|
||||
{
|
||||
if (int.TryParse(mask, out var m)) p.WarningMask = m;
|
||||
if (custom is not null) p.CustomWarning = custom.Length > 512 ? custom[..512] : custom;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>First value present under any of the given names, looking at
|
||||
/// the form, the query string, then a JSON body. Null means "not sent",
|
||||
/// which every caller treats as "leave this field alone".</summary>
|
||||
private async Task<string?> ReadFieldAsync(params string[] names)
|
||||
{
|
||||
var all = await ReadFieldsAsync(names);
|
||||
return all.Count > 0 ? all[0] : null;
|
||||
}
|
||||
|
||||
/// <summary>Every value present under any of the given names — the client
|
||||
/// repeats a field name to send a list.</summary>
|
||||
private async Task<List<string>> ReadFieldsAsync(params string[] names)
|
||||
{
|
||||
var found = new List<string>();
|
||||
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
foreach (var n in names)
|
||||
foreach (var v in form[n])
|
||||
if (!string.IsNullOrWhiteSpace(v)) found.Add(v!);
|
||||
}
|
||||
foreach (var n in names)
|
||||
foreach (var v in Request.Query[n])
|
||||
if (!string.IsNullOrWhiteSpace(v)) found.Add(v!);
|
||||
if (found.Count > 0) return found;
|
||||
|
||||
if (!Request.HasFormContentType)
|
||||
{
|
||||
try
|
||||
{
|
||||
Request.EnableBuffering();
|
||||
Request.Body.Position = 0;
|
||||
using var doc = await System.Text.Json.JsonDocument.ParseAsync(Request.Body);
|
||||
Request.Body.Position = 0;
|
||||
var root = doc.RootElement;
|
||||
|
||||
// The tags endpoint sends a bare JSON array of strings.
|
||||
if (root.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
foreach (var el in root.EnumerateArray())
|
||||
if (el.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
found.Add(el.GetString()!);
|
||||
return found;
|
||||
}
|
||||
|
||||
if (root.ValueKind == System.Text.Json.JsonValueKind.Object)
|
||||
{
|
||||
foreach (var n in names)
|
||||
{
|
||||
if (!root.TryGetProperty(n, out var v)) continue;
|
||||
if (v.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
foreach (var el in v.EnumerateArray()) found.Add(el.ToString());
|
||||
}
|
||||
else if (v.ValueKind != System.Text.Json.JsonValueKind.Null)
|
||||
{
|
||||
found.Add(v.ToString());
|
||||
}
|
||||
if (found.Count > 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Text.Json.JsonException) { /* no usable body */ }
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ApplyMutation(long playlistId, Action<PlaylistEntity> mutator)
|
||||
{
|
||||
|
||||
@@ -72,6 +72,30 @@ public class RoleController(DorkNetDbContext db) : ControllerBase
|
||||
return Ok(isAdmin ?? false);
|
||||
}
|
||||
|
||||
/// <summary>The March-2023 client probes this one in addition to
|
||||
/// <c>role/developer</c>. Both calls funnel through the same helper
|
||||
/// (<c>OPMAPIOEIFG.ILHILCFCNFF</c>, which formats <c>"role/{0}/{1}"</c>
|
||||
/// and issues it with HTTPMethods.GET); the two call sites differ only
|
||||
/// in the role literal they pass — <c>"developer"</c> at
|
||||
/// RecNet.Runtime/OPMAPIOEIFG.txt:9402 and <c>"moderator"</c> at
|
||||
/// :9471. The 2020 build never referenced the moderator probe, so it
|
||||
/// used to 404 for every remote player on the 2023 client.
|
||||
///
|
||||
/// DorkNet has no dedicated moderator bit, so admins and community-team
|
||||
/// members answer true — they are the accounts that actually hold
|
||||
/// moderation powers here.</summary>
|
||||
[HttpGet("/role/moderator/{accountId:long}")]
|
||||
[HttpGet("/api/role/moderator/{accountId:long}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<bool>> IsModerator(long accountId)
|
||||
{
|
||||
var isMod = await db.Players
|
||||
.Where(p => p.Id == accountId)
|
||||
.Select(p => (bool?)(p.IsAdmin || p.IsCommunityTeam))
|
||||
.FirstOrDefaultAsync();
|
||||
return Ok(isMod ?? false);
|
||||
}
|
||||
|
||||
/// <summary>Future-proofing for a hypothetical
|
||||
/// <c>role/communityteam/{id}</c> probe — not referenced by the 2020
|
||||
/// build, but mirrors the developer shape so a patched watch can
|
||||
|
||||
@@ -227,6 +227,11 @@ public class RoomConsumablesController(DorkNetDbContext db, LevelService level)
|
||||
/// — buy one stack unit with tokens. Response is
|
||||
/// <c>{ OperationResult, BalanceUpdateResult, TokenBalanceResponse:
|
||||
/// { CurrencyType, Balance, Platform } }</c>.</summary>
|
||||
// The 2023-03-21 client PUTs this (verb literal 3 = PUT, at the route
|
||||
// construction in RecNet.Runtime/DCFKEFHJAGC.txt instrs 097-108), so a
|
||||
// POST-only mapping returned 405 and token-priced consumables were
|
||||
// unbuyable. Both verbs are accepted now.
|
||||
[HttpPut("api/roomconsumables/v1/roomconsumable/{publicId:guid}/purchase/tokens")]
|
||||
[HttpPost("api/roomconsumables/v1/roomconsumable/{publicId:guid}/purchase/tokens")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> PurchaseTokens(Guid publicId)
|
||||
@@ -263,6 +268,9 @@ public class RoomConsumablesController(DorkNetDbContext db, LevelService level)
|
||||
/// — buy with a room currency. Response is <c>{ OperationResult,
|
||||
/// BalanceUpdateResult, CurrencyBalanceResponse: { AccountId,
|
||||
/// CurrencyId, Balance, ModifiedAt } }</c>.</summary>
|
||||
// PUT per the client (verb literal 3 at DCFKEFHJAGC.txt instrs 112-123);
|
||||
// POST-only returned 405 and currency-priced consumables were unbuyable.
|
||||
[HttpPut("api/roomconsumables/v1/roomconsumable/{publicId:guid}/purchase/currency")]
|
||||
[HttpPost("api/roomconsumables/v1/roomconsumable/{publicId:guid}/purchase/currency")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> PurchaseCurrency(Guid publicId)
|
||||
@@ -304,6 +312,10 @@ public class RoomConsumablesController(DorkNetDbContext db, LevelService level)
|
||||
/// NewConcurrencyCode }</c>; response <c>{ Status, InventoryItem }</c>.
|
||||
/// On a code mismatch the current row is returned so the client can
|
||||
/// resync.</summary>
|
||||
// PUT per the client (route format "{0}/v1/roomConsumable/{1}/consume"
|
||||
// with verb literal 3 at RecNet.Runtime/LPNHMEFDAAG.txt:1449-1460);
|
||||
// POST-only returned 405, so no consumable could ever be consumed.
|
||||
[HttpPut("api/roomconsumables/v1/roomConsumable/{publicId:guid}/consume")]
|
||||
[HttpPost("api/roomconsumables/v1/roomConsumable/{publicId:guid}/consume")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Consume(Guid publicId)
|
||||
@@ -404,6 +416,18 @@ public class RoomConsumablesController(DorkNetDbContext db, LevelService level)
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>Consumable descriptor as the client READS it.
|
||||
///
|
||||
/// The price is FLAT here — the client's response formatter FCIBLPCOODP
|
||||
/// reads <c>Price</c>, <c>PurchaseCurrencyId</c> and <c>ModifiedAt</c>
|
||||
/// directly off the descriptor (RecNet.Runtime/FCIBLPCOODP.txt:670-710).
|
||||
/// Emitting a nested <c>PriceAndCurrency</c> object left every item in
|
||||
/// every room shop reading back Price=0 with a null currency, so the whole
|
||||
/// catalogue looked token-priced and free.
|
||||
///
|
||||
/// Note the asymmetry: REQUEST bodies really do nest PriceAndCurrency, and
|
||||
/// ReadBodyAsync below still flattens that on the way in. Only the response
|
||||
/// shape changed.</summary>
|
||||
private static object ToDescWire(RoomConsumableEntity c) => new
|
||||
{
|
||||
RoomConsumableId = c.PublicId,
|
||||
@@ -411,7 +435,9 @@ public class RoomConsumablesController(DorkNetDbContext db, LevelService level)
|
||||
c.Name,
|
||||
c.Description,
|
||||
c.ImageName,
|
||||
PriceAndCurrency = new { c.Price, c.CurrencyId },
|
||||
c.Price,
|
||||
PurchaseCurrencyId = c.CurrencyId,
|
||||
ModifiedAt = DateTime.SpecifyKind(c.UpdatedAt, DateTimeKind.Utc),
|
||||
};
|
||||
|
||||
private static object ToInventoryWire(RoomConsumableOwnershipEntity own, RoomConsumableEntity item) => new
|
||||
|
||||
@@ -45,7 +45,7 @@ public class RoomCurrenciesController(DorkNetDbContext db, LevelService level) :
|
||||
Name = name,
|
||||
Description = Trim(ReadString(fields, "description", "Description"), 512),
|
||||
ImageName = Trim(ReadString(fields, "imageName", "ImageName"), 256),
|
||||
DailyLimit = Math.Max(0, ReadInt(fields, "dailyLimit", "DailyLimit") ?? 0),
|
||||
DailyLimit = Math.Max(0, ReadInt(fields, "limit", "Limit", "dailyLimit", "DailyLimit") ?? 0),
|
||||
};
|
||||
db.RoomCurrencies.Add(currency);
|
||||
await db.SaveChangesAsync();
|
||||
@@ -65,7 +65,7 @@ public class RoomCurrenciesController(DorkNetDbContext db, LevelService level) :
|
||||
if (ReadString(fields, "name", "Name") is { } name) currency.Name = Trim(name, 64);
|
||||
if (ReadString(fields, "description", "Description") is { } description) currency.Description = Trim(description, 512);
|
||||
if (ReadString(fields, "imageName", "ImageName") is { } imageName) currency.ImageName = Trim(imageName, 256);
|
||||
if (ReadInt(fields, "dailyLimit", "DailyLimit") is int dailyLimit) currency.DailyLimit = Math.Max(0, dailyLimit);
|
||||
if (ReadInt(fields, "limit", "Limit", "dailyLimit", "DailyLimit") is int dailyLimit) currency.DailyLimit = Math.Max(0, dailyLimit);
|
||||
currency.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(ToWire(currency));
|
||||
@@ -77,13 +77,20 @@ public class RoomCurrenciesController(DorkNetDbContext db, LevelService level) :
|
||||
var fields = await ReadFieldsAsync();
|
||||
var currency = await FindCurrencyAsync(fields);
|
||||
if (currency is null) return NotFound();
|
||||
var playerId = ReadLong(fields, "playerId", "PlayerId") ?? Me;
|
||||
// The client sends "accountId" and can ask about ANOTHER player
|
||||
// (circuits read other players' balances); reading only playerId made
|
||||
// every such query silently answer for the caller instead.
|
||||
var playerId = ReadLong(fields, "accountId", "AccountId", "playerId", "PlayerId") ?? Me;
|
||||
var balance = await GetBalanceAsync(playerId, currency.Id);
|
||||
return Ok(new
|
||||
{
|
||||
AccountId = (int)playerId,
|
||||
CurrencyId = currency.PublicId,
|
||||
Balance = balance,
|
||||
ModifiedAt = DateTime.UtcNow,
|
||||
// legacy aliases
|
||||
PlayerId = (int)playerId,
|
||||
RoomCurrencyId = currency.PublicId,
|
||||
Balance = balance,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -104,9 +111,13 @@ public class RoomCurrenciesController(DorkNetDbContext db, LevelService level) :
|
||||
.ToListAsync();
|
||||
return Ok(rows.Select(x => new
|
||||
{
|
||||
AccountId = (int)playerId,
|
||||
CurrencyId = x.c.PublicId,
|
||||
x.b.Balance,
|
||||
ModifiedAt = DateTime.SpecifyKind(x.b.UpdatedAt, DateTimeKind.Utc),
|
||||
// legacy aliases
|
||||
PlayerId = (int)playerId,
|
||||
RoomCurrencyId = x.c.PublicId,
|
||||
x.b.Balance,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -373,33 +384,62 @@ public class RoomCurrenciesController(DorkNetDbContext db, LevelService level) :
|
||||
return s.Length <= max ? s : s[..max];
|
||||
}
|
||||
|
||||
/// <summary>Room currency as the client READS it (formatter PEOAOMGOMGC):
|
||||
/// <c>CurrencyId</c>, <c>RoomId</c>, <c>Name</c>, <c>Description</c>,
|
||||
/// <c>CurrencyType</c>, <c>Limit</c>, <c>ImageName</c>, <c>CreatedAt</c>,
|
||||
/// <c>ModifiedAt</c>.
|
||||
///
|
||||
/// The previous names were server inventions (<c>RoomCurrencyId</c>,
|
||||
/// <c>DailyLimit</c>, <c>UpdatedAt</c>) that shared almost no keys with
|
||||
/// what the client reads, so every currency came back with an empty
|
||||
/// CurrencyId and a zero limit — the room currency HUD could not match a
|
||||
/// balance to a currency. The legacy aliases are kept alongside the correct
|
||||
/// names because unknown members are ignored client-side and other DorkNet
|
||||
/// surfaces (admin UI) still read them.</summary>
|
||||
private static object ToWire(RoomCurrencyEntity currency) => new
|
||||
{
|
||||
CurrencyId = currency.PublicId,
|
||||
currency.RoomId,
|
||||
currency.Name,
|
||||
currency.Description,
|
||||
CurrencyType = 300,
|
||||
Limit = (long)currency.DailyLimit,
|
||||
currency.ImageName,
|
||||
CreatedAt = DateTime.SpecifyKind(currency.CreatedAt, DateTimeKind.Utc),
|
||||
ModifiedAt = DateTime.SpecifyKind(currency.UpdatedAt, DateTimeKind.Utc),
|
||||
|
||||
// legacy aliases
|
||||
RoomCurrencyId = currency.PublicId,
|
||||
Id = currency.PublicId,
|
||||
InternalId = currency.Id,
|
||||
currency.RoomId,
|
||||
CreatorPlayerId = (int)currency.CreatorPlayerId,
|
||||
currency.Name,
|
||||
currency.Description,
|
||||
currency.ImageName,
|
||||
currency.DailyLimit,
|
||||
currency.CreatedAt,
|
||||
currency.UpdatedAt,
|
||||
};
|
||||
|
||||
/// <summary>Purchase offer as the client READS it (formatter AALDCAANEMM):
|
||||
/// <c>CurrencyPurchaseOfferId</c>, <c>CurrencyId</c>, <c>Name</c>,
|
||||
/// <c>CurrencyAmount</c>, <c>Price</c>, <c>CurrencyType</c>,
|
||||
/// <c>CreatedAt</c>, <c>ModifiedAt</c>. Legacy aliases retained as in
|
||||
/// <see cref="ToWire"/>.</summary>
|
||||
private static object ToOfferWire(RoomCurrencyPurchaseOfferEntity offer, RoomCurrencyEntity currency) => new
|
||||
{
|
||||
CurrencyPurchaseOfferId = offer.PublicId,
|
||||
CurrencyId = currency.PublicId,
|
||||
offer.Name,
|
||||
CurrencyAmount = offer.Amount,
|
||||
offer.Price,
|
||||
offer.CurrencyType,
|
||||
CreatedAt = DateTime.SpecifyKind(offer.CreatedAt, DateTimeKind.Utc),
|
||||
ModifiedAt = DateTime.SpecifyKind(offer.UpdatedAt, DateTimeKind.Utc),
|
||||
|
||||
// legacy aliases
|
||||
PurchaseOfferId = offer.PublicId,
|
||||
RoomCurrencyPackageId = offer.PublicId,
|
||||
Id = offer.PublicId,
|
||||
InternalId = offer.Id,
|
||||
RoomCurrencyId = currency.PublicId,
|
||||
offer.Name,
|
||||
offer.Amount,
|
||||
offer.Price,
|
||||
offer.CurrencyType,
|
||||
offer.CreatedAt,
|
||||
offer.UpdatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,12 +31,21 @@ public class RoomKeysController(DorkNetDbContext db) : ControllerBase
|
||||
return Ok(rows.Select(ToWire));
|
||||
}
|
||||
|
||||
/// <summary>The watch key ring — keys the caller HOLDS. This used to return
|
||||
/// keys the caller CREATED, which is a different set: a creator never buys
|
||||
/// their own key, so players saw none of the keys they had actually
|
||||
/// purchased. Keys the caller created are unioned in so room owners still
|
||||
/// see their own.</summary>
|
||||
[HttpGet("api/roomkeys/v1/mine")]
|
||||
public async Task<IActionResult> Mine()
|
||||
{
|
||||
var pid = Me;
|
||||
var purchasedIds = await db.RoomKeyPurchases
|
||||
.Where(p => p.PlayerId == pid)
|
||||
.Select(p => p.RoomKeyId)
|
||||
.ToListAsync();
|
||||
var rows = await db.RoomKeys
|
||||
.Where(k => k.CreatorPlayerId == pid && !k.IsDeleted)
|
||||
.Where(k => !k.IsDeleted && (purchasedIds.Contains(k.Id) || k.CreatorPlayerId == pid))
|
||||
.OrderByDescending(k => k.UpdatedAt)
|
||||
.ToListAsync();
|
||||
return Ok(rows.Select(ToWire));
|
||||
@@ -93,6 +102,25 @@ public class RoomKeysController(DorkNetDbContext db) : ControllerBase
|
||||
return Ok(RoomKeyResponse(RoomKeyStatus.Success, key));
|
||||
}
|
||||
|
||||
/// <summary>The 2023-03-21 client does not call <c>update</c>. It builds
|
||||
/// <c>"api/roomkeys/v1/" + action</c> with verb PUT and a <c>RoomKeyId</c>
|
||||
/// field, where action is one of <c>updateAll</c>, <c>updateName</c>,
|
||||
/// <c>updateDescription</c> or <c>updatePrice</c>
|
||||
/// (RecNet.Runtime/AHMBBJNANBP.txt: prefix concat at instr 058, verb 3 at
|
||||
/// 067, <c>"RoomKeyId"</c> at 079; action literals at :1345, :1559, :1772,
|
||||
/// :2043). None were registered, so every room-key edit 405'd.
|
||||
///
|
||||
/// All four share this handler: the request DTO's fields are already
|
||||
/// optional, so a partial update naturally leaves the others untouched.</summary>
|
||||
[HttpPut("api/roomkeys/v1/updateAll")]
|
||||
[HttpPost("api/roomkeys/v1/updateAll")]
|
||||
[HttpPut("api/roomkeys/v1/updateName")]
|
||||
[HttpPost("api/roomkeys/v1/updateName")]
|
||||
[HttpPut("api/roomkeys/v1/updateDescription")]
|
||||
[HttpPost("api/roomkeys/v1/updateDescription")]
|
||||
[HttpPut("api/roomkeys/v1/updatePrice")]
|
||||
[HttpPost("api/roomkeys/v1/updatePrice")]
|
||||
[HttpPut("api/roomkeys/v1/update")]
|
||||
[HttpPost("api/roomkeys/v1/update")]
|
||||
public async Task<IActionResult> Update()
|
||||
{
|
||||
@@ -140,35 +168,104 @@ public class RoomKeysController(DorkNetDbContext db) : ControllerBase
|
||||
return Content(((int)RoomKeyStatus.Success).ToString(), "application/json");
|
||||
}
|
||||
|
||||
/// <summary>"Has anyone bought this key?" — the creator calls this before a
|
||||
/// destructive edit. It previously answered "has the CALLER bought it",
|
||||
/// which is always false for the creator, so the guard never fired and
|
||||
/// creators could silently invalidate keys players had paid for.</summary>
|
||||
[HttpGet("api/roomkeys/v1/purchased/{roomKeyId:long}")]
|
||||
public async Task<IActionResult> Purchased(long roomKeyId)
|
||||
{
|
||||
var pid = Me;
|
||||
var owns = await db.RoomKeyPurchases.AnyAsync(p => p.RoomKeyId == roomKeyId && p.PlayerId == pid);
|
||||
return Content(owns ? "true" : "false", "application/json");
|
||||
var anyPurchase = await db.RoomKeyPurchases.AnyAsync(p => p.RoomKeyId == roomKeyId);
|
||||
return Content(anyPurchase ? "true" : "false", "application/json");
|
||||
}
|
||||
|
||||
/// <summary>The client sends both <c>roomKeyId</c> and <c>playerId</c> — key
|
||||
/// doors evaluate OTHER players' ownership, not just the caller's. Ignoring
|
||||
/// playerId meant a door asked about someone else and got the caller's
|
||||
/// answer.</summary>
|
||||
[HttpGet("api/roomkeys/v1/owns")]
|
||||
public async Task<IActionResult> Owns([FromQuery] long roomKeyId)
|
||||
public async Task<IActionResult> Owns([FromQuery] long roomKeyId, [FromQuery] long? playerId = null)
|
||||
{
|
||||
var pid = Me;
|
||||
var owns = await db.RoomKeyPurchases.AnyAsync(p => p.RoomKeyId == roomKeyId && p.PlayerId == pid);
|
||||
var subject = playerId is long p && p > 0 ? p : Me;
|
||||
var owns = await db.RoomKeyPurchases.AnyAsync(x => x.RoomKeyId == roomKeyId && x.PlayerId == subject);
|
||||
return Content(owns ? "true" : "false", "application/json");
|
||||
}
|
||||
|
||||
/// <summary>Bulk key-ownership probe, used by key doors to gate a whole
|
||||
/// room of players at once.
|
||||
///
|
||||
/// The 2023-03-21 client POSTs a JSON <b>array</b> of
|
||||
/// <c>{RoomKeyId, AccountId}</c> pairs as a raw body
|
||||
/// (RecNet.Runtime/AHMBBJNANBP_NestedType_JIPDBBEFAPN.txt instrs 044-065),
|
||||
/// so each element names the player it is asking about. The old
|
||||
/// query/form-only reader saw an empty id list and answered for the caller,
|
||||
/// meaning key doors evaluated the wrong player — or nobody at all.</summary>
|
||||
[HttpGet("api/roomkeys/v1/owns/bulk")]
|
||||
[HttpPost("api/roomkeys/v1/owns/bulk")]
|
||||
[Consumes("application/json", "application/x-www-form-urlencoded", "multipart/form-data")]
|
||||
public async Task<IActionResult> OwnsBulk()
|
||||
{
|
||||
var ids = await ReadRoomKeyIdsAsync();
|
||||
if (ids.Count == 0) return Ok(Array.Empty<object>());
|
||||
var pid = Me;
|
||||
var owned = await db.RoomKeyPurchases
|
||||
.Where(p => p.PlayerId == pid && ids.Contains(p.RoomKeyId))
|
||||
.Select(p => p.RoomKeyId)
|
||||
.ToListAsync();
|
||||
var ownedSet = owned.ToHashSet();
|
||||
return Ok(ids.Select(id => new { RoomKeyId = id, Owns = ownedSet.Contains(id) }).ToList());
|
||||
var pairs = await ReadOwnershipPairsAsync();
|
||||
if (pairs.Count == 0)
|
||||
{
|
||||
// Legacy query/form path: ids only, always about the caller.
|
||||
var ids = await ReadRoomKeyIdsAsync();
|
||||
if (ids.Count == 0) return Ok(Array.Empty<object>());
|
||||
pairs = ids.Select(id => (RoomKeyId: id, AccountId: Me)).ToList();
|
||||
}
|
||||
|
||||
var keyIds = pairs.Select(p => p.RoomKeyId).Distinct().ToList();
|
||||
var accountIds = pairs.Select(p => p.AccountId).Distinct().ToList();
|
||||
var owned = (await db.RoomKeyPurchases
|
||||
.Where(p => keyIds.Contains(p.RoomKeyId) && accountIds.Contains(p.PlayerId))
|
||||
.Select(p => new { p.RoomKeyId, p.PlayerId })
|
||||
.ToListAsync())
|
||||
.Select(p => (p.RoomKeyId, p.PlayerId))
|
||||
.ToHashSet();
|
||||
|
||||
return Ok(pairs.Select(p => new
|
||||
{
|
||||
p.RoomKeyId,
|
||||
AccountId = (int)p.AccountId,
|
||||
DoesPlayerOwnRoomKey = owned.Contains((p.RoomKeyId, p.AccountId)),
|
||||
// legacy alias
|
||||
Owns = owned.Contains((p.RoomKeyId, p.AccountId)),
|
||||
}).ToList());
|
||||
}
|
||||
|
||||
/// <summary>Parse the 2023 client's JSON array of
|
||||
/// <c>{RoomKeyId, AccountId}</c> pairs. Returns empty for a form post or a
|
||||
/// body that isn't an array, so the legacy path still applies.</summary>
|
||||
private async Task<List<(long RoomKeyId, long AccountId)>> ReadOwnershipPairsAsync()
|
||||
{
|
||||
var pairs = new List<(long, long)>();
|
||||
if (Request.HasFormContentType) return pairs;
|
||||
try
|
||||
{
|
||||
Request.EnableBuffering();
|
||||
Request.Body.Position = 0;
|
||||
using var doc = await System.Text.Json.JsonDocument.ParseAsync(Request.Body);
|
||||
Request.Body.Position = 0;
|
||||
if (doc.RootElement.ValueKind != System.Text.Json.JsonValueKind.Array) return pairs;
|
||||
|
||||
foreach (var el in doc.RootElement.EnumerateArray())
|
||||
{
|
||||
if (el.ValueKind != System.Text.Json.JsonValueKind.Object) continue;
|
||||
long? Read(params string[] names)
|
||||
{
|
||||
foreach (var n in names)
|
||||
if (el.TryGetProperty(n, out var v) &&
|
||||
v.ValueKind == System.Text.Json.JsonValueKind.Number &&
|
||||
v.TryGetInt64(out var x)) return x;
|
||||
return null;
|
||||
}
|
||||
var keyId = Read("RoomKeyId", "roomKeyId");
|
||||
if (keyId is not long k || k <= 0) continue;
|
||||
pairs.Add((k, Read("AccountId", "accountId", "PlayerId", "playerId") ?? Me));
|
||||
}
|
||||
}
|
||||
catch (System.Text.Json.JsonException) { /* not a JSON array body */ }
|
||||
return pairs;
|
||||
}
|
||||
|
||||
public static object RoomKeyResponse(RoomKeyStatus status, RoomKeyEntity? key) => new
|
||||
@@ -300,6 +397,8 @@ public class RoomKeysController(DorkNetDbContext db) : ControllerBase
|
||||
|
||||
public sealed class UpdateRoomKeyRequest
|
||||
{
|
||||
// The 2023 client spells this "RoomKeyId"; System.Text.Json is
|
||||
// configured case-insensitively so both land on this property.
|
||||
[JsonPropertyName("roomKeyId")] public long RoomKeyId { get; set; }
|
||||
[JsonPropertyName("name")] public string? Name { get; set; }
|
||||
[JsonPropertyName("description")] public string? Description { get; set; }
|
||||
|
||||
@@ -304,7 +304,11 @@ public class RoomsController(
|
||||
/// </summary>
|
||||
[HttpGet("api/rooms/v2/baserooms")]
|
||||
[HttpGet("api/rooms/v3/baserooms")]
|
||||
// The 2023 client issues this from NLDBPDCNNCF, whose requests all carry
|
||||
// the roomserver/ prefix in this deployment — without the twin the base/AG
|
||||
// room list 404s and the room browser's base section is empty.
|
||||
[HttpGet("rooms/base")]
|
||||
[HttpGet("roomserver/rooms/base")]
|
||||
public async Task<IActionResult> BaseRooms()
|
||||
{
|
||||
var names = await serverSettings.GetBaseRoomNamesAsync();
|
||||
@@ -2532,7 +2536,9 @@ public class RoomsController(
|
||||
/// <c>api/rooms/v3/featured</c>). Returning the
|
||||
/// <see cref="Featured"/> shape here throws KeyNotFoundException on
|
||||
/// the watch's group-id lookup.</summary>
|
||||
// Same roomserver/ prefix requirement as rooms/base.
|
||||
[HttpGet("featuredrooms/current")]
|
||||
[HttpGet("roomserver/featuredrooms/current")]
|
||||
public async Task<IActionResult> FeaturedRoomsCurrent()
|
||||
{
|
||||
var ids = await rooms.FeaturedAgRoomIdsAsync(12);
|
||||
@@ -2540,10 +2546,22 @@ public class RoomsController(
|
||||
.Where(r => ids.Contains(r.Id))
|
||||
.ToListAsync();
|
||||
var byId = featuredRooms.ToDictionary(r => r.Id);
|
||||
// The featured-tile reader looks for RoomName, which ToWireRoom does not
|
||||
// emit — every tile's caption came back null. Re-project the wire object
|
||||
// into a dictionary so the extra key sits alongside the real ones rather
|
||||
// than nesting them.
|
||||
var wireRooms = ids
|
||||
.Select(id => byId.TryGetValue(id, out var room) ? RoomService.ToWireRoom(room) : null)
|
||||
.Where(room => room is not null)
|
||||
.Cast<object>()
|
||||
.Where(byId.ContainsKey)
|
||||
.Select(id =>
|
||||
{
|
||||
var room = byId[id];
|
||||
var wire = RoomService.ToWireRoom(room);
|
||||
var map = new Dictionary<string, object?>();
|
||||
foreach (var prop in wire.GetType().GetProperties())
|
||||
map[prop.Name] = prop.GetValue(wire);
|
||||
map["RoomName"] = room.Name;
|
||||
return (object)map;
|
||||
})
|
||||
.ToList();
|
||||
return Ok(new
|
||||
{
|
||||
|
||||
@@ -10,22 +10,50 @@ namespace DorkNet.Server.Controllers.API.ScreenShareReports;
|
||||
[Authorize]
|
||||
public class ScreenShareReportsController(DorkNetDbContext db) : ControllerBase
|
||||
{
|
||||
/// <summary>Screen-share (photo) report. The 2023-03-21 client posts the
|
||||
/// form keys <c>ImageName</c>, <c>ReportedPlayerId</c>, <c>RoomId</c>,
|
||||
/// <c>RoomInstanceId</c>, <c>RoomInstanceType</c> and <c>Details</c>
|
||||
/// (RecNet.Runtime/KAEAEIODGBG.txt:281-360). It sends no
|
||||
/// <c>reportCategory</c> at all.
|
||||
///
|
||||
/// The old signature bound <c>targetPlayerId</c>, so the reported player
|
||||
/// was stored as 0 on every screen-share report and the reported image was
|
||||
/// dropped entirely — the report was unactionable. Form binding is
|
||||
/// case-insensitive, so <c>RoomId</c>/<c>Details</c> did land; the names
|
||||
/// that differ are the ones spelled out explicitly below.</summary>
|
||||
[HttpPost("api/screensharereports/v1/report")]
|
||||
public async Task<IActionResult> Report(
|
||||
[FromForm(Name = "ReportedPlayerId")] long? reportedPlayerId,
|
||||
[FromForm] long? targetPlayerId,
|
||||
[FromForm] long? roomId,
|
||||
[FromForm(Name = "RoomInstanceId")] long? roomInstanceId,
|
||||
[FromForm(Name = "RoomInstanceType")] int? roomInstanceType,
|
||||
[FromForm(Name = "ImageName")] string? imageName,
|
||||
[FromForm] int? reportCategory,
|
||||
[FromForm] string? details)
|
||||
{
|
||||
var reporter = this.RequireCurrentPlayerId();
|
||||
var target = targetPlayerId
|
||||
?? (long.TryParse(Request.Query["targetPlayerId"].FirstOrDefault(), out var qTarget) ? qTarget : 0);
|
||||
var target = reportedPlayerId
|
||||
?? targetPlayerId
|
||||
?? (long.TryParse(Request.Query["ReportedPlayerId"].FirstOrDefault()
|
||||
?? Request.Query["targetPlayerId"].FirstOrDefault(), out var qTarget) ? qTarget : 0);
|
||||
var room = roomId
|
||||
?? (long.TryParse(Request.Query["roomId"].FirstOrDefault(), out var qRoom) ? qRoom : 0);
|
||||
// The client never sends a category for screen-share reports; 5 is the
|
||||
// server's existing "other" bucket.
|
||||
var category = reportCategory
|
||||
?? (int.TryParse(Request.Query["reportCategory"].FirstOrDefault(), out var qCategory) ? qCategory : 5);
|
||||
var message = details ?? Request.Query["details"].FirstOrDefault() ?? string.Empty;
|
||||
|
||||
// Keep the evidence (image + instance) in the stored text: ReportEntity
|
||||
// has no dedicated columns for them and a report without the offending
|
||||
// image is not actionable by a moderator.
|
||||
var context = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(imageName)) context.Add($"image={imageName}");
|
||||
if (roomInstanceId is > 0) context.Add($"instance={roomInstanceId}");
|
||||
if (roomInstanceType is not null) context.Add($"instanceType={roomInstanceType}");
|
||||
if (context.Count > 0) message = $"[screenshare {string.Join(' ', context)}] {message}".Trim();
|
||||
|
||||
db.Reports.Add(new ReportEntity
|
||||
{
|
||||
ReporterPlayerId = reporter,
|
||||
|
||||
@@ -11,9 +11,17 @@ namespace DorkNet.Server.Controllers.API.UgcPurchasables;
|
||||
[ApiController]
|
||||
public class UgcPurchasablesController(DorkNetDbContext db) : ControllerBase
|
||||
{
|
||||
/// <summary>The 2023-03-21 client asks for a room's in-room store with the
|
||||
/// path form <c>api/ugcPurchasables/v1/items/room/{roomId}</c>, not the
|
||||
/// <c>?roomId=</c> query this handler was originally written for. Without
|
||||
/// the path route it 404'd and the in-room UGC store never populated on
|
||||
/// room join.</summary>
|
||||
[HttpGet("api/ugcPurchasables/v1/items/room/{roomId:long}")]
|
||||
[HttpGet("api/ugcPurchasables")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> List([FromQuery] long roomId = 0)
|
||||
// No explicit binding source: roomId comes from the route on the path form
|
||||
// above and from the query string on the bare route.
|
||||
public async Task<IActionResult> List(long roomId = 0)
|
||||
{
|
||||
var rows = await db.UgcPurchasables
|
||||
.Where(i => !i.IsDeleted && (roomId <= 0 || i.RoomId == roomId))
|
||||
|
||||
@@ -175,11 +175,18 @@ public class AccountsController(
|
||||
[HttpGet("/account/search")]
|
||||
[HttpGet("/account/v1/search")]
|
||||
[Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
||||
public async Task<IActionResult> Search([FromQuery] string? query, [FromQuery] int take = 20)
|
||||
// The 2023-03-21 client names the search term "name"; binding only "query"
|
||||
// meant every player search came back empty (the handler short-circuited on
|
||||
// a null term before it ever reached the service).
|
||||
public async Task<IActionResult> Search(
|
||||
[FromQuery] string? query,
|
||||
[FromQuery] string? name,
|
||||
[FromQuery] int take = 20)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query)) return Ok(Array.Empty<object>());
|
||||
var term = !string.IsNullOrWhiteSpace(name) ? name : query;
|
||||
if (string.IsNullOrWhiteSpace(term)) return Ok(Array.Empty<object>());
|
||||
var clamped = Math.Clamp(take, 1, 50);
|
||||
var matches = await playerService.SearchAsync(query.Trim(), clamped);
|
||||
var matches = await playerService.SearchAsync(term.Trim(), clamped);
|
||||
return Ok(matches
|
||||
.Select(p => BuildAccount(p.Id, p.Username, p.DisplayName, p.ProfileImageName))
|
||||
.ToList());
|
||||
@@ -338,6 +345,10 @@ public class AccountsController(
|
||||
/// cutoff is <13 years old; we recompute it on every birthday
|
||||
/// write so a freshly turned 13 player loses the junior flag the
|
||||
/// next time they update.</summary>
|
||||
// The 2023 client PUTs birthday (verb 3 at OPMAPIOEIFG.txt:7950-7954);
|
||||
// POST-only returned 405 and the birthday step of onboarding always failed.
|
||||
[HttpPut("/account/v1/birthday")]
|
||||
[HttpPut("/account/me/birthday")]
|
||||
[HttpPost("/account/v1/birthday")]
|
||||
[HttpPost("/account/me/birthday")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
|
||||
@@ -325,6 +325,9 @@ public class AuthController(
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
// Sign-out / "forget this account" sends DELETE (LBNJFPOLCDL.txt:4190,
|
||||
// verb 4); GET-only returned 405 and the forget-account promise rejected.
|
||||
[HttpDelete("/cachedlogin/current")]
|
||||
[HttpGet("/cachedlogin/current")]
|
||||
public async Task<IActionResult> CurrentCachedLogin()
|
||||
{
|
||||
|
||||
@@ -334,6 +334,9 @@ public class ChatController(
|
||||
/// inbox. DMs can't be left — there are only two participants and
|
||||
/// removing one effectively ends the conversation; the watch
|
||||
/// surfaces a "delete chat" option for DMs instead.</summary>
|
||||
// The 2023-03-21 client POSTs this; DELETE-only returned 405 and leaving a
|
||||
// group chat was impossible on that build.
|
||||
[HttpPost("/thread/{chatThreadId}/leave")]
|
||||
[HttpDelete("/thread/{chatThreadId}/leave")]
|
||||
public async Task<IActionResult> Leave(string chatThreadId)
|
||||
{
|
||||
|
||||
@@ -272,6 +272,20 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
{
|
||||
if (HttpMethods.IsPut(Request.Method) || HttpMethods.IsDelete(Request.Method))
|
||||
{
|
||||
// The client sends roomId in the form-urlencoded BODY, not the query
|
||||
// string. Reading only the query bound null, and the PUT branch then
|
||||
// wrote that null straight through — so "set clubhouse" silently
|
||||
// CLEARED the clubhouse instead of setting it.
|
||||
if (roomId is null && Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
foreach (var key in new[] { "roomId", "RoomId" })
|
||||
if (long.TryParse(form[key].FirstOrDefault(), out var fromForm) && fromForm > 0)
|
||||
{
|
||||
roomId = fromForm;
|
||||
break;
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
var updated = await clubs.ModifyAsync(clubId, Me, c =>
|
||||
@@ -368,7 +382,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
[HttpPost("/club/{clubId:long}/modifydetails")]
|
||||
[HttpPut("/club/{clubId:long}/modifydetails")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> ClubModify(long clubId, [FromBody] ModifyClubRequest req)
|
||||
public async Task<IActionResult> ClubModify(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] ModifyClubRequest req)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -396,7 +410,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
[HttpPost("/club/{clubId:long}/mainimage")]
|
||||
[HttpPut("/club/{clubId:long}/mainimage")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> ClubMainImage(long clubId, [FromBody] MainImageRequest req)
|
||||
public async Task<IActionResult> ClubMainImage(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] MainImageRequest req)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -417,7 +431,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
[HttpPost("/club/{clubId:long}/additionalimage/{slot:int}")]
|
||||
[HttpPut("/club/{clubId:long}/additionalimage/{slot:int}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> ClubAdditionalImage(long clubId, int slot, [FromBody] MainImageRequest req)
|
||||
public async Task<IActionResult> ClubAdditionalImage(long clubId, int slot, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] MainImageRequest req)
|
||||
{
|
||||
var club = await clubs.GetByIdAsync(clubId);
|
||||
if (club is null) return NotFound();
|
||||
@@ -450,10 +464,15 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
{
|
||||
var club = await clubs.GetByIdAsync(clubId);
|
||||
if (club is null) return NotFound();
|
||||
// Joinability enum, confirmed from the 2023 dump (enum FEHIHCMDOLN):
|
||||
// Open = 0, InviteOnly = 1, AskToJoin = 2.
|
||||
// This used to treat 1 as request-to-join and 2 as invite-only, i.e.
|
||||
// exactly backwards: "Ask to join" clubs rejected every request while
|
||||
// invite-only clubs quietly accepted them.
|
||||
var perms = club.Joinability switch
|
||||
{
|
||||
0 => 0, // Open: instant member
|
||||
1 => 128, // RequestToJoin: pending
|
||||
2 => 128, // AskToJoin: pending approval
|
||||
_ => -1, // InviteOnly: rejected
|
||||
};
|
||||
if (perms == -1) return Forbid();
|
||||
@@ -503,7 +522,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
[HttpPost("/club/{clubId:long}/members/invite")]
|
||||
[HttpPut("/club/{clubId:long}/members/invite")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> MemberInvite(long clubId, [FromBody] MemberInviteRequest req)
|
||||
public async Task<IActionResult> MemberInvite(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] MemberInviteRequest req)
|
||||
{
|
||||
if (!await clubs.CanManageAsync(clubId, Me)) return Forbid();
|
||||
var target = req.PlayerId ?? req.AccountId ?? 0;
|
||||
@@ -524,7 +543,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
|
||||
[HttpPost("/club/{clubId:long}/members/invitemembers")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> MemberInviteMembers(long clubId, [FromBody] InviteMembersRequest req)
|
||||
public async Task<IActionResult> MemberInviteMembers(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] InviteMembersRequest req)
|
||||
{
|
||||
if (!await clubs.CanManageAsync(clubId, Me)) return Forbid();
|
||||
var ids = (req.PlayerIds ?? req.AccountIds ?? new List<int>()).Distinct().ToList();
|
||||
@@ -568,7 +587,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
/// target's row from Pending to Member.</summary>
|
||||
[HttpPost("/club/{clubId:long}/members/acceptrequest")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> MemberAcceptRequest(long clubId, [FromBody] MemberTargetRequest req)
|
||||
public async Task<IActionResult> MemberAcceptRequest(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] MemberTargetRequest req)
|
||||
{
|
||||
if (!await clubs.CanManageAsync(clubId, Me)) return Forbid();
|
||||
var target = req.PlayerId ?? req.AccountId ?? 0;
|
||||
@@ -588,7 +607,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
|
||||
[HttpPost("/club/{clubId:long}/members/acceptrequests")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> MemberAcceptRequests(long clubId, [FromBody] BulkTargetRequest req)
|
||||
public async Task<IActionResult> MemberAcceptRequests(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] BulkTargetRequest req)
|
||||
{
|
||||
if (!await clubs.CanManageAsync(clubId, Me)) return Forbid();
|
||||
var ids = (req.PlayerIds ?? req.AccountIds ?? new List<int>()).Distinct().ToList();
|
||||
@@ -603,7 +622,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
|
||||
[HttpPost("/club/{clubId:long}/members/denyrequest")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> MemberDenyRequest(long clubId, [FromBody] MemberTargetRequest req)
|
||||
public async Task<IActionResult> MemberDenyRequest(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] MemberTargetRequest req)
|
||||
{
|
||||
if (!await clubs.CanManageAsync(clubId, Me)) return Forbid();
|
||||
var target = req.PlayerId ?? req.AccountId ?? 0;
|
||||
@@ -614,7 +633,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
|
||||
[HttpPost("/club/{clubId:long}/members/denyrequests")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> MemberDenyRequests(long clubId, [FromBody] BulkTargetRequest req)
|
||||
public async Task<IActionResult> MemberDenyRequests(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] BulkTargetRequest req)
|
||||
{
|
||||
if (!await clubs.CanManageAsync(clubId, Me)) return Forbid();
|
||||
var ids = (req.PlayerIds ?? req.AccountIds ?? new List<int>()).Distinct().ToList();
|
||||
@@ -625,7 +644,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
|
||||
[HttpPost("/club/{clubId:long}/members/remove")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> MemberRemove(long clubId, [FromBody] MemberTargetRequest req)
|
||||
public async Task<IActionResult> MemberRemove(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] MemberTargetRequest req)
|
||||
{
|
||||
var club = await clubs.GetByIdAsync(clubId);
|
||||
if (club is null) return NotFound();
|
||||
@@ -643,7 +662,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
/// request-to-join flow can refuse them later.</summary>
|
||||
[HttpPost("/club/{clubId:long}/members/ban")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> MemberBan(long clubId, [FromBody] MemberTargetRequest req)
|
||||
public async Task<IActionResult> MemberBan(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] MemberTargetRequest req)
|
||||
{
|
||||
var club = await clubs.GetByIdAsync(clubId);
|
||||
if (club is null) return NotFound();
|
||||
@@ -658,7 +677,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
|
||||
[HttpPost("/club/{clubId:long}/members/unban")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> MemberUnban(long clubId, [FromBody] MemberTargetRequest req)
|
||||
public async Task<IActionResult> MemberUnban(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] MemberTargetRequest req)
|
||||
{
|
||||
if (!await clubs.CanManageAsync(clubId, Me)) return Forbid();
|
||||
var target = req.PlayerId ?? req.AccountId ?? 0;
|
||||
@@ -674,7 +693,7 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
[HttpPost("/club/{clubId:long}/members/changetype")]
|
||||
[HttpPut("/club/{clubId:long}/members/changetype")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> MemberChangeType(long clubId, [FromBody] MemberTargetRequest req)
|
||||
public async Task<IActionResult> MemberChangeType(long clubId, [ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] MemberTargetRequest req)
|
||||
{
|
||||
var club = await clubs.GetByIdAsync(clubId);
|
||||
if (club is null) return NotFound();
|
||||
@@ -759,7 +778,6 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
/// double-fires are safe.</summary>
|
||||
[HttpDelete("/announcements/club/{clubId:long}/{announcementId:long}")]
|
||||
[HttpPost("/announcements/club/{clubId:long}/{announcementId:long}")]
|
||||
[HttpPut("/announcements/club/{clubId:long}/{announcementId:long}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> AnnouncementDelete(long clubId, long announcementId)
|
||||
{
|
||||
@@ -767,6 +785,72 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>PUT <c>/announcements/club/{clubId}/{announcementId}</c> — EDIT
|
||||
/// an announcement.
|
||||
///
|
||||
/// PUT used to be a third binding on <see cref="AnnouncementDelete"/>, so
|
||||
/// editing an announcement silently DELETED it — the edit form's save
|
||||
/// button destroyed the post it was editing. It is its own handler now.</summary>
|
||||
[HttpPut("/announcements/club/{clubId:long}/{announcementId:long}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> AnnouncementEdit(long clubId, long announcementId)
|
||||
{
|
||||
var (title, body, imageName) = await ReadAnnouncementFieldsAsync();
|
||||
if (!await clubs.UpdateAnnouncementAsync(clubId, announcementId, Me, title, body, imageName))
|
||||
return Forbid();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>POST <c>/announcements/club/{clubId}</c> — CREATE an
|
||||
/// announcement. The client posts the fields to the COLLECTION url and
|
||||
/// reads the response as a bare Int64 (the new announcement id). No POST
|
||||
/// was registered at that template, so posting a club announcement 404'd.</summary>
|
||||
[HttpPost("/announcements/club/{clubId:long}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> AnnouncementCreate(long clubId)
|
||||
{
|
||||
var (title, body, imageName) = await ReadAnnouncementFieldsAsync();
|
||||
var id = await clubs.CreateAnnouncementAsync(
|
||||
clubId, Me, title ?? string.Empty, body ?? string.Empty, imageName ?? string.Empty);
|
||||
if (id is null) return Forbid();
|
||||
return Content(id.Value.ToString(), "application/json");
|
||||
}
|
||||
|
||||
/// <summary>Announcement fields as the client sends them: form-urlencoded,
|
||||
/// with a JSON body accepted as a fallback. Null means "not supplied", so
|
||||
/// an edit can leave a field alone.</summary>
|
||||
private async Task<(string? Title, string? Body, string? ImageName)> ReadAnnouncementFieldsAsync()
|
||||
{
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
string? F(params string[] keys)
|
||||
{
|
||||
foreach (var k in keys)
|
||||
if (form.TryGetValue(k, out var v) && v.Count > 0) return v.ToString();
|
||||
return null;
|
||||
}
|
||||
return (F("title", "Title"), F("body", "Body"), F("imageName", "ImageName"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Request.EnableBuffering();
|
||||
Request.Body.Position = 0;
|
||||
using var doc = await System.Text.Json.JsonDocument.ParseAsync(Request.Body);
|
||||
Request.Body.Position = 0;
|
||||
string? P(params string[] keys)
|
||||
{
|
||||
foreach (var k in keys)
|
||||
if (doc.RootElement.TryGetProperty(k, out var v) &&
|
||||
v.ValueKind == System.Text.Json.JsonValueKind.String) return v.GetString();
|
||||
return null;
|
||||
}
|
||||
return (P("title", "Title"), P("body", "Body"), P("imageName", "ImageName"));
|
||||
}
|
||||
catch (System.Text.Json.JsonException) { return (null, null, null); }
|
||||
}
|
||||
|
||||
// ── Wire-shape mappers ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -98,12 +98,14 @@ public class EconController(DorkNetDbContext db, LevelService level) : Controlle
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>The client reads this body as a BARE integer — the issuing
|
||||
/// method is <c>FGLDKEJLAKB<System.Int32> KPOENLDFAJD()</c>
|
||||
/// (RecNet.Runtime/MPBLNLMCEDL.txt:2881, route literal on :2955).
|
||||
/// Returning a JSON object made the deserialize throw, so the ownership-cap
|
||||
/// check that runs before a custom-shirt purchase always errored out.</summary>
|
||||
[HttpGet("/econ/customAvatarItems/v1/itemOwnershipLimit")]
|
||||
public IActionResult CustomAvatarItemOwnershipLimit() => Ok(new
|
||||
{
|
||||
Limit = 1000,
|
||||
ItemOwnershipLimit = 1000,
|
||||
});
|
||||
public IActionResult CustomAvatarItemOwnershipLimit()
|
||||
=> Content("1000", "application/json");
|
||||
|
||||
private static object ToCustomAvatarItemWire(DorkNet.Server.Data.Entities.CustomAvatarItemEntity i) => new
|
||||
{
|
||||
|
||||
@@ -323,6 +323,10 @@ public class MatchController(
|
||||
/// PrivateInstance row. The watch only needs a RecNet result
|
||||
/// wrapper, but the server persists the privacy state so later
|
||||
/// invite joins are enforced by <see cref="PrivateInstanceService"/>.</summary>
|
||||
// The 2023-03-21 client sends PUT (verb constant 3 at
|
||||
// RecNet/Matchmaking.txt:15483 and :15774) with an empty body. POST-only
|
||||
// returned 405, so a host could never close a public instance to private.
|
||||
[HttpPut("/roominstance/{instanceId:long}/markprivate")]
|
||||
[HttpPost("/roominstance/{instanceId:long}/markprivate")]
|
||||
public async Task<IActionResult> MarkPrivate(long instanceId)
|
||||
{
|
||||
@@ -357,14 +361,51 @@ public class MatchController(
|
||||
return Ok(new { Success = true, Error = string.Empty });
|
||||
}
|
||||
|
||||
/// <summary>GET/POST <c>/roominstance/{id}/roomcode</c> — raw
|
||||
/// JSON string room code shown by the watch. Codes are stable for
|
||||
/// an instance id and do not expose the numeric id directly.</summary>
|
||||
/// <summary>GET/POST/PUT <c>/roominstance/{id}/roomcode</c> — the room code
|
||||
/// shown by the watch, as a raw JSON string.
|
||||
///
|
||||
/// GET returns the instance's code. The 2023-03-21 client also PUTs here to
|
||||
/// SET a custom code, sending form fields <c>roomCode</c> and
|
||||
/// <c>forceChange</c> (verb 3 at RecNet/Matchmaking.txt:16234). PUT was not
|
||||
/// registered, so custom room codes 405'd; and because the handler always
|
||||
/// regenerated the deterministic code, a submitted one would have been
|
||||
/// discarded anyway. A submitted code is now validated, claimed and
|
||||
/// persisted, and the accepted code is echoed back.
|
||||
///
|
||||
/// <c>forceChange</c> lets the host take a code already claimed by one of
|
||||
/// their own instances; it never steals another player's code.</summary>
|
||||
[HttpGet("/roominstance/{instanceId:long}/roomcode")]
|
||||
[HttpPost("/roominstance/{instanceId:long}/roomcode")]
|
||||
public IActionResult RoomCode(long instanceId)
|
||||
[HttpPut("/roominstance/{instanceId:long}/roomcode")]
|
||||
public async Task<IActionResult> RoomCode(
|
||||
long instanceId,
|
||||
[FromForm(Name = "roomCode")] string? roomCode,
|
||||
[FromForm(Name = "forceChange")] bool? forceChange)
|
||||
{
|
||||
var code = RoomCodeService.Generate(instanceId);
|
||||
var requested = (roomCode ?? Request.Query["roomCode"].FirstOrDefault())?.Trim();
|
||||
|
||||
if (!string.IsNullOrEmpty(requested))
|
||||
{
|
||||
var normalized = requested.ToUpperInvariant();
|
||||
var taken = await db.PrivateInstances
|
||||
.AnyAsync(p => p.RoomCode == normalized && p.Id != instanceId);
|
||||
if (taken && forceChange != true)
|
||||
return Content(JsonSerializer.Serialize(RoomCodeService.Generate(instanceId)), "application/json");
|
||||
|
||||
var row = await db.PrivateInstances.FirstOrDefaultAsync(p => p.Id == instanceId);
|
||||
if (row is not null)
|
||||
{
|
||||
row.RoomCode = normalized;
|
||||
await db.SaveChangesAsync();
|
||||
return Content(JsonSerializer.Serialize(normalized), "application/json");
|
||||
}
|
||||
}
|
||||
|
||||
var stored = await db.PrivateInstances
|
||||
.Where(p => p.Id == instanceId)
|
||||
.Select(p => p.RoomCode)
|
||||
.FirstOrDefaultAsync();
|
||||
var code = string.IsNullOrEmpty(stored) ? RoomCodeService.Generate(instanceId) : stored;
|
||||
return Content(JsonSerializer.Serialize(code), "application/json");
|
||||
}
|
||||
|
||||
|
||||
@@ -112,12 +112,17 @@ public class MatchPlayerController(
|
||||
/// </summary>
|
||||
[HttpPost("/player/notifydisconnect")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
||||
public IActionResult NotifyDisconnect([FromForm(Name = "otherPlayerId")] int? otherPlayerId)
|
||||
public IActionResult NotifyDisconnect(
|
||||
[FromForm(Name = "otherPlayerId")] int? otherPlayerId,
|
||||
// The 2023-03-21 client sends PascalCase PlayerId + RoomInstanceId
|
||||
// instead of 2020's otherPlayerId, so the legacy binding logged null.
|
||||
[FromForm(Name = "PlayerId")] long? playerId,
|
||||
[FromForm(Name = "RoomInstanceId")] long? roomInstanceId)
|
||||
{
|
||||
var caller = TryGetCurrentPlayerId();
|
||||
log.LogInformation(
|
||||
"[player-notify-disconnect] caller={Caller} reported other={Other} left",
|
||||
caller, otherPlayerId);
|
||||
"[player-notify-disconnect] caller={Caller} reported other={Other} instance={Instance} left",
|
||||
caller, otherPlayerId ?? (int?)playerId, roomInstanceId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -158,9 +163,12 @@ public class MatchPlayerController(
|
||||
return Ok(new PresenceDto
|
||||
{
|
||||
PlayerId = playerId,
|
||||
StatusVisibility = 0, // Everyone (was 1 = FriendsOnly)
|
||||
// Defaults: 0 = Everyone. Both come from the player's stored
|
||||
// preference so a value set via /player/statusvisibility or
|
||||
// /player/vrmovementmode survives the next heartbeat.
|
||||
StatusVisibility = await GetPlayerPrefIntAsync(playerId, PrefStatusVisibility, 0),
|
||||
DeviceClass = 0,
|
||||
VrMovementMode = 0,
|
||||
VrMovementMode = await GetPlayerPrefIntAsync(playerId, PrefVrMovementMode, 0),
|
||||
RoomInstance = room,
|
||||
});
|
||||
}
|
||||
@@ -170,13 +178,14 @@ public class MatchPlayerController(
|
||||
public async Task<ActionResult<PresenceDto>> SetStatusVisibility([FromForm(Name = "statusVisibility")] int? value)
|
||||
{
|
||||
var playerId = TryGetCurrentPlayerId();
|
||||
if (value is int v) await SetPlayerPrefAsync(playerId, PrefStatusVisibility, v.ToString());
|
||||
var room = await GetPresenceRoomForResponseAsync(playerId);
|
||||
return Ok(new PresenceDto
|
||||
{
|
||||
PlayerId = playerId,
|
||||
StatusVisibility = value ?? 0, // default Everyone; honor explicit client choice
|
||||
StatusVisibility = value ?? await GetPlayerPrefIntAsync(playerId, PrefStatusVisibility, 0),
|
||||
DeviceClass = 0,
|
||||
VrMovementMode = 0,
|
||||
VrMovementMode = await GetPlayerPrefIntAsync(playerId, PrefVrMovementMode, 0),
|
||||
RoomInstance = room,
|
||||
});
|
||||
}
|
||||
@@ -186,17 +195,58 @@ public class MatchPlayerController(
|
||||
public async Task<ActionResult<PresenceDto>> SetVrMovementMode([FromForm(Name = "vrMovementMode")] int? value)
|
||||
{
|
||||
var playerId = TryGetCurrentPlayerId();
|
||||
if (value is int v) await SetPlayerPrefAsync(playerId, PrefVrMovementMode, v.ToString());
|
||||
var room = await GetPresenceRoomForResponseAsync(playerId);
|
||||
return Ok(new PresenceDto
|
||||
{
|
||||
PlayerId = playerId,
|
||||
StatusVisibility = 0, // Everyone (was 1 = FriendsOnly)
|
||||
StatusVisibility = await GetPlayerPrefIntAsync(playerId, PrefStatusVisibility, 0),
|
||||
DeviceClass = 0,
|
||||
VrMovementMode = value ?? 0,
|
||||
VrMovementMode = value ?? await GetPlayerPrefIntAsync(playerId, PrefVrMovementMode, 0),
|
||||
RoomInstance = room,
|
||||
});
|
||||
}
|
||||
|
||||
// ── per-player matchmaking preferences ───────────────────────────
|
||||
// These three were previously echo-only: the client's chosen value came
|
||||
// back in the response and was then dropped, so the setting reverted on the
|
||||
// next heartbeat and never survived a restart. They live in PlayerSettings
|
||||
// (no schema change needed) and are read back wherever presence is built.
|
||||
private const string PrefStatusVisibility = "pref:statusVisibility";
|
||||
private const string PrefVrMovementMode = "pref:vrMovementMode";
|
||||
private const string PrefAvoidJuniors = "pref:avoidJuniors";
|
||||
|
||||
private async Task SetPlayerPrefAsync(long playerId, string key, string value)
|
||||
{
|
||||
if (playerId == 0) return;
|
||||
var row = await db.PlayerSettings
|
||||
.FirstOrDefaultAsync(s => s.PlayerId == playerId && s.Key == key);
|
||||
if (row is null)
|
||||
{
|
||||
db.PlayerSettings.Add(new DorkNet.Server.Data.Entities.PlayerSettingEntity
|
||||
{
|
||||
PlayerId = playerId, Key = key, Value = value,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
row.Value = value;
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task<string?> GetPlayerPrefAsync(long playerId, string key)
|
||||
{
|
||||
if (playerId == 0) return null;
|
||||
return await db.PlayerSettings
|
||||
.Where(s => s.PlayerId == playerId && s.Key == key)
|
||||
.Select(s => s.Value)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
private async Task<int> GetPlayerPrefIntAsync(long playerId, string key, int fallback)
|
||||
=> int.TryParse(await GetPlayerPrefAsync(playerId, key), out var v) ? v : fallback;
|
||||
|
||||
/// <summary>
|
||||
/// GET <c>/player/avoidjuniors</c> — returns a primitive boolean.
|
||||
/// Verified against
|
||||
@@ -209,18 +259,28 @@ public class MatchPlayerController(
|
||||
/// matchmaking init in a never-completes state on some boot flows.
|
||||
/// </summary>
|
||||
[HttpGet("/player/avoidjuniors")]
|
||||
public ActionResult<bool> GetAvoidJuniors() => Ok(false);
|
||||
public async Task<ActionResult<bool>> GetAvoidJuniors()
|
||||
{
|
||||
var playerId = TryGetCurrentPlayerId();
|
||||
var stored = await GetPlayerPrefAsync(playerId, PrefAvoidJuniors);
|
||||
return Ok(bool.TryParse(stored, out var v) && v);
|
||||
}
|
||||
|
||||
/// <summary>PUT <c>/player/avoidjuniors</c> — body
|
||||
/// <c>avoidJuniors=true|false</c> (Matchmaking.txt:7883). Stub for
|
||||
/// now; the multi-tenant server doesn't currently track this
|
||||
/// preference per-player. Returns the new value as a JSON
|
||||
/// <c>avoidJuniors=true|false</c> (Matchmaking.txt:7883). Persisted
|
||||
/// per-player in PlayerSettings and read back by
|
||||
/// <see cref="GetAvoidJuniors"/>. Returns the new value as a JSON
|
||||
/// primitive so the watch's ExpectPrimitiveResponse is happy.</summary>
|
||||
[HttpPut("/player/avoidjuniors")]
|
||||
[HttpPost("/player/avoidjuniors")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
||||
public ActionResult<bool> SetAvoidJuniors([FromForm(Name = "avoidJuniors")] bool? value)
|
||||
=> Ok(value ?? false);
|
||||
public async Task<ActionResult<bool>> SetAvoidJuniors([FromForm(Name = "avoidJuniors")] bool? value)
|
||||
{
|
||||
var playerId = TryGetCurrentPlayerId();
|
||||
if (value is bool v)
|
||||
await SetPlayerPrefAsync(playerId, PrefAvoidJuniors, v ? "true" : "false");
|
||||
return Ok(value ?? false);
|
||||
}
|
||||
|
||||
/// <summary>POST <c>/player/photonregionpings</c> — watch reports
|
||||
/// its measured Photon region ping vector so the server can pick
|
||||
@@ -260,6 +320,27 @@ public class MatchPlayerController(
|
||||
// Sanity bound — discard wild values from misbehaving clients.
|
||||
pairs[r] = Math.Clamp(p, 0, 2000);
|
||||
}
|
||||
|
||||
// The 2023-03-21 client does NOT use the repeated (region, ping) pair
|
||||
// shape above. It sends one field per measured region, keyed by the
|
||||
// region name with the ping as the value (us=42&eu=90&...). Against the
|
||||
// pair parser both arrays came back empty, so no 2023 client ever
|
||||
// registered a ping and region selection fell back to the default.
|
||||
if (pairs.Count == 0)
|
||||
{
|
||||
var fields = Request.HasFormContentType
|
||||
? (IEnumerable<KeyValuePair<string, Microsoft.Extensions.Primitives.StringValues>>)Request.Form
|
||||
: Request.Query;
|
||||
foreach (var (key, value) in fields)
|
||||
{
|
||||
var name = key?.Trim();
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
if (name.Equals("region", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Equals("ping", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (!int.TryParse(value.FirstOrDefault(), out var p)) continue;
|
||||
pairs[name] = Math.Clamp(p, 0, 2000);
|
||||
}
|
||||
}
|
||||
if (pairs.Count > 0)
|
||||
presence.SetPhotonRegionPings(playerId, pairs);
|
||||
log.LogInformation("[player-region-pings] player={Player} regions={Count}", playerId, pairs.Count);
|
||||
|
||||
@@ -56,6 +56,25 @@ public class PlaylistEntity
|
||||
/// <summary>Total joins across all member rooms.</summary>
|
||||
public int VisitCount { get; set; } = 0;
|
||||
|
||||
/// <summary>Playlist accessibility enum as the client sends it
|
||||
/// (0 = private, 1 = public). Previously the accessibility/restrictions/
|
||||
/// level-voting/warning mutations were acknowledged and thrown away, so a
|
||||
/// creator could never actually publish a playlist or change its settings —
|
||||
/// the wire always reported the hardcoded defaults back.</summary>
|
||||
public int Accessibility { get; set; } = 0;
|
||||
|
||||
public bool SupportsLevelVoting { get; set; } = false;
|
||||
public bool SupportsJuniors { get; set; } = true;
|
||||
public bool SupportsScreens { get; set; } = true;
|
||||
public bool SupportsTeleportVR { get; set; } = true;
|
||||
public bool SupportsWalkVR { get; set; } = true;
|
||||
|
||||
/// <summary>Content-warning bitmask, plus the creator's free-text warning.</summary>
|
||||
public int WarningMask { get; set; } = 0;
|
||||
|
||||
[MaxLength(512)]
|
||||
public string CustomWarning { get; set; } = string.Empty;
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -38,5 +38,11 @@ public class PrivateInstanceEntity
|
||||
public int MaxCapacity { get; set; } = 8;
|
||||
[MaxLength(128)] public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Host-chosen room code, set via PUT
|
||||
/// <c>/roominstance/{id}/roomcode</c>. Empty means "no custom code", in
|
||||
/// which case the deterministic <c>RoomCodeService.Generate</c> value is
|
||||
/// served instead.</summary>
|
||||
[MaxLength(16)] public string RoomCode { get; set; } = string.Empty;
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3572 @@
|
||||
// <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("20260727200252_Client2023WireFixes")]
|
||||
partial class Client2023WireFixes
|
||||
{
|
||||
/// <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<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.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<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<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.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>("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>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
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<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.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>("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<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,132 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DorkNet.Server.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Columns needed by the March-2023 client wire fixes: the playlist
|
||||
/// settings the client can set (accessibility, level voting, VR/junior
|
||||
/// restrictions, content warning) which used to be acknowledged and thrown
|
||||
/// away, plus a host-chosen room code on private instances.
|
||||
///
|
||||
/// Hand-trimmed after generation: `migrations add` diffs against the
|
||||
/// committed model snapshot, which already lagged the entity model, so the
|
||||
/// generated file also carried unrelated pre-existing drift
|
||||
/// (ServerSettings, Rooms, Avatars, ...). Replaying that against a database
|
||||
/// where those columns already exist fails on the first duplicate
|
||||
/// ADD COLUMN, so only the columns this change introduces are kept.
|
||||
/// </summary>
|
||||
public partial class Client2023WireFixes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "RoomCode",
|
||||
table: "PrivateInstances",
|
||||
type: "TEXT",
|
||||
maxLength: 16,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Accessibility",
|
||||
table: "Playlists",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CustomWarning",
|
||||
table: "Playlists",
|
||||
type: "TEXT",
|
||||
maxLength: 512,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "SupportsJuniors",
|
||||
table: "Playlists",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "SupportsLevelVoting",
|
||||
table: "Playlists",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "SupportsScreens",
|
||||
table: "Playlists",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "SupportsTeleportVR",
|
||||
table: "Playlists",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "SupportsWalkVR",
|
||||
table: "Playlists",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "WarningMask",
|
||||
table: "Playlists",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RoomCode",
|
||||
table: "PrivateInstances");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Accessibility",
|
||||
table: "Playlists");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CustomWarning",
|
||||
table: "Playlists");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SupportsJuniors",
|
||||
table: "Playlists");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SupportsLevelVoting",
|
||||
table: "Playlists");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SupportsScreens",
|
||||
table: "Playlists");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SupportsTeleportVR",
|
||||
table: "Playlists");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SupportsWalkVR",
|
||||
table: "Playlists");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "WarningMask",
|
||||
table: "Playlists");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,10 @@ namespace DorkNet.Server.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CustomAvatarItemsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EquippedItemsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
@@ -87,6 +91,11 @@ namespace DorkNet.Server.Migrations
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OutfitSelectionsV2")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -214,6 +223,34 @@ namespace DorkNet.Server.Migrations
|
||||
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")
|
||||
@@ -732,6 +769,109 @@ namespace DorkNet.Server.Migrations
|
||||
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")
|
||||
@@ -766,11 +906,23 @@ namespace DorkNet.Server.Migrations
|
||||
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");
|
||||
|
||||
@@ -920,6 +1072,9 @@ namespace DorkNet.Server.Migrations
|
||||
b.Property<long>("RecipientPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("SourceStoreItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SupportsCurrentPlatform")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -998,6 +1153,9 @@ namespace DorkNet.Server.Migrations
|
||||
b.Property<int>("Permission")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Price")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ReplicationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
@@ -1094,8 +1252,85 @@ namespace DorkNet.Server.Migrations
|
||||
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");
|
||||
|
||||
@@ -1109,9 +1344,6 @@ namespace DorkNet.Server.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -1312,6 +1544,37 @@ namespace DorkNet.Server.Migrations
|
||||
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")
|
||||
@@ -1718,6 +1981,9 @@ namespace DorkNet.Server.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Accessibility")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CheerCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1727,6 +1993,11 @@ namespace DorkNet.Server.Migrations
|
||||
b.Property<long>("CreatorPlayerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CustomWarning")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
@@ -1752,6 +2023,21 @@ namespace DorkNet.Server.Migrations
|
||||
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)
|
||||
@@ -1766,6 +2052,9 @@ namespace DorkNet.Server.Migrations
|
||||
b.Property<int>("VisitorCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("WarningMask")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorPlayerId");
|
||||
@@ -1873,6 +2162,11 @@ namespace DorkNet.Server.Migrations
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoomCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RoomId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -2105,6 +2399,221 @@ namespace DorkNet.Server.Migrations
|
||||
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")
|
||||
@@ -2155,6 +2664,9 @@ namespace DorkNet.Server.Migrations
|
||||
b.Property<int>("Accessibility")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowNewUsers")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowsJuniors")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -2208,23 +2720,58 @@ namespace DorkNet.Server.Migrations
|
||||
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");
|
||||
|
||||
@@ -2421,6 +2968,19 @@ namespace DorkNet.Server.Migrations
|
||||
.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");
|
||||
@@ -2585,6 +3145,44 @@ namespace DorkNet.Server.Migrations
|
||||
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>("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");
|
||||
|
||||
@@ -2613,6 +3211,43 @@ namespace DorkNet.Server.Migrations
|
||||
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")
|
||||
@@ -2821,6 +3456,72 @@ namespace DorkNet.Server.Migrations
|
||||
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")
|
||||
|
||||
@@ -522,6 +522,50 @@ public class ClubService(DorkNetDbContext db)
|
||||
public Task<ClubAnnouncementEntity?> AnnouncementAsync(long clubId, long announcementId) =>
|
||||
db.ClubAnnouncements.FirstOrDefaultAsync(a => a.Id == announcementId && a.ClubId == clubId);
|
||||
|
||||
/// <summary>Create a club announcement. Returns the new row's id, or null
|
||||
/// when the caller may not manage the club.</summary>
|
||||
public async Task<long?> CreateAnnouncementAsync(
|
||||
long clubId, long callerId, string title, string body, string imageName)
|
||||
{
|
||||
if (!await CanManageAsync(clubId, callerId)) return null;
|
||||
var row = new ClubAnnouncementEntity
|
||||
{
|
||||
ClubId = clubId,
|
||||
AuthorPlayerId = callerId,
|
||||
Title = Truncate(title, 200),
|
||||
Body = Truncate(body, 4000),
|
||||
ImageName = Truncate(imageName, 256),
|
||||
};
|
||||
db.ClubAnnouncements.Add(row);
|
||||
await db.SaveChangesAsync();
|
||||
return row.Id;
|
||||
}
|
||||
|
||||
/// <summary>Edit an existing announcement in place. Null arguments leave
|
||||
/// the corresponding field untouched. Returns false when the caller may
|
||||
/// not manage the club or the row is unknown.</summary>
|
||||
public async Task<bool> UpdateAnnouncementAsync(
|
||||
long clubId, long announcementId, long callerId,
|
||||
string? title, string? body, string? imageName)
|
||||
{
|
||||
if (!await CanManageAsync(clubId, callerId)) return false;
|
||||
var row = await db.ClubAnnouncements
|
||||
.FirstOrDefaultAsync(a => a.Id == announcementId && a.ClubId == clubId);
|
||||
if (row is null) return false;
|
||||
if (title is not null) row.Title = Truncate(title, 200);
|
||||
if (body is not null) row.Body = Truncate(body, 4000);
|
||||
if (imageName is not null) row.ImageName = Truncate(imageName, 256);
|
||||
row.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string Truncate(string? value, int max)
|
||||
{
|
||||
value = (value ?? string.Empty).Trim();
|
||||
return value.Length <= max ? value : value[..max];
|
||||
}
|
||||
|
||||
/// <summary>Delete an announcement after verifying the caller can
|
||||
/// manage the club. Idempotent: missing row → no-op.</summary>
|
||||
public async Task<bool> DeleteAnnouncementAsync(long clubId, long announcementId, long callerId)
|
||||
|
||||
@@ -86,31 +86,41 @@ public class NotificationService(
|
||||
/// concrete example: <c>GiftPackageReceived</c> registers
|
||||
/// <c>"30"</c> and <c>GiftPackageReceivedImmediate</c>
|
||||
/// registers <c>"31"</c>; sending the enum name misses.</item>
|
||||
/// </list></summary>
|
||||
/// </list>
|
||||
///
|
||||
/// <para><b>The default is numeric, and that matters.</b> Both enum
|
||||
/// overloads of the client's <c>RegisterHandler</c>
|
||||
/// (<c>OMHPBIEHOHN.PNDNMHHDOOJ</c>) call <c>Int32.ToString()</c> on the id
|
||||
/// and register ONLY that numeric string —
|
||||
/// <c>RecNet.Runtime/OMHPBIEHOHN.txt:662</c> and <c>:733</c>. There is no
|
||||
/// enum-name companion key. The dispatcher does a plain case-sensitive
|
||||
/// <c>Dictionary.TryGetValue</c> on the envelope's <c>Id</c> and returns
|
||||
/// silently on a miss (<c>OMHPBIEHOHN.txt:4052-4237</c>), so a wrong key is
|
||||
/// not an error anywhere — the push simply never arrives.</para>
|
||||
///
|
||||
/// <para>This used to default to <c>id.ToString()</c> (the enum NAME), which
|
||||
/// silently dropped every enum-registered push that wasn't explicitly listed:
|
||||
/// friend-request add/accept/remove and the RelationshipsInvalid refresh,
|
||||
/// forced logout, maintenance broadcasts, live token-balance updates after a
|
||||
/// level-up or purchase, consumable inventory changes, and invention
|
||||
/// moderation state. Only the six ids below are genuinely string-keyed;
|
||||
/// everything else must go out as a number. Verify against the binary before
|
||||
/// adding an entry here — guessing wrong fails silently in both directions.</para>
|
||||
/// </summary>
|
||||
private static string WireName(PushNotificationId id) => id switch
|
||||
{
|
||||
// Custom short names — registered under a bespoke string,
|
||||
// no integer companion in the dispatch dict.
|
||||
// The ONLY ids registered under a bespoke string. Everything else is
|
||||
// numeric — see the note below for why the default flipped.
|
||||
PushNotificationId.SubscriptionUpdateProfile => "AccountUpdate",
|
||||
PushNotificationId.SubscriptionUpdatePresence => "PresenceUpdate",
|
||||
PushNotificationId.SubscriptionUpdateGameSession => "RoomInstanceUpdate",
|
||||
PushNotificationId.SubscriptionUpdateProfile => "AccountUpdate",
|
||||
// Int-only registrations — sending the enum name misses.
|
||||
PushNotificationId.MessageReceived => "2",
|
||||
PushNotificationId.MessageDeleted => "3",
|
||||
PushNotificationId.ModerationKick => "22",
|
||||
PushNotificationId.ModerationKickAttemptFailed => "23",
|
||||
PushNotificationId.ModerationRoomBan => "24",
|
||||
PushNotificationId.GiftPackageReceived => "30",
|
||||
PushNotificationId.GiftPackageReceivedImmediate => "31",
|
||||
PushNotificationId.PlayerEventCreated => "80",
|
||||
PushNotificationId.PlayerEventUpdated => "81",
|
||||
PushNotificationId.PlayerEventDeleted => "82",
|
||||
PushNotificationId.PlayerEventResponseChanged => "83",
|
||||
PushNotificationId.PlayerEventResponseDeleted => "84",
|
||||
PushNotificationId.PlayerEventStateChanged => "85",
|
||||
// Everything else: enum.ToString() matches because the watch's
|
||||
// enum-overload RegisterHandler registers both forms.
|
||||
_ => id.ToString(),
|
||||
PushNotificationId.SubscriptionUpdateRoom => "RoomUpdate",
|
||||
PushNotificationId.ChatMessageReceived => "ChatMessageReceived",
|
||||
PushNotificationId.CommunityBoardUpdate => "CommunityBoardUpdate",
|
||||
|
||||
// Numeric, because the client registered these through the enum
|
||||
// overload. This is the default for a reason — see below.
|
||||
_ => ((int)id).ToString(),
|
||||
};
|
||||
|
||||
private static string Serialize(PushNotificationId id, object? msg) =>
|
||||
|
||||
Reference in New Issue
Block a user