yikes,
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Vanadium.Controllers;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
|
||||
namespace Vanadium.Classes
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks which clients are running the Bluetint patch, and keeps the ones that are not
|
||||
/// out of the game.
|
||||
/// <para>
|
||||
/// The awkward fact this is built around: the patch announces itself by calling
|
||||
/// /api/anticheat/hwid, and that call needs a bearer token, so it can only happen AFTER
|
||||
/// a successful login. A login gate that simply demanded a prior check-in would refuse
|
||||
/// every first login ever made, patched clients included, and no amount of installing
|
||||
/// the patch would fix it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// So enforcement runs in two steps that between them cannot deadlock. A session that
|
||||
/// goes quiet past the grace window is kicked and the account is flagged. The flag denies
|
||||
/// exactly one login and is then cleared, so the next attempt always gets a fresh grace
|
||||
/// session. An unpatched client therefore alternates between a refusal and a kick and
|
||||
/// never gets to play, while a client that has since installed the patch is refused at
|
||||
/// most once and then checks in normally. Nobody can be locked out permanently by this.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Worth saying plainly: this stops people running an unpatched client. It does not stop
|
||||
/// someone who reverse-engineers the check-in call and replays it, because no check that
|
||||
/// runs on the player machine ever can. It raises the floor; it is not a wall.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class Bluetint
|
||||
{
|
||||
private static readonly ConcurrentDictionary<long, DateTime> _lastHwid = new();
|
||||
private static readonly ConcurrentDictionary<long, DateTime> _firstSeen = new();
|
||||
private static readonly ConcurrentDictionary<long, byte> _loggedUnprotected = new();
|
||||
|
||||
/// <summary>Accounts whose last session never checked in. Denies one login, then clears.</summary>
|
||||
private static readonly ConcurrentDictionary<long, DateTime> _unpatched = new();
|
||||
|
||||
/// <summary>How stale a check-in may be before the client counts as gone.</summary>
|
||||
private static readonly TimeSpan ProtectionWindow = TimeSpan.FromMinutes(5);
|
||||
|
||||
public const string DefaultMessage = "No skidding. Download the Bluetint patch";
|
||||
|
||||
public static void MarkProtected(long playerId)
|
||||
{
|
||||
_lastHwid[playerId] = DateTime.UtcNow;
|
||||
_loggedUnprotected.TryRemove(playerId, out _);
|
||||
|
||||
// The patch is here now, so whatever the last session did is no longer relevant.
|
||||
_unpatched.TryRemove(playerId, out _);
|
||||
}
|
||||
|
||||
/// <summary>True while this account has a recent enough check-in from the patch.</summary>
|
||||
public static bool IsProtected(long playerId)
|
||||
{
|
||||
return _lastHwid.TryGetValue(playerId, out var t)
|
||||
&& (DateTime.UtcNow - t) <= ProtectionWindow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the patch has announced itself at any point since this player came
|
||||
/// online. Deliberately weaker than <see cref="IsProtected"/>, and the default,
|
||||
/// because it does not assume the client re-submits on a timer. If the shipped patch
|
||||
/// only calls in once at startup, a freshness rule would kick every honest player a
|
||||
/// few minutes into their session, and a mass false positive costs far more than the
|
||||
/// gap it closes.
|
||||
/// </summary>
|
||||
public static bool HasCheckedInThisSession(long playerId)
|
||||
{
|
||||
return _lastHwid.ContainsKey(playerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The login gate. Returns null to allow, or the message the client should show.
|
||||
/// Called from AuthController.TokenResponse, which every grant type funnels through.
|
||||
/// </summary>
|
||||
public static string? DenyLogin(long playerId, Microsoft.AspNetCore.Http.HttpRequest request)
|
||||
{
|
||||
if (!ServerConfig.RequirePatchToPlay)
|
||||
return null;
|
||||
|
||||
// Developers are exempt, the same way they are exempt from DevsOnlyToken and the
|
||||
// PC gate. Without this the first deploy of a broken check locks out the only
|
||||
// people who can turn it back off.
|
||||
var player = DBs.PlayerDB.Players.FindById(playerId);
|
||||
if (player?.PlayerRoles?.Contains(PlayerRoles.Developer) == true)
|
||||
return null;
|
||||
|
||||
// Oculus (Quest) players are exempt: the Bluetint patch is a PC MelonLoader mod
|
||||
// that cannot run on a standalone headset, so gating them on it would lock out
|
||||
// every Quest player. There is nothing for them to install, so nothing to check.
|
||||
if (IsPlatformExempt(player))
|
||||
return null;
|
||||
|
||||
// Strong gate: the patch proves itself in the login request. Only usable once the
|
||||
// client ships support for it, which is why it is off by default.
|
||||
if (ServerConfig.RequirePatchHeader)
|
||||
{
|
||||
if (!VerifyPatchHeader(request))
|
||||
{
|
||||
Console.WriteLine($"[Bluetint] Login refused (no patch header): player {playerId}");
|
||||
return Message();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Weak gate: deny on the evidence of the previous session, then clear the flag so
|
||||
// the next attempt gets a fresh chance to check in. This is the half that works
|
||||
// with the client as it ships today.
|
||||
if (_unpatched.TryRemove(playerId, out _))
|
||||
{
|
||||
Console.WriteLine($"[Bluetint] Login refused (last session never checked in): player {playerId}");
|
||||
return Message();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Configured refusal text, falling back to the built-in default.</summary>
|
||||
private static string Message()
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(ServerConfig.PatchRequiredMessage)
|
||||
? DefaultMessage
|
||||
: ServerConfig.PatchRequiredMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the account must never be gated on the Bluetint patch because of the
|
||||
/// platform it plays on. Oculus (Quest) is standalone and cannot load a PC
|
||||
/// MelonLoader mod, so there is nothing for those players to install or check.
|
||||
/// </summary>
|
||||
private static bool IsPlatformExempt(FullPlayer player)
|
||||
{
|
||||
return player?.PlatformIds != null
|
||||
&& player.PlatformIds.Any(p => p.Platform == Platforms.Oculus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies X-Bluetint-Patch: an HMAC of the current minute under a shared secret.
|
||||
/// <para>
|
||||
/// Time-bucketed so a header captured off the wire stops working within a couple of
|
||||
/// minutes rather than forever. The secret still lives inside a DLL on the player
|
||||
/// machine and can be pulled out of it - this raises the cost of copying a login
|
||||
/// request, nothing more.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static bool VerifyPatchHeader(Microsoft.AspNetCore.Http.HttpRequest request)
|
||||
{
|
||||
string? provided = request.Headers["X-Bluetint-Patch"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(provided))
|
||||
return false;
|
||||
|
||||
string secret = ServerConfig.PatchHeaderSecret;
|
||||
if (string.IsNullOrWhiteSpace(secret))
|
||||
{
|
||||
// Refusing everyone because an operator forgot to set a secret would take the
|
||||
// whole game offline, so this fails open and says so.
|
||||
Console.WriteLine("[Bluetint] RequirePatchHeader is on but PatchHeaderSecret is empty; allowing login");
|
||||
return true;
|
||||
}
|
||||
|
||||
long minute = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 60;
|
||||
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
|
||||
|
||||
foreach (long bucket in new[] { minute, minute - 1, minute + 1 })
|
||||
{
|
||||
string expected = Convert.ToBase64String(
|
||||
hmac.ComputeHash(Encoding.UTF8.GetBytes(bucket.ToString())));
|
||||
|
||||
if (CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(provided)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void StartSweep()
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Sweep();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// The sweep must never be the thing that takes the server down.
|
||||
Console.WriteLine($"[Bluetint] Sweep failed: {ex.Message}");
|
||||
}
|
||||
|
||||
await Task.Delay(30000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kicks online players whose patch never checked in, and flags them so their next
|
||||
/// login is refused with the message.
|
||||
/// </summary>
|
||||
private static async Task Sweep()
|
||||
{
|
||||
var online = new HashSet<long>(NotificationsController.PlayerConnections.Keys);
|
||||
|
||||
// Forget people who went offline, so a player who quits and comes back gets a
|
||||
// fresh grace window instead of inheriting a stale clock.
|
||||
foreach (var id in _firstSeen.Keys.ToList())
|
||||
{
|
||||
if (!online.Contains(id))
|
||||
{
|
||||
_firstSeen.TryRemove(id, out _);
|
||||
_loggedUnprotected.TryRemove(id, out _);
|
||||
|
||||
// Drop the check-in too, so "has checked in" means "this session" and
|
||||
// a reconnect has to prove itself again.
|
||||
_lastHwid.TryRemove(id, out _);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ServerConfig.RequirePatchToPlay)
|
||||
return;
|
||||
|
||||
var grace = TimeSpan.FromSeconds(Math.Max(30, ServerConfig.PatchGraceSeconds));
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var playerId in online)
|
||||
{
|
||||
if (!_firstSeen.TryGetValue(playerId, out var since))
|
||||
{
|
||||
// First time seeing them online. The grace window starts now, not at
|
||||
// login, so a slow client that is still loading is not punished for it.
|
||||
_firstSeen[playerId] = now;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool announced = ServerConfig.PatchRequiresPeriodicCheckIn
|
||||
? IsProtected(playerId)
|
||||
: HasCheckedInThisSession(playerId);
|
||||
|
||||
if (announced)
|
||||
{
|
||||
_loggedUnprotected.TryRemove(playerId, out _);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (now - since < grace)
|
||||
continue;
|
||||
|
||||
var player = DBs.PlayerDB.Players.FindById(playerId);
|
||||
if (player?.PlayerRoles?.Contains(PlayerRoles.Developer) == true)
|
||||
continue;
|
||||
|
||||
// Oculus (Quest) players cannot run the PC patch, so they are never kicked
|
||||
// for lacking it - same exemption as the login gate in DenyLogin.
|
||||
if (IsPlatformExempt(player))
|
||||
continue;
|
||||
|
||||
// Kick once per session, not once per sweep. A client that ignores the
|
||||
// dialog should not be sent it every thirty seconds; the flag clears when
|
||||
// they go offline, so a reconnect gets a fresh one.
|
||||
if (!_loggedUnprotected.TryAdd(playerId, 0))
|
||||
continue;
|
||||
|
||||
Console.WriteLine($"[Bluetint] Unpatched client, kicking player {playerId}");
|
||||
|
||||
_unpatched[playerId] = now;
|
||||
|
||||
await Kick(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops one unpatched player with the message on screen.
|
||||
/// <para>
|
||||
/// This reuses the ban dialog because that is the only channel the client already has
|
||||
/// for showing a reason before it disconnects, but nothing is written to the database
|
||||
/// and no ban is recorded. The player is not banned - they are told to install the
|
||||
/// patch. Category Unknown is what makes that work: the message formatter rewrites
|
||||
/// every other category into "Moderator (...)" or "Cheating (...)", and Unknown is
|
||||
/// the one that passes the text through exactly as written.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static async Task Kick(long playerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await NotificationsController.SendBanned(playerId, new ModerationBlockDetails
|
||||
{
|
||||
IsBan = true,
|
||||
ReportCategory = ReportCategory.Unknown,
|
||||
Duration = 0,
|
||||
Message = Message(),
|
||||
ModerationSetUnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Bluetint] Could not deliver the message to {playerId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class ActionLinkDB
|
||||
{
|
||||
public class ActionLink
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Code { get; set; } = "";
|
||||
public long CreatorPlayerId { get; set; }
|
||||
public string Data { get; set; } = "";
|
||||
public int CodeType { get; set; }
|
||||
public int ExtraDataId { get; set; }
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class ActionLinkResponse
|
||||
{
|
||||
public bool isValid { get; set; }
|
||||
public int codeType { get; set; }
|
||||
public string extraJson { get; set; } = "";
|
||||
public long creatorPlayerId { get; set; }
|
||||
public string data { get; set; } = "";
|
||||
}
|
||||
|
||||
public static LiteDatabase ActionLinkDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "ActionLinks.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<ActionLink> ActionLinks = ActionLinkDBFile.GetCollection<ActionLink>("ActionLinks");
|
||||
|
||||
static ActionLinkDB()
|
||||
{
|
||||
ActionLinks.EnsureIndex(x => x.Code);
|
||||
ActionLinks.EnsureIndex(x => x.CreatorPlayerId);
|
||||
ActionLinks.EnsureIndex(x => x.ExpiresAt);
|
||||
}
|
||||
|
||||
private static string GenerateCode()
|
||||
{
|
||||
const string chars = "abcdefghijklmnopqrstuvwxyz";
|
||||
return new string(Enumerable.Range(0, 9).Select(_ => chars[Random.Shared.Next(chars.Length)]).ToArray());
|
||||
}
|
||||
|
||||
public static ActionLink? CreateActionLink(long creatorPlayerId, string data, int codeType, int extraDataId, int validHours)
|
||||
{
|
||||
if (PlayerDB.Players.FindById(creatorPlayerId) == null)
|
||||
return null;
|
||||
|
||||
if (codeType == 5 && !RoomDB.DoesRoomExist(extraDataId))
|
||||
return null;
|
||||
|
||||
string code;
|
||||
do { code = GenerateCode(); } while (ActionLinks.FindOne(x => x.Code == code) != null);
|
||||
|
||||
var link = new ActionLink
|
||||
{
|
||||
Code = code,
|
||||
CreatorPlayerId = creatorPlayerId,
|
||||
Data = data,
|
||||
CodeType = codeType,
|
||||
ExtraDataId = extraDataId,
|
||||
ExpiresAt = DateTime.UtcNow.AddHours(validHours),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
ActionLinks.Insert(link);
|
||||
return link;
|
||||
}
|
||||
|
||||
public static ActionLinkResponse GetActionLinkResponse(string code)
|
||||
{
|
||||
var invalid = new ActionLinkResponse { isValid = false, codeType = 0, extraJson = "", creatorPlayerId = 0, data = "" };
|
||||
var link = ActionLinks.FindOne(x => x.Code == code);
|
||||
if (link == null || DateTime.UtcNow > link.ExpiresAt) return invalid;
|
||||
if (PlayerDB.Players.FindById(link.CreatorPlayerId) == null) return invalid;
|
||||
if (link.CodeType == 5 && !RoomDB.DoesRoomExist(link.ExtraDataId)) return invalid;
|
||||
return new ActionLinkResponse
|
||||
{
|
||||
isValid = true,
|
||||
codeType = link.CodeType,
|
||||
extraJson = link.Data,
|
||||
creatorPlayerId = link.CreatorPlayerId,
|
||||
data = link.Data
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using static Vanadium.Classes.DBs.DBClasses.AnnoucementDBClasses;
|
||||
using System.Text.RegularExpressions;
|
||||
using LiteDB;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.IO;
|
||||
using Vanadium.Controllers;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class AnnoucementDB
|
||||
{
|
||||
public static LiteDatabase AnnoucementDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Annoucements.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<Announcement> Annoucements = AnnoucementDBFile.GetCollection<Announcement>("Annoucements");
|
||||
|
||||
private static readonly Random _random = new Random();
|
||||
|
||||
public static List<Announcement> GetAllAnnouncements()
|
||||
{
|
||||
return Annoucements.FindAll().ToList();
|
||||
}
|
||||
|
||||
public static async Task<bool> CreateAnnouncement(Announcement newAnnouncement, bool sendWs = false)
|
||||
{
|
||||
long uniqueId;
|
||||
while (true)
|
||||
{
|
||||
uniqueId = _random.Next(1147483647, 2147483647);
|
||||
if (!Annoucements.Exists(x => x.AnnouncementId == uniqueId))
|
||||
break;
|
||||
}
|
||||
newAnnouncement.AnnouncementId = uniqueId;
|
||||
var result = Annoucements.Insert(newAnnouncement);
|
||||
bool success = result.AsInt64 == uniqueId;
|
||||
|
||||
if (sendWs && success)
|
||||
await NotificationsController.SendAnnoucementUpdate(newAnnouncement);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
public static bool DeleteAnnouncement(long id)
|
||||
{
|
||||
return Annoucements.Delete(id);
|
||||
}
|
||||
|
||||
public static async Task<int> DeleteAllAnnouncements(bool sendWs = false)
|
||||
{
|
||||
var ids = Annoucements.FindAll().Select(x => x.AnnouncementId).ToList();
|
||||
int count = Annoucements.DeleteAll();
|
||||
if (sendWs)
|
||||
{
|
||||
foreach (var id in ids)
|
||||
await NotificationsController.SendAnnoucementDelete(id);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using LiteDB;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.AntiCheatDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public static class AntiCheatDB
|
||||
{
|
||||
public static LiteDatabase DBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "AntiCheat.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<Detection> Detections = DBFile.GetCollection<Detection>("Detections");
|
||||
|
||||
static AntiCheatDB()
|
||||
{
|
||||
Detections.EnsureIndex(x => x.PlayerId);
|
||||
Detections.EnsureIndex(x => x.SteamId);
|
||||
}
|
||||
|
||||
public static Detection Record(long playerId, string username, ulong steamId, string type,
|
||||
string reason, string modVersion, long? roomId, TimeSpan dedupWindow, out bool isNew)
|
||||
{
|
||||
var cutoff = DateTime.UtcNow - dedupWindow;
|
||||
var existing = Detections.FindOne(d =>
|
||||
d.PlayerId == playerId && d.Type == type && d.Reason == reason && d.LastSeen >= cutoff);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Count++;
|
||||
existing.LastSeen = DateTime.UtcNow;
|
||||
Detections.Update(existing);
|
||||
isNew = false;
|
||||
return existing;
|
||||
}
|
||||
|
||||
var detection = new Detection
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
PlayerId = playerId,
|
||||
Username = username,
|
||||
SteamId = steamId,
|
||||
Type = type,
|
||||
Reason = reason,
|
||||
ModVersion = modVersion,
|
||||
RoomId = roomId,
|
||||
DetectedAt = DateTime.UtcNow,
|
||||
LastSeen = DateTime.UtcNow,
|
||||
Count = 1
|
||||
};
|
||||
Detections.Insert(detection);
|
||||
isNew = true;
|
||||
return detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
using LiteDB;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static Vanadium.Classes.DBs.DBClasses.ChatDBClasses;
|
||||
using System.Text.Json;
|
||||
using Vanadium.Controllers;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class ChatDB
|
||||
{
|
||||
private static readonly Lazy<LiteDatabase> _dbLazy = new(() =>
|
||||
new LiteDatabase(new ConnectionString
|
||||
{
|
||||
Filename = Path.Combine(Environment.CurrentDirectory, "Data", "DBs", "Chat.db"),
|
||||
Connection = ConnectionType.Shared
|
||||
}));
|
||||
|
||||
public static LiteDatabase ChatDBFile => _dbLazy.Value;
|
||||
|
||||
private static ILiteCollection<ChatThread> Threads => ChatDBFile.GetCollection<ChatThread>("threads");
|
||||
private static ILiteCollection<ChatMessage> Messages => ChatDBFile.GetCollection<ChatMessage>("messages");
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
Threads.EnsureIndex(x => x.ChatThreadId, unique: true);
|
||||
Threads.EnsureIndex(x => x.PlayerIds);
|
||||
Messages.EnsureIndex(x => x.ChatThreadId);
|
||||
Messages.EnsureIndex(x => x.ChatMessageId, unique: true);
|
||||
}
|
||||
|
||||
public static ChatThread CreateThread(List<long> memberIds, long creatorid, string? name, out bool createdNew)
|
||||
{
|
||||
// existing
|
||||
var normalizedMemberIds = memberIds
|
||||
.Distinct()
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
|
||||
var existingThread = Threads.FindAll().FirstOrDefault(t =>
|
||||
t.PlayerIds != null &&
|
||||
t.PlayerIds.Distinct().OrderBy(x => x).SequenceEqual(normalizedMemberIds));
|
||||
|
||||
if (existingThread != null)
|
||||
{
|
||||
createdNew = false;
|
||||
return GetThread(existingThread.ChatThreadId)!;
|
||||
}
|
||||
// create ig
|
||||
|
||||
long newThreadId = Threads.Count() + 1;
|
||||
|
||||
ChatThread thread = new ChatThread
|
||||
{
|
||||
ChatThreadId = newThreadId,
|
||||
PlayerIds = normalizedMemberIds,
|
||||
ChatThreadName = name ?? string.Empty,
|
||||
IsFavorited = false,
|
||||
SnoozedUntil = null,
|
||||
LastReadMessageId = 1,
|
||||
Messages = new List<ChatMessage>()
|
||||
};
|
||||
|
||||
Threads.Insert(thread);
|
||||
|
||||
var messageJson = new MessageJson
|
||||
{
|
||||
Type = MessageContentType.Text,
|
||||
Version = 1,
|
||||
Data = $"Player <@U{creatorid}> started a chat"
|
||||
};
|
||||
|
||||
string json = System.Text.Json.JsonSerializer.Serialize(messageJson);
|
||||
|
||||
AddMessage(
|
||||
threadId: newThreadId,
|
||||
senderId: -5,
|
||||
jsonContents: json
|
||||
);
|
||||
|
||||
createdNew = true;
|
||||
return GetThread(newThreadId)!;
|
||||
}
|
||||
|
||||
|
||||
public static ChatMessage AddMessage(long threadId, int senderId, string jsonContents)
|
||||
{
|
||||
var thread = GetThread(threadId);
|
||||
if (thread == null)
|
||||
return null;
|
||||
|
||||
long newMessageId = Messages.Count() + 1;
|
||||
|
||||
ChatMessage msg = new ChatMessage
|
||||
{
|
||||
ChatMessageId = newMessageId,
|
||||
ChatThreadId = threadId,
|
||||
SenderPlayerId = senderId,
|
||||
TimeSent = DateTime.UtcNow,
|
||||
Contents = jsonContents,
|
||||
ModerationState = 0
|
||||
};
|
||||
|
||||
Messages.Insert(msg);
|
||||
|
||||
thread.Messages.Add(msg);
|
||||
thread.LastReadMessageId = newMessageId;
|
||||
Threads.Update(thread);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ChatThread RenameThread(long threadId, long senderId, string NewThreadName)
|
||||
{
|
||||
var thread = GetThread(threadId);
|
||||
if (thread == null)
|
||||
return null;
|
||||
|
||||
thread.ChatThreadName = NewThreadName;
|
||||
|
||||
var messageJson = new MessageJson
|
||||
{
|
||||
Type = MessageContentType.Text,
|
||||
Version = 1,
|
||||
Data = $"Player <@U{senderId}> renamed the chat"
|
||||
};
|
||||
|
||||
string json = System.Text.Json.JsonSerializer.Serialize(messageJson);
|
||||
|
||||
var newmsg = AddMessage(
|
||||
threadId: threadId,
|
||||
senderId: -5,
|
||||
jsonContents: json
|
||||
);
|
||||
|
||||
var payload = new
|
||||
{
|
||||
Id = "ChatMessageReceived",
|
||||
Msg = new
|
||||
{
|
||||
chatMessageId = newmsg.ChatMessageId,
|
||||
chatThreadId = newmsg.ChatThreadId,
|
||||
senderPlayerId = newmsg.SenderPlayerId,
|
||||
timeSent = newmsg.TimeSent,
|
||||
contents = newmsg.Contents,
|
||||
moderationState = newmsg.ModerationState
|
||||
}
|
||||
};
|
||||
|
||||
NotificationsController.SendToPlayers(thread.PlayerIds, System.Text.Json.JsonSerializer.Serialize(payload));
|
||||
Threads.Update(thread);
|
||||
return GetThread(threadId);
|
||||
}
|
||||
|
||||
public static ChatThread LeaveThread(long threadId, long senderId)
|
||||
{
|
||||
var thread = GetThread(threadId);
|
||||
if (thread == null)
|
||||
return null;
|
||||
|
||||
thread.PlayerIds.Remove(senderId);
|
||||
var messageJson = new MessageJson
|
||||
{
|
||||
Type = MessageContentType.Text,
|
||||
Version = 1,
|
||||
Data = $"Player <@U{senderId}> left"
|
||||
};
|
||||
|
||||
string json = System.Text.Json.JsonSerializer.Serialize(messageJson);
|
||||
|
||||
var newmsg = AddMessage(
|
||||
threadId: threadId,
|
||||
senderId: -5,
|
||||
jsonContents: json
|
||||
);
|
||||
|
||||
var payload = new
|
||||
{
|
||||
Id = "ChatMessageReceived",
|
||||
Msg = new
|
||||
{
|
||||
chatMessageId = newmsg.ChatMessageId,
|
||||
chatThreadId = newmsg.ChatThreadId,
|
||||
senderPlayerId = newmsg.SenderPlayerId,
|
||||
timeSent = newmsg.TimeSent,
|
||||
contents = newmsg.Contents,
|
||||
moderationState = newmsg.ModerationState
|
||||
}
|
||||
};
|
||||
|
||||
NotificationsController.SendToPlayers(thread.PlayerIds, System.Text.Json.JsonSerializer.Serialize(payload));
|
||||
|
||||
Threads.Update(thread);
|
||||
return GetThread(threadId);
|
||||
}
|
||||
|
||||
public static ChatThread? GetThread(long threadId)
|
||||
{
|
||||
var thread = Threads.FindOne(x => x.ChatThreadId == threadId);
|
||||
if (thread == null) return null;
|
||||
|
||||
thread.Messages = Messages.Find(x => x.ChatThreadId == threadId).OrderBy(x => x.ChatMessageId).ToList();
|
||||
return thread;
|
||||
}
|
||||
|
||||
public static List<ChatThread> GetThreadsForPlayer(long playerId, int maxCount)
|
||||
{
|
||||
var list = Threads.Find(x => x.PlayerIds.Contains(playerId)).OrderByDescending(x => x.LastReadMessageId).Take(maxCount).ToList();
|
||||
|
||||
foreach (var t in list)
|
||||
{
|
||||
t.Messages = Messages.Find(x => x.ChatThreadId == t.ChatThreadId).OrderByDescending(x => x.ChatMessageId).Take(1).ToList();
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static object? SetThreadFavorited(long threadId, long playerId, bool favorite)
|
||||
{
|
||||
var thread = Threads.FindOne(x => x.ChatThreadId == threadId);
|
||||
if (thread == null) return null;
|
||||
if (!thread.PlayerIds.Contains(playerId)) return null;
|
||||
thread.IsFavorited = favorite;
|
||||
Threads.Update(thread);
|
||||
thread = GetThread(threadId);
|
||||
if (thread == null) return null;
|
||||
|
||||
return new
|
||||
{
|
||||
latestMessage = thread.Messages.Count > 0 ? new
|
||||
{
|
||||
chatMessageId = thread.Messages.Last().ChatMessageId,
|
||||
chatThreadId = thread.Messages.Last().ChatThreadId,
|
||||
senderPlayerId = thread.Messages.Last().SenderPlayerId,
|
||||
timeSent = thread.Messages.Last().TimeSent,
|
||||
contents = thread.Messages.Last().Contents,
|
||||
moderationState = thread.Messages.Last().ModerationState
|
||||
} : (object?)null,
|
||||
chatThreadId = thread.ChatThreadId,
|
||||
playerIds = thread.PlayerIds,
|
||||
lastReadMessageId = thread.LastReadMessageId,
|
||||
chatThreadName = thread.ChatThreadName,
|
||||
chatThreadType = 0,
|
||||
snoozedUntil = thread.SnoozedUntil,
|
||||
isFavorited = thread.IsFavorited
|
||||
};
|
||||
}
|
||||
|
||||
public static bool SetLastReadMessage(long threadId, long playerId, long messageId)
|
||||
{
|
||||
var thread = Threads.FindOne(x => x.ChatThreadId == threadId);
|
||||
if (thread == null) return false;
|
||||
|
||||
if (!thread.PlayerIds.Contains(playerId)) return false;
|
||||
|
||||
thread.LastReadMessageId = messageId;
|
||||
return Threads.Update(thread);
|
||||
}
|
||||
|
||||
public static ChatThread AddMemberToThread(long threadId, long senderId, long memberId)
|
||||
{
|
||||
var thread = GetThread(threadId);
|
||||
if (thread == null)
|
||||
return null;
|
||||
|
||||
thread.PlayerIds.Add(memberId);
|
||||
var messageJson = new MessageJson
|
||||
{
|
||||
Type = MessageContentType.Text,
|
||||
Version = 1,
|
||||
Data = $"Player <@U{memberId}> was added by <@U{senderId}>"
|
||||
};
|
||||
|
||||
string json = System.Text.Json.JsonSerializer.Serialize(messageJson);
|
||||
|
||||
var newmsg = AddMessage(
|
||||
threadId: threadId,
|
||||
senderId: -5,
|
||||
jsonContents: json
|
||||
);
|
||||
|
||||
var payload = new
|
||||
{
|
||||
Id = "ChatMessageReceived",
|
||||
Msg = new
|
||||
{
|
||||
chatMessageId = newmsg.ChatMessageId,
|
||||
chatThreadId = newmsg.ChatThreadId,
|
||||
senderPlayerId = newmsg.SenderPlayerId,
|
||||
timeSent = newmsg.TimeSent,
|
||||
contents = newmsg.Contents,
|
||||
moderationState = newmsg.ModerationState
|
||||
}
|
||||
};
|
||||
|
||||
NotificationsController.SendToPlayers(thread.PlayerIds, System.Text.Json.JsonSerializer.Serialize(payload));
|
||||
|
||||
Threads.Update(thread);
|
||||
return GetThread(threadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,838 @@
|
||||
using LiteDB;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static Vanadium.Classes.DBs.DBClasses.ClubsDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class ClubsDB
|
||||
{
|
||||
public static LiteDatabase ClubsDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Clubs.db") + ";Connection=shared");
|
||||
|
||||
public static readonly ILiteCollection<Club> Clubs =
|
||||
ClubsDBFile.GetCollection<Club>("Clubs");
|
||||
|
||||
private static readonly ILiteCollection<ClubDbCounters> Counters =
|
||||
ClubsDBFile.GetCollection<ClubDbCounters>("Counters");
|
||||
|
||||
public static readonly ILiteCollection<ClubAnnouncement> Announcements =
|
||||
ClubsDBFile.GetCollection<ClubAnnouncement>("ClubAnnouncements");
|
||||
|
||||
|
||||
public static void Setup()
|
||||
{
|
||||
Clubs.EnsureIndex(x => x.ClubId);
|
||||
Clubs.EnsureIndex(x => x.CreatorAccountId);
|
||||
Clubs.EnsureIndex(x => x.Name);
|
||||
Clubs.EnsureIndex(x => x.IsDeleted);
|
||||
Announcements.EnsureIndex(x => x.ClubId);
|
||||
Announcements.EnsureIndex(x => x.AnnouncementId);
|
||||
}
|
||||
|
||||
|
||||
public static long GetNextClubId()
|
||||
{
|
||||
var counter = Counters.FindById("club");
|
||||
if (counter == null)
|
||||
{
|
||||
counter = new ClubDbCounters { Key = "club", Value = 1 };
|
||||
Counters.Insert(counter);
|
||||
return 1;
|
||||
}
|
||||
counter.Value++;
|
||||
Counters.Update(counter);
|
||||
return counter.Value;
|
||||
}
|
||||
|
||||
private static long GetNextPermissionsId()
|
||||
{
|
||||
var counter = Counters.FindById("clubperms");
|
||||
if (counter == null)
|
||||
{
|
||||
counter = new ClubDbCounters { Key = "clubperms", Value = 1 };
|
||||
Counters.Insert(counter);
|
||||
return 1;
|
||||
}
|
||||
counter.Value++;
|
||||
Counters.Update(counter);
|
||||
return counter.Value;
|
||||
}
|
||||
|
||||
private static string GeneratePlayerClubHandle()
|
||||
{
|
||||
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
var rng = new Random();
|
||||
return new string(Enumerable.Range(0, 16).Select(_ => chars[rng.Next(chars.Length)]).ToArray());
|
||||
}
|
||||
|
||||
private static List<ClubPermissions> CreateDefaultPermissions(long clubId)
|
||||
{
|
||||
return new List<ClubPermissions>
|
||||
{
|
||||
new ClubPermissions
|
||||
{
|
||||
ClubPermissionsId = GetNextPermissionsId(),
|
||||
ClubId = clubId,
|
||||
Type = 30,
|
||||
ApproveMember = true,
|
||||
BanUnban = true,
|
||||
CreateEvent = true,
|
||||
EditDetails = true,
|
||||
EditPermissionSettings = true,
|
||||
PostAnnouncement = true
|
||||
},
|
||||
new ClubPermissions
|
||||
{
|
||||
ClubPermissionsId = GetNextPermissionsId(),
|
||||
ClubId = clubId,
|
||||
Type = 20,
|
||||
ApproveMember = true,
|
||||
BanUnban = true,
|
||||
CreateEvent = false,
|
||||
EditDetails = false,
|
||||
EditPermissionSettings = false,
|
||||
PostAnnouncement = false
|
||||
},
|
||||
new ClubPermissions
|
||||
{
|
||||
ClubPermissionsId = GetNextPermissionsId(),
|
||||
ClubId = clubId,
|
||||
Type = 10,
|
||||
ApproveMember = false,
|
||||
BanUnban = false,
|
||||
CreateEvent = false,
|
||||
EditDetails = false,
|
||||
EditPermissionSettings = false,
|
||||
PostAnnouncement = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static Club CreateClub(
|
||||
long creatorId,
|
||||
string name,
|
||||
string description,
|
||||
string category)
|
||||
{
|
||||
long clubId = GetNextClubId();
|
||||
|
||||
var club = new Club
|
||||
{
|
||||
ClubId = clubId,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Category = category,
|
||||
CreatorAccountId = creatorId,
|
||||
MemberCount = 1,
|
||||
Visibility = (int)ClubVisibility.Public,
|
||||
Joinability = (int)ClubJoinability.Open,
|
||||
AllowJuniors = true,
|
||||
MainImageName = "DefaultRoomImage.png",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Members = new List<ClubMember>
|
||||
{
|
||||
new ClubMember
|
||||
{
|
||||
MemberId = GetNextMemberId(),
|
||||
AccountId = creatorId,
|
||||
MemberType = (int)ClubMemberType.Owner,
|
||||
JoinedAt = DateTime.UtcNow
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
club.Permissions = CreateDefaultPermissions(clubId);
|
||||
|
||||
Clubs.Insert(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
private static long GetNextMemberId()
|
||||
{
|
||||
var counter = Counters.FindById("clubmember");
|
||||
if (counter == null)
|
||||
{
|
||||
counter = new ClubDbCounters { Key = "clubmember", Value = 1 };
|
||||
Counters.Insert(counter);
|
||||
return 1;
|
||||
}
|
||||
counter.Value++;
|
||||
Counters.Update(counter);
|
||||
return counter.Value;
|
||||
}
|
||||
|
||||
public static Club CreatePlayerClub(long creatorId)
|
||||
{
|
||||
long clubId = GetNextClubId();
|
||||
string handle = GeneratePlayerClubHandle();
|
||||
|
||||
var club = new Club
|
||||
{
|
||||
ClubId = clubId,
|
||||
Name = handle,
|
||||
Description = "",
|
||||
Category = "Social",
|
||||
CreatorAccountId = creatorId,
|
||||
MemberCount = 1,
|
||||
Visibility = (int)ClubVisibility.Unlisted,
|
||||
Joinability = (int)ClubJoinability.Open,
|
||||
AllowJuniors = true,
|
||||
MainImageName = "DefaultRoomImage.png",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Members = new List<ClubMember>
|
||||
{
|
||||
new ClubMember
|
||||
{
|
||||
MemberId = GetNextMemberId(),
|
||||
AccountId = creatorId,
|
||||
MemberType = (int)ClubMemberType.Owner,
|
||||
JoinedAt = DateTime.UtcNow
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
club.Permissions = CreateDefaultPermissions(clubId);
|
||||
|
||||
Clubs.Insert(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club? GetClub(long clubId)
|
||||
{
|
||||
var club = Clubs.FindById(clubId);
|
||||
return (club == null || club.IsDeleted) ? null : club;
|
||||
}
|
||||
|
||||
public static bool DeleteClub(long clubId)
|
||||
{
|
||||
var club = Clubs.FindById(clubId);
|
||||
if (club == null || club.IsDeleted) return false;
|
||||
club.IsDeleted = true;
|
||||
Clubs.Update(club);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static List<Club> GetClubsCreatedBy(long playerId)
|
||||
=> Clubs.Find(c => c.CreatorAccountId == playerId && !c.IsDeleted).ToList();
|
||||
|
||||
public static List<Club> GetClubsJoinedBy(long playerId)
|
||||
{
|
||||
return Clubs.FindAll()
|
||||
.Where(c => !c.IsDeleted &&
|
||||
c.Members.Any(m => m.AccountId == playerId))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static int GetSubscriberCount(long playerClubId)
|
||||
{
|
||||
var club = GetClub(playerClubId);
|
||||
if (club == null) return 0;
|
||||
return Math.Max(0, club.MemberCount - 1);
|
||||
}
|
||||
|
||||
public static ClubSearchResult Search(
|
||||
string? query,
|
||||
string? category,
|
||||
int sort,
|
||||
int count,
|
||||
int skip = 0)
|
||||
{
|
||||
IEnumerable<Club> all = Clubs.FindAll()
|
||||
.Where(c => !c.IsDeleted && c.Visibility == (int)ClubVisibility.Public);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(category))
|
||||
all = all.Where(c =>
|
||||
string.Equals(c.Category, category, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
string q = query.ToLowerInvariant();
|
||||
|
||||
all = all
|
||||
.Select(c =>
|
||||
{
|
||||
string nameLower = (c.Name ?? "").ToLowerInvariant();
|
||||
string descLower = (c.Description ?? "").ToLowerInvariant();
|
||||
|
||||
int score = 0;
|
||||
|
||||
if (nameLower == q || descLower == q)
|
||||
score += 200;
|
||||
|
||||
if (nameLower.StartsWith(q))
|
||||
score += 100;
|
||||
|
||||
if (nameLower.Contains(q))
|
||||
score += 50;
|
||||
|
||||
if (descLower.Contains(q))
|
||||
score += 20;
|
||||
|
||||
if (c.CustomTags.Any(t => t.ToLowerInvariant().Contains(q)))
|
||||
score += 30;
|
||||
|
||||
score -= Math.Min(nameLower.Length, 50);
|
||||
|
||||
return new { Club = c, Score = score };
|
||||
})
|
||||
.Where(x => x.Score > 0)
|
||||
.OrderByDescending(x => x.Score)
|
||||
.Select(x => x.Club);
|
||||
}
|
||||
|
||||
IEnumerable<Club> sorted = sort switch
|
||||
{
|
||||
1 => all.OrderByDescending(c => c.CreatedAt),
|
||||
2 => all.OrderBy(c => c.CreatedAt),
|
||||
_ => all.OrderByDescending(c => c.MemberCount)
|
||||
};
|
||||
|
||||
var list = sorted.ToList();
|
||||
int total = list.Count;
|
||||
|
||||
var page = list.Skip(skip).Take(count).ToList();
|
||||
|
||||
return new ClubSearchResult
|
||||
{
|
||||
Clubs = page.Select(ToSummary).ToList(),
|
||||
ContinuationToken = null,
|
||||
TotalClubs = total
|
||||
};
|
||||
}
|
||||
|
||||
public static ClubMember? GetMember(long clubId, long playerId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
return club?.Members.FirstOrDefault(m => m.AccountId == playerId);
|
||||
}
|
||||
|
||||
public static bool IsMember(long clubId, long playerId)
|
||||
=> GetMember(clubId, playerId) != null;
|
||||
|
||||
public static List<ClubMemberDTO> GetClubMembers(long clubId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null)
|
||||
return new List<ClubMemberDTO>();
|
||||
|
||||
return club.Members
|
||||
.Select(m => new ClubMemberDTO
|
||||
{
|
||||
MemberId = m.MemberId,
|
||||
AccountId = m.AccountId,
|
||||
ClubId = club.ClubId,
|
||||
MembershipType = m.MemberType,
|
||||
CreatedAt = m.JoinedAt
|
||||
})
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static bool IsOwnerOrCoOwner(long clubId, long playerId)
|
||||
{
|
||||
var m = GetMember(clubId, playerId);
|
||||
return m != null && m.MemberType >= (int)ClubMemberType.CoOwner;
|
||||
}
|
||||
|
||||
public static bool CanEditDetails(long clubId, long playerId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null) return false;
|
||||
if (club.CreatorAccountId == playerId) return true;
|
||||
|
||||
var member = club.Members.FirstOrDefault(m => m.AccountId == playerId);
|
||||
if (member == null) return false;
|
||||
|
||||
var perms = club.Permissions.FirstOrDefault(p => p.Type == member.MemberType);
|
||||
return perms?.EditDetails == true || member.MemberType >= (int)ClubMemberType.CoOwner;
|
||||
}
|
||||
|
||||
public static Club? JoinClub(long clubId, long playerId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null) return null;
|
||||
if (club.BannedMemberIds.Contains(playerId)) return null;
|
||||
if (IsMember(clubId, playerId)) return club;
|
||||
|
||||
bool needsApproval = club.Joinability == (int)ClubJoinability.ApprovalRequired;
|
||||
|
||||
club.Members.Add(new ClubMember
|
||||
{
|
||||
MemberId = GetNextMemberId(),
|
||||
AccountId = playerId,
|
||||
MemberType = (int)ClubMemberType.Member,
|
||||
JoinedAt = DateTime.UtcNow,
|
||||
IsPendingApproval = needsApproval
|
||||
});
|
||||
|
||||
if (!needsApproval)
|
||||
club.MemberCount++;
|
||||
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club LeaveClub(long clubId, long playerId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null) return null;
|
||||
|
||||
var member = club.Members.FirstOrDefault(m => m.AccountId == playerId);
|
||||
if (member == null) return null;
|
||||
|
||||
if (member.MemberType == (int)ClubMemberType.Owner) return null;
|
||||
|
||||
club.Members.Remove(member);
|
||||
if (!member.IsPendingApproval)
|
||||
club.MemberCount = Math.Max(0, club.MemberCount - 1);
|
||||
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club KickMember(long clubId, long targetId, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null) return null;
|
||||
|
||||
var requester = club.Members.FirstOrDefault(m => m.AccountId == requesterId);
|
||||
var target = club.Members.FirstOrDefault(m => m.AccountId == targetId);
|
||||
|
||||
if (requester == null || target == null) return null;
|
||||
if (requester.MemberType <= target.MemberType && club.CreatorAccountId != requesterId)
|
||||
return null;
|
||||
|
||||
club.Members.Remove(target);
|
||||
if (!target.IsPendingApproval)
|
||||
club.MemberCount = Math.Max(0, club.MemberCount - 1);
|
||||
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club ApproveMember(long clubId, long targetId, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null) return null;
|
||||
|
||||
var requester = club.Members.FirstOrDefault(m => m.AccountId == requesterId);
|
||||
if (requester == null) return null;
|
||||
|
||||
var perms = club.Permissions.FirstOrDefault(p => p.Type == requester.MemberType);
|
||||
bool canApprove = club.CreatorAccountId == requesterId ||
|
||||
requester.MemberType >= (int)ClubMemberType.CoOwner ||
|
||||
(perms?.ApproveMember == true);
|
||||
|
||||
if (!canApprove) return null;
|
||||
|
||||
var target = club.Members.FirstOrDefault(m => m.AccountId == targetId && m.IsPendingApproval);
|
||||
if (target == null) return null;
|
||||
|
||||
target.IsPendingApproval = false;
|
||||
club.MemberCount++;
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static bool SetMemberRole(long clubId, long targetId, int newRole, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null) return false;
|
||||
|
||||
var requester = club.Members.FirstOrDefault(m => m.AccountId == requesterId);
|
||||
if (requester == null && club.CreatorAccountId != requesterId) return false;
|
||||
|
||||
var target = club.Members.FirstOrDefault(m => m.AccountId == targetId);
|
||||
if (target == null) return false;
|
||||
|
||||
target.MemberType = newRole;
|
||||
Clubs.Update(club);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool BanMember(long clubId, long targetId, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null) return false;
|
||||
|
||||
if (!IsOwnerOrCoOwner(clubId, requesterId) && club.CreatorAccountId != requesterId)
|
||||
return false;
|
||||
|
||||
var target = club.Members.FirstOrDefault(m => m.AccountId == targetId);
|
||||
if (target != null)
|
||||
{
|
||||
club.Members.Remove(target);
|
||||
if (!target.IsPendingApproval)
|
||||
club.MemberCount = Math.Max(0, club.MemberCount - 1);
|
||||
}
|
||||
|
||||
if (!club.BannedMemberIds.Contains(targetId))
|
||||
club.BannedMemberIds.Add(targetId);
|
||||
|
||||
Clubs.Update(club);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool UnbanMember(long clubId, long targetId, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null) return false;
|
||||
|
||||
if (!IsOwnerOrCoOwner(clubId, requesterId) && club.CreatorAccountId != requesterId)
|
||||
return false;
|
||||
|
||||
club.BannedMemberIds.Remove(targetId);
|
||||
Clubs.Update(club);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Club? SetMainImage(long clubId, string imageName, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null || !CanEditDetails(clubId, requesterId)) return null;
|
||||
club.MainImageName = imageName;
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club? DeleteMainImage(long clubId, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null || !CanEditDetails(clubId, requesterId)) return null;
|
||||
if (club.AdditionalImages.Count == 0) return null;
|
||||
club.MainImageName = club.AdditionalImages[0].ImageName;
|
||||
club.AdditionalImages.RemoveAt(0);
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club? SetAdditionalImage(long clubId, int index, string imageName, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null || !CanEditDetails(clubId, requesterId)) return null;
|
||||
|
||||
var existing = club.AdditionalImages.FirstOrDefault(i => i.Index == index);
|
||||
if (existing != null)
|
||||
existing.ImageName = imageName;
|
||||
else
|
||||
club.AdditionalImages.Add(new ClubAdditionalImage { Index = index, ImageName = imageName });
|
||||
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club? DeleteAdditionalImage(long clubId, int index, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null || !CanEditDetails(clubId, requesterId)) return null;
|
||||
|
||||
var img = club.AdditionalImages.FirstOrDefault(i => i.Index == index);
|
||||
if (img != null)
|
||||
club.AdditionalImages.Remove(img);
|
||||
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club? SetClubhouse(long clubId, long roomId, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null || !IsOwnerOrCoOwner(clubId, requesterId) && club.CreatorAccountId != requesterId)
|
||||
return null;
|
||||
club.ClubhouseRoomId = roomId;
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club? RemoveClubhouse(long clubId, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null || !IsOwnerOrCoOwner(clubId, requesterId) && club.CreatorAccountId != requesterId)
|
||||
return null;
|
||||
club.ClubhouseRoomId = null;
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club? SetPermissions(
|
||||
long clubId,
|
||||
int membershipType,
|
||||
bool approveMember,
|
||||
bool banUnban,
|
||||
bool createEvent,
|
||||
bool editDetails,
|
||||
bool editPermissionSettings,
|
||||
bool postAnnouncement)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null) return null;
|
||||
|
||||
int normalizedType = membershipType switch
|
||||
{
|
||||
0 => (int)ClubMemberType.Member,
|
||||
1 => (int)ClubMemberType.Moderator,
|
||||
2 => (int)ClubMemberType.CoOwner,
|
||||
(int)ClubMemberType.Member or (int)ClubMemberType.Moderator or (int)ClubMemberType.CoOwner => membershipType,
|
||||
_ => -1
|
||||
};
|
||||
|
||||
if (normalizedType == -1) return null;
|
||||
|
||||
var existing = club.Permissions.FirstOrDefault(p => p.Type == normalizedType);
|
||||
if (existing == null)
|
||||
{
|
||||
existing = new ClubPermissions
|
||||
{
|
||||
ClubPermissionsId = GetNextPermissionsId(),
|
||||
ClubId = clubId,
|
||||
Type = normalizedType
|
||||
};
|
||||
club.Permissions.Add(existing);
|
||||
}
|
||||
|
||||
existing.ApproveMember = approveMember;
|
||||
existing.BanUnban = banUnban;
|
||||
existing.CreateEvent = createEvent;
|
||||
existing.EditDetails = editDetails;
|
||||
existing.EditPermissionSettings = editPermissionSettings;
|
||||
existing.PostAnnouncement = postAnnouncement;
|
||||
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club? Modify(
|
||||
long clubId,
|
||||
string name,
|
||||
string description,
|
||||
string category,
|
||||
long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null || !CanEditDetails(clubId, requesterId)) return null;
|
||||
club.Name = name;
|
||||
club.Description = description;
|
||||
club.Category = category;
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club? SetMinLevel(long clubId, int minLevel, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null || !CanEditDetails(clubId, requesterId)) return null;
|
||||
club.MinLevel = minLevel;
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static Club? ModifyDetails(
|
||||
long clubId,
|
||||
int? visibility,
|
||||
int? joinability,
|
||||
bool? allowJuniors,
|
||||
List<string>? customTags,
|
||||
long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null || !CanEditDetails(clubId, requesterId)) return null;
|
||||
|
||||
if (visibility.HasValue) club.Visibility = visibility.Value;
|
||||
if (joinability.HasValue) club.Joinability = joinability.Value;
|
||||
if (allowJuniors.HasValue) club.AllowJuniors = allowJuniors.Value;
|
||||
if (customTags != null)
|
||||
{
|
||||
club.CustomTags = customTags
|
||||
.Select(t => t.Trim())
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
Clubs.Update(club);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static ClubSummaryDTO ToSummary(Club club) => new ClubSummaryDTO
|
||||
{
|
||||
ClubId = club.ClubId,
|
||||
Name = club.Name,
|
||||
Description = club.Description,
|
||||
Category = club.Category,
|
||||
Joinability = club.Joinability,
|
||||
Visibility = club.Visibility,
|
||||
AllowJuniors = club.AllowJuniors,
|
||||
MinLevel = club.MinLevel,
|
||||
ClubType = club.ClubType,
|
||||
IsRRO = club.IsRRO,
|
||||
State = club.State,
|
||||
CreatorAccountId = club.CreatorAccountId,
|
||||
ClubhouseRoomId = club.ClubhouseRoomId,
|
||||
MainImageName = club.MainImageName,
|
||||
MemberCount = club.MemberCount
|
||||
};
|
||||
|
||||
public static ClubDetailsDTO ToDetails(Club club, long requesterId) => new ClubDetailsDTO
|
||||
{
|
||||
ClubId = club.ClubId,
|
||||
Club = ToSummary(club),
|
||||
AdditionalImages = club.AdditionalImages ?? new(),
|
||||
CustomTags = club.CustomTags ?? new(),
|
||||
CoownerPermissions = club.Permissions.FirstOrDefault(p => p.Type == 30),
|
||||
ModeratorPermissions = club.Permissions.FirstOrDefault(p => p.Type == 20),
|
||||
MemberPermissions = club.Permissions.FirstOrDefault(p => p.Type == 10),
|
||||
MyMembershipType = club.Members
|
||||
.FirstOrDefault(m => m.AccountId == requesterId)?.MemberType ?? 0
|
||||
};
|
||||
|
||||
private static long GetNextAnnouncementId()
|
||||
{
|
||||
var counter = Counters.FindById("announcement");
|
||||
if (counter == null)
|
||||
{
|
||||
counter = new ClubDbCounters { Key = "announcement", Value = 1 };
|
||||
Counters.Insert(counter);
|
||||
return 1;
|
||||
}
|
||||
counter.Value++;
|
||||
Counters.Update(counter);
|
||||
return counter.Value;
|
||||
}
|
||||
|
||||
public static bool CanPostAnnouncement(long clubId, long playerId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null) return false;
|
||||
if (club.CreatorAccountId == playerId) return true;
|
||||
|
||||
var member = club.Members.FirstOrDefault(m => m.AccountId == playerId);
|
||||
if (member == null) return false;
|
||||
|
||||
var perms = club.Permissions.FirstOrDefault(p => p.Type == member.MemberType);
|
||||
return member.MemberType >= (int)ClubMemberType.CoOwner || perms?.PostAnnouncement == true;
|
||||
}
|
||||
|
||||
public static ClubAnnouncement? CreateAnnouncement(long clubId, long creatorAccountId, string title, string body, string? imageName, string? meta)
|
||||
{
|
||||
if (!CanPostAnnouncement(clubId, creatorAccountId)) return null;
|
||||
|
||||
var announcement = new ClubAnnouncement
|
||||
{
|
||||
AnnouncementId = GetNextAnnouncementId(),
|
||||
ClubId = clubId,
|
||||
CreatorAccountId = creatorAccountId,
|
||||
Title = title.Trim(),
|
||||
Body = body.Trim(),
|
||||
ImageName = string.IsNullOrWhiteSpace(imageName) ? null : imageName.Trim(),
|
||||
Meta = string.IsNullOrWhiteSpace(meta) ? null : meta,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
Announcements.Insert(announcement);
|
||||
return announcement;
|
||||
}
|
||||
|
||||
public static ClubAnnouncement? EditAnnouncement(long clubId, long announcementId, long requesterId, string title, string body, string? imageName, string? meta)
|
||||
{
|
||||
var announcement = Announcements.FindOne(a => a.AnnouncementId == announcementId && a.ClubId == clubId);
|
||||
if (announcement == null) return null;
|
||||
|
||||
if (announcement.CreatorAccountId != requesterId && !CanPostAnnouncement(clubId, requesterId))
|
||||
return null;
|
||||
|
||||
announcement.Title = title.Trim();
|
||||
announcement.Body = body.Trim();
|
||||
announcement.ImageName = string.IsNullOrWhiteSpace(imageName) ? null : imageName.Trim();
|
||||
announcement.Meta = string.IsNullOrWhiteSpace(meta) ? null : meta;
|
||||
|
||||
Announcements.Update(announcement);
|
||||
return announcement;
|
||||
}
|
||||
|
||||
public static bool DeleteAnnouncement(long clubId, long announcementId, long requesterId)
|
||||
{
|
||||
var announcement = Announcements.FindOne(a => a.AnnouncementId == announcementId && a.ClubId == clubId);
|
||||
if (announcement == null) return false;
|
||||
|
||||
if (announcement.CreatorAccountId != requesterId && !CanPostAnnouncement(clubId, requesterId))
|
||||
return false;
|
||||
|
||||
Announcements.Delete(announcementId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool MarkAnnouncementRead(long clubId, long announcementId, long accountId)
|
||||
{
|
||||
var announcement = Announcements.FindOne(a => a.AnnouncementId == announcementId && a.ClubId == clubId);
|
||||
if (announcement == null) return false;
|
||||
|
||||
var allForClub = Announcements.Find(a => a.ClubId == clubId).ToList();
|
||||
|
||||
foreach (var a in allForClub)
|
||||
{
|
||||
if (!a.LastReadAnnouncementIdByAccount.ContainsKey(accountId) ||
|
||||
a.LastReadAnnouncementIdByAccount[accountId] < announcementId)
|
||||
{
|
||||
a.LastReadAnnouncementIdByAccount[accountId] = announcementId;
|
||||
Announcements.Update(a);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ClubAnnouncementListDTO? GetClubAnnouncements(long clubId, long requesterId)
|
||||
{
|
||||
var club = GetClub(clubId);
|
||||
if (club == null) return null;
|
||||
|
||||
var allAnnouncements = Announcements
|
||||
.Find(a => a.ClubId == clubId)
|
||||
.OrderByDescending(a => a.CreatedAt)
|
||||
.ToList();
|
||||
|
||||
long? lastAnnouncementId = allAnnouncements.Count > 0
|
||||
? allAnnouncements.Max(a => a.AnnouncementId)
|
||||
: (long?)null;
|
||||
|
||||
long lastReadAnnouncementId = 0;
|
||||
foreach (var a in allAnnouncements)
|
||||
{
|
||||
if (a.LastReadAnnouncementIdByAccount.TryGetValue(requesterId, out long readId))
|
||||
lastReadAnnouncementId = Math.Max(lastReadAnnouncementId, readId);
|
||||
}
|
||||
|
||||
return new ClubAnnouncementListDTO
|
||||
{
|
||||
ClubId = clubId,
|
||||
Announcements = allAnnouncements.Select(ToAnnouncementDTO).ToList(),
|
||||
LastAnnouncementId = lastAnnouncementId,
|
||||
LastReadAnnouncementId = lastReadAnnouncementId
|
||||
};
|
||||
}
|
||||
|
||||
public static List<ClubAnnouncementListDTO> GetAnnouncementsForMyClubs(long accountId)
|
||||
{
|
||||
return GetClubsJoinedBy(accountId)
|
||||
.Select(c => GetClubAnnouncements(c.ClubId, accountId))
|
||||
.Where(dto => dto != null && dto.Announcements.Count > 0)
|
||||
.ToList()!;
|
||||
}
|
||||
|
||||
public static ClubAnnouncementDTO ToAnnouncementDTO(ClubAnnouncement a) => new ClubAnnouncementDTO
|
||||
{
|
||||
AnnouncementId = a.AnnouncementId,
|
||||
ClubId = a.ClubId,
|
||||
CreatorAccountId = a.CreatorAccountId,
|
||||
Title = a.Title,
|
||||
Body = a.Body,
|
||||
ImageName = a.ImageName,
|
||||
Meta = a.Meta,
|
||||
CreatedAt = a.CreatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using LiteDB;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using static Vanadium.Classes.DBs.DBClasses.CustomClothingDBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class CustomClothingDB
|
||||
{
|
||||
public static LiteDatabase CustomClothingDBFile = new LiteDatabase(
|
||||
"Filename=" + Path.Join(Program.dataDir, "DBs", "CustomClothing.db") + ";Connection=shared");
|
||||
|
||||
public static readonly ILiteCollection<CustomClothingItem> CustomClothing =
|
||||
CustomClothingDBFile.GetCollection<CustomClothingItem>("CustomClothing");
|
||||
|
||||
private static readonly HttpClient Http = new HttpClient();
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
|
||||
public static List<CustomClothingItem> GetByCreator(long creatorId, bool includePrivate)
|
||||
{
|
||||
if (includePrivate)
|
||||
return CustomClothing.Find(x => x.CreatorAccountId == creatorId).ToList();
|
||||
return CustomClothing.Find(x => x.CreatorAccountId == creatorId && x.Accessibility == ClothingAccessibility.Public).ToList();
|
||||
}
|
||||
|
||||
public static int CountByCreator(long creatorId, bool includePrivate)
|
||||
{
|
||||
if (includePrivate)
|
||||
return CustomClothing.Count(x => x.CreatorAccountId == creatorId);
|
||||
return CustomClothing.Count(x => x.CreatorAccountId == creatorId && x.Accessibility == ClothingAccessibility.Public);
|
||||
}
|
||||
|
||||
public static List<CustomClothingItem> GetBulk(List<Guid> ids)
|
||||
{
|
||||
return CustomClothing.Find(x => ids.Contains(x.Id)).ToList();
|
||||
}
|
||||
|
||||
public static CustomClothingItem? FindById(Guid id)
|
||||
{
|
||||
return CustomClothing.FindById(id);
|
||||
}
|
||||
|
||||
public static void Insert(CustomClothingItem item)
|
||||
{
|
||||
CustomClothing.Insert(item);
|
||||
}
|
||||
|
||||
public static void Update(CustomClothingItem item)
|
||||
{
|
||||
CustomClothing.Update(item);
|
||||
}
|
||||
|
||||
public static List<CustomClothingItem> GetFeatured(int limit = 20) // expermintal
|
||||
{
|
||||
return CustomClothing.FindAll()
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<CustomClothingItem> GetHot(int limit = 20) // expermintal
|
||||
{
|
||||
return CustomClothing.FindAll()
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static long GetOrCreateImportAccountId()
|
||||
{
|
||||
var existing = PlayerDB.Players.FindOne(x => x.Player != null && x.Player.Username == "Import");
|
||||
if (existing != null)
|
||||
return existing.PlayerId;
|
||||
|
||||
var importPlayer = new FullPlayer
|
||||
{
|
||||
PlatformIds = new List<mPlatformID>(),
|
||||
Player = new Player
|
||||
{
|
||||
Username = "Import",
|
||||
DisplayName = "Import",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
LastLoginAt = DateTime.UtcNow,
|
||||
ProfileImage = "DefaultPFP.png",
|
||||
AvailableUsernameChanges = 0,
|
||||
},
|
||||
PlayerRoles = new List<PlayerRoles> { PlayerRoles.Developer },
|
||||
AuthToken = Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
PlayerDB.Players.Insert(importPlayer);
|
||||
return importPlayer.PlayerId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using LiteDB;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class AnnoucementDBClasses
|
||||
{
|
||||
// from game dump :3
|
||||
public class Announcement
|
||||
{
|
||||
[BsonId]
|
||||
public long AnnouncementId { get; set; }
|
||||
public AnnouncementType AnnouncementType { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Body { get; set; }
|
||||
public string? ImageName { get; set; }
|
||||
public LinkType LinkType { get; set; }
|
||||
public string? LinkName { get; set; }
|
||||
public string? LinkButtonLabel { get; set; }
|
||||
public string? LinkUri { get; set; }
|
||||
public Platforms Platform { get; set; } = Platforms.All;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class DirectorAnnouncementRequest : Announcement { public bool Clear { get; set; } = false; }
|
||||
|
||||
public enum AnnouncementType
|
||||
{
|
||||
Update,
|
||||
Contest,
|
||||
Store,
|
||||
Event,
|
||||
Warning,
|
||||
Item
|
||||
}
|
||||
|
||||
public enum LinkType
|
||||
{
|
||||
Url,
|
||||
AccountId,
|
||||
EventId,
|
||||
RoomName,
|
||||
Storefront,
|
||||
[Obsolete] // game said not to use ts?
|
||||
ActionCode,
|
||||
Item,
|
||||
CustomAvatarItem,
|
||||
AuthorizedRecNetUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public static class AntiCheatDBClasses
|
||||
{
|
||||
public class Detection
|
||||
{
|
||||
[BsonId]
|
||||
public ObjectId Id { get; set; }
|
||||
public long PlayerId { get; set; }
|
||||
public string Username { get; set; }
|
||||
public ulong SteamId { get; set; }
|
||||
public string Type { get; set; } // "cheat" | "injection"
|
||||
public string Reason { get; set; }
|
||||
public string ModVersion { get; set; }
|
||||
public long? RoomId { get; set; }
|
||||
public DateTime DetectedAt { get; set; }
|
||||
public DateTime LastSeen { get; set; }
|
||||
public int Count { get; set; } = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using LiteDB;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class ChatDBClasses
|
||||
{
|
||||
public class ChatThread
|
||||
{
|
||||
public List<ChatMessage> Messages { get; set; }
|
||||
public long ChatThreadId { get; set; }
|
||||
public List<long> PlayerIds { get; set; }
|
||||
public long LastReadMessageId { get; set; }
|
||||
[JsonIgnore]
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public Dictionary<long, long> PlayerReadStates { get; set; } = new();
|
||||
public string ChatThreadName { get; set; }
|
||||
public DateTime? SnoozedUntil { get; set; }
|
||||
public bool IsFavorited { get; set; }
|
||||
}
|
||||
|
||||
public class ChatMessage
|
||||
{
|
||||
public long ChatMessageId { get; set; }
|
||||
public long ChatThreadId { get; set; }
|
||||
public int SenderPlayerId { get; set; }
|
||||
public DateTime TimeSent { get; set; }
|
||||
public string Contents { get; set; }
|
||||
public int ModerationState { get; set; }
|
||||
}
|
||||
|
||||
public class MessageJson
|
||||
{
|
||||
public MessageContentType Type { get; set; }
|
||||
public int Version { get; set; }
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
public struct SendMessageResponse
|
||||
{
|
||||
public ChatResults ChatResult { get; set; }
|
||||
public ChatThread ChatThread { get; set; }
|
||||
}
|
||||
|
||||
public enum ChatResults
|
||||
{
|
||||
Success,
|
||||
InvalidArguments,
|
||||
ThreadNotFound,
|
||||
MembershipNotFound,
|
||||
PlayerAlreadyOnThread,
|
||||
CannotMessagePlayer,
|
||||
InvalidCharacters,
|
||||
RecentlyLeftThread,
|
||||
ThreadTooLarge
|
||||
}
|
||||
|
||||
public enum ModerationState : byte
|
||||
{
|
||||
Active,
|
||||
Junior_Pending = 11,
|
||||
Moderation_Pending = 100,
|
||||
Moderation_Closed,
|
||||
Moderation_Banned,
|
||||
MarkedForDelete = 255
|
||||
}
|
||||
|
||||
public enum QueryMode
|
||||
{
|
||||
Latest,
|
||||
NewerThan,
|
||||
OlderThan
|
||||
}
|
||||
|
||||
public enum MessageContentType
|
||||
{
|
||||
Text,
|
||||
PartyInvite,
|
||||
Photo
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class ClubsDBClasses
|
||||
{
|
||||
public class Club
|
||||
{
|
||||
[BsonId]
|
||||
public long ClubId { get; set; }
|
||||
|
||||
public string Name { get; set; } = "";
|
||||
public string? Description { get; set; }
|
||||
public string Category { get; set; } = "Social";
|
||||
|
||||
public int Joinability { get; set; } = 0;
|
||||
|
||||
public int Visibility { get; set; } = 1;
|
||||
|
||||
public bool AllowJuniors { get; set; } = true;
|
||||
public int MinLevel { get; set; } = 0;
|
||||
public ClubType ClubType { get; set; } = ClubType.Generic;
|
||||
public bool IsRRO { get; set; } = false;
|
||||
|
||||
public int State { get; set; } = 0;
|
||||
|
||||
public long CreatorAccountId { get; set; }
|
||||
public long? ClubhouseRoomId { get; set; }
|
||||
|
||||
public string MainImageName { get; set; } = "DefaultRoomImage.png";
|
||||
|
||||
public int MemberCount { get; set; } = 1;
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public List<ClubMember> Members { get; set; } = new();
|
||||
|
||||
public List<ClubAdditionalImage> AdditionalImages { get; set; } = new();
|
||||
|
||||
public List<string> CustomTags { get; set; } = new();
|
||||
|
||||
public List<ClubPermissions> Permissions { get; set; } = new();
|
||||
|
||||
public List<long> BannedMemberIds { get; set; } = new();
|
||||
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
}
|
||||
|
||||
public class ClubMember
|
||||
{
|
||||
public long MemberId { get; set; }
|
||||
public long AccountId { get; set; }
|
||||
public int MemberType { get; set; } = 10;
|
||||
public DateTime JoinedAt { get; set; } = DateTime.UtcNow;
|
||||
public bool IsPendingApproval { get; set; } = false;
|
||||
}
|
||||
|
||||
|
||||
public class ClubAdditionalImage
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public string ImageName { get; set; } = "";
|
||||
}
|
||||
|
||||
public class ClubPermissions
|
||||
{
|
||||
[BsonId]
|
||||
public long ClubPermissionsId { get; set; }
|
||||
|
||||
public long ClubId { get; set; }
|
||||
|
||||
public int Type { get; set; }
|
||||
|
||||
public bool ApproveMember { get; set; }
|
||||
public bool BanUnban { get; set; }
|
||||
public bool CreateEvent { get; set; }
|
||||
public bool EditDetails { get; set; }
|
||||
public bool EditPermissionSettings { get; set; }
|
||||
public bool PostAnnouncement { get; set; }
|
||||
}
|
||||
|
||||
public class ClubDbCounters
|
||||
{
|
||||
[BsonId]
|
||||
public string Key { get; set; } = "";
|
||||
public long Value { get; set; }
|
||||
}
|
||||
|
||||
public class ClubAnnouncement
|
||||
{
|
||||
[BsonId]
|
||||
public long AnnouncementId { get; set; }
|
||||
|
||||
public long ClubId { get; set; }
|
||||
|
||||
public long CreatorAccountId { get; set; }
|
||||
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
public string Body { get; set; } = "";
|
||||
|
||||
public string? ImageName { get; set; }
|
||||
|
||||
public string? Meta { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public Dictionary<long, long> LastReadAnnouncementIdByAccount { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ClubAnnouncementDTO
|
||||
{
|
||||
public long AnnouncementId { get; set; }
|
||||
public long ClubId { get; set; }
|
||||
public long CreatorAccountId { get; set; }
|
||||
public string Title { get; set; } = "";
|
||||
public string Body { get; set; } = "";
|
||||
public string? ImageName { get; set; }
|
||||
public string? Meta { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class ClubAnnouncementListDTO
|
||||
{
|
||||
public long ClubId { get; set; }
|
||||
public List<ClubAnnouncementDTO> Announcements { get; set; } = new();
|
||||
public long? LastAnnouncementId { get; set; }
|
||||
public long LastReadAnnouncementId { get; set; }
|
||||
}
|
||||
|
||||
public class ClubSummaryDTO
|
||||
{
|
||||
public long ClubId { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string? Description { get; set; }
|
||||
public string Category { get; set; } = "Social";
|
||||
public int Joinability { get; set; }
|
||||
public int Visibility { get; set; }
|
||||
public bool AllowJuniors { get; set; }
|
||||
public int MinLevel { get; set; }
|
||||
public ClubType ClubType { get; set; }
|
||||
public bool IsRRO { get; set; }
|
||||
public int State { get; set; }
|
||||
public long CreatorAccountId { get; set; }
|
||||
public long? ClubhouseRoomId { get; set; }
|
||||
public string MainImageName { get; set; } = "";
|
||||
public int MemberCount { get; set; }
|
||||
}
|
||||
|
||||
public class ClubDetailsDTO
|
||||
{
|
||||
public long ClubId { get; set; }
|
||||
public ClubSummaryDTO Club { get; set; } = new();
|
||||
public List<ClubAdditionalImage> AdditionalImages { get; set; } = new();
|
||||
public List<string> CustomTags { get; set; } = new();
|
||||
public ClubPermissions? CoownerPermissions { get; set; }
|
||||
public ClubPermissions? ModeratorPermissions { get; set; }
|
||||
public ClubPermissions? MemberPermissions { get; set; }
|
||||
|
||||
public int MyMembershipType { get; set; }
|
||||
}
|
||||
|
||||
public class ClubSearchResult
|
||||
{
|
||||
public List<ClubSummaryDTO> Clubs { get; set; } = new();
|
||||
public string? ContinuationToken { get; set; }
|
||||
public int TotalClubs { get; set; }
|
||||
}
|
||||
|
||||
public class ClubMemberDTO
|
||||
{
|
||||
public long MemberId { get; set; }
|
||||
public long AccountId { get; set; }
|
||||
public long ClubId { get; set; }
|
||||
public int MembershipType { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public enum ClubVisibility
|
||||
{
|
||||
Private = 0,
|
||||
Public = 1,
|
||||
Unlisted = 2
|
||||
}
|
||||
|
||||
public enum ClubJoinability
|
||||
{
|
||||
Open = 0,
|
||||
ApprovalRequired = 1,
|
||||
Closed = 2
|
||||
}
|
||||
|
||||
public enum ClubMemberType
|
||||
{
|
||||
Member = 10,
|
||||
Moderator = 20,
|
||||
CoOwner = 30,
|
||||
Owner = 100
|
||||
}
|
||||
|
||||
public enum ClubType
|
||||
{
|
||||
Generic,
|
||||
Creator
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class CustomClothingDBClasses
|
||||
{
|
||||
public class CustomClothingItem
|
||||
{
|
||||
[BsonId]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public long CreatorAccountId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public int Price { get; set; }
|
||||
public bool IsFeatured { get; set; } = false;
|
||||
public int? BaseAvatarItemId { get; set; }
|
||||
public string BaseAvatarItemColor { get; set; } = "#FFFFFF";
|
||||
public string DesignFilename { get; set; } = string.Empty;
|
||||
public string ThumbnailImageFilename { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime ModifiedAt { get; set; } = DateTime.UtcNow;
|
||||
public ClothingAccessibility Accessibility { get; set; } = ClothingAccessibility.Public;
|
||||
public ClothingPreviewOrientation PreviewOrientation { get; set; } = ClothingPreviewOrientation.Front;
|
||||
|
||||
[BsonIgnore]
|
||||
public Dictionary<string, object> ToDictionary()
|
||||
{
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["CustomAvatarItemId"] = Id.ToString(),
|
||||
["CreatorAccountId"] = CreatorAccountId,
|
||||
["Name"] = Name,
|
||||
["Description"] = Description,
|
||||
["Price"] = Price,
|
||||
["Accessibility"] = (int)Accessibility,
|
||||
["IsFeatured"] = IsFeatured,
|
||||
["BaseAvatarItemColor"] = BaseAvatarItemColor,
|
||||
["DesignFilename"] = DesignFilename,
|
||||
["ThumbnailImageFilename"] = ThumbnailImageFilename,
|
||||
["CreatedAt"] = CreatedAt.ToString("O"),
|
||||
["ModifiedAt"] = ModifiedAt.ToString("O"),
|
||||
["PreviewOrientation"] = (int)PreviewOrientation,
|
||||
};
|
||||
if (BaseAvatarItemId.HasValue)
|
||||
data["BaseAvatarItemId"] = BaseAvatarItemId.Value;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomClothingMetaRequest
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public required int Price { get; set; }
|
||||
public int? BaseAvatarItemId { get; set; }
|
||||
public required string BaseAvatarItemColor { get; set; }
|
||||
public required ClothingAccessibility Accessibility { get; set; }
|
||||
//public required ClothingPreviewOrientation PreviewOrientation { get; set; }
|
||||
}
|
||||
|
||||
public class RecNetCustomClothingDto
|
||||
{
|
||||
public Guid CustomAvatarItemId { get; set; }
|
||||
public long CreatorAccountId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public int Price { get; set; }
|
||||
public ClothingAccessibility Accessibility { get; set; }
|
||||
public bool IsFeatured { get; set; }
|
||||
public string BaseAvatarItemColor { get; set; } = string.Empty;
|
||||
public string DesignFilename { get; set; } = string.Empty;
|
||||
public string ThumbnailImageFilename { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime ModifiedAt { get; set; }
|
||||
public int? BaseAvatarItemId { get; set; }
|
||||
public ClothingPreviewOrientation PreviewOrientation { get; set; } = ClothingPreviewOrientation.Front;
|
||||
}
|
||||
|
||||
public enum ClothingAccessibility
|
||||
{
|
||||
Public = 0,
|
||||
Private = 1,
|
||||
JuniorFriendly = 2
|
||||
}
|
||||
|
||||
public enum ClothingPreviewOrientation
|
||||
{
|
||||
Front = 0,
|
||||
Back = 1,
|
||||
Side = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class EventDBClasses
|
||||
{
|
||||
public enum PlayerEventAccessibility
|
||||
{
|
||||
Private = 0,
|
||||
Public = 1,
|
||||
Unlisted = 2
|
||||
}
|
||||
|
||||
public enum PlayerEventResponseType
|
||||
{
|
||||
None = -1,
|
||||
Yes = 0,
|
||||
Interested = 1,
|
||||
No = 2,
|
||||
Pending = 3
|
||||
}
|
||||
|
||||
public class PlayerEventTag
|
||||
{
|
||||
[BsonId]
|
||||
public long Id { get; set; }
|
||||
public long PlayerEventId { get; set; }
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
public int Type { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class PlayerEventRsvp
|
||||
{
|
||||
[BsonId]
|
||||
public long PlayerEventResponseId { get; set; }
|
||||
public long PlayerEventId { get; set; }
|
||||
public long PlayerId { get; set; }
|
||||
public int Type { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class PlayerEvent
|
||||
{
|
||||
[BsonId]
|
||||
public long PlayerEventId { get; set; }
|
||||
public long CreatorPlayerId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public int Accessibility { get; set; } = (int)PlayerEventAccessibility.Public;
|
||||
public long? ClubId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public long? SubRoomId { get; set; }
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime EndTime { get; set; }
|
||||
public string ImageName { get; set; } = string.Empty;
|
||||
public int State { get; set; } = 0;
|
||||
public int AttendeeCount { get; set; } = 0;
|
||||
public List<PlayerEventTag> Tags { get; set; } = new();
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public int CanRequestBroadcastPermissions { get; set; } = 0;
|
||||
public int DefaultBroadcastPermissions { get; set; } = 0;
|
||||
public bool IsMultiInstance { get; set; } = false;
|
||||
public bool SupportMultiInstanceRoomChat { get; set; } = false;
|
||||
}
|
||||
|
||||
public class LivePlayerEvent
|
||||
{
|
||||
public long PlayerEventId { get; set; }
|
||||
public long CreatorPlayerId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public int Accessibility { get; set; }
|
||||
public long? ClubId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public long? SubRoomId { get; set; }
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime EndTime { get; set; }
|
||||
public string? ImageName { get; set; }
|
||||
public int State { get; set; }
|
||||
public int AttendeeCount { get; set; }
|
||||
public List<PlayerEventTag> Tags { get; set; } = new();
|
||||
public bool IsFull { get; set; }
|
||||
public int PlayerCount { get; set; }
|
||||
public int CanRequestBroadcastPermissions { get; set; } = 0;
|
||||
public int DefaultBroadcastPermissions { get; set; } = 0;
|
||||
public bool IsMultiInstance { get; set; } = false;
|
||||
public bool SupportMultiInstanceRoomChat { get; set; } = false;
|
||||
}
|
||||
|
||||
public class RespondRequest
|
||||
{
|
||||
public long PlayerEventId { get; set; }
|
||||
public string Type { get; set; } = "Yes";
|
||||
}
|
||||
|
||||
public class PlayerEventRequest
|
||||
{
|
||||
public int Accessibility { get; set; } = (int)PlayerEventAccessibility.Public;
|
||||
public long? ClubId { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string EndTime { get; set; } = string.Empty;
|
||||
public string ImageName { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public long RoomId { get; set; }
|
||||
public string StartTime { get; set; } = string.Empty;
|
||||
public long? SubRoomId { get; set; }
|
||||
public List<string> Tags { get; set; } = new();
|
||||
public int CanRequestBroadcastPermissions { get; set; } = 0;
|
||||
public int DefaultBroadcastPermissions { get; set; } = 0;
|
||||
public bool IsMultiInstance { get; set; } = false;
|
||||
public bool SupportMultiInstanceRoomChat { get; set; } = false;
|
||||
}
|
||||
|
||||
public class PlayerEventResponse
|
||||
{
|
||||
public PlayerEvent? PlayerEvent { get; set; }
|
||||
public int Result { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class PlayerEventRespondRequest
|
||||
{
|
||||
public long PlayerEventId { get; set; }
|
||||
public int Type { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerEventRoomRequest
|
||||
{
|
||||
public long RoomId { get; set; }
|
||||
public long? SubRoomId { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerEventTimeRequest
|
||||
{
|
||||
public string? StartTime { get; set; }
|
||||
public string? EndTime { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerEventClubRequest
|
||||
{
|
||||
public long? ClubId { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerEventMultiInstanceRequest
|
||||
{
|
||||
public bool IsMultiInstance { get; set; }
|
||||
public bool SupportMultiInstanceRoomChat { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class FriendsDBClasses
|
||||
{
|
||||
public class FriendEntry
|
||||
{
|
||||
[BsonId]
|
||||
public ObjectId? Id { get; set; }
|
||||
public long PlayerID { get; set; }
|
||||
public RelationshipDetail Relationship { get; set; } = new RelationshipDetail();
|
||||
}
|
||||
|
||||
public class RelationshipDetail
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long PlayerID { get; set; }
|
||||
public long OtherPlayerID { get; set; }
|
||||
public RelationshipType RelationshipType { get; set; }
|
||||
public ReciprocalStatus Muted { get; set; }
|
||||
public ReciprocalStatus Ignored { get; set; }
|
||||
public ReciprocalStatus Favorited { get; set; }
|
||||
public int VoiceVolume { get; set; } = 100;
|
||||
}
|
||||
|
||||
public enum RelationshipType
|
||||
{
|
||||
None,
|
||||
Sent,
|
||||
Received,
|
||||
Friend,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum FriendInfo
|
||||
{
|
||||
None = 0,
|
||||
Favorited = 1,
|
||||
Muted = 2,
|
||||
Ignored = 4,
|
||||
Blocked = 5
|
||||
}
|
||||
|
||||
public enum ReciprocalStatus
|
||||
{
|
||||
None,
|
||||
Local,
|
||||
Remote,
|
||||
Mutual
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class GiftsDBClasses
|
||||
{
|
||||
public class Gift
|
||||
{
|
||||
[BsonId]
|
||||
public long Id { get; set; }
|
||||
public long ToPlayerId { get; set; }
|
||||
public long FromPlayerId { get; set; } = 1;
|
||||
public string AvatarItemDesc { get; set; } = "";
|
||||
public int AvatarItemType { get; set; } = 0;
|
||||
public int BalanceType { get; set; } = -2;
|
||||
public string ConsumableItemDesc { get; set; } = "";
|
||||
public int Currency { get; set; } = 0;
|
||||
public int CurrencyType { get; set; } = 0;
|
||||
public string EquipmentModificationGuid { get; set; } = "";
|
||||
public string EquipmentPrefabName { get; set; } = "";
|
||||
public int GiftContext { get; set; } = 0;
|
||||
public int GiftRarity { get; set; } = -1;
|
||||
public int Level { get; set; } = 0;
|
||||
public string Message { get; set; } = "A gift for you <3";
|
||||
public int Platform { get; set; } = -1;
|
||||
public int PlatformsToSpawnOn { get; set; } = -1;
|
||||
public int Xp { get; set; } = 0;
|
||||
public bool Consumed { get; set; } = false;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class GiftDTO
|
||||
{
|
||||
public string AvatarItemDesc { get; set; } = "";
|
||||
public int AvatarItemType { get; set; } = 0;
|
||||
public int BalanceType { get; set; } = -2;
|
||||
public string ConsumableItemDesc { get; set; } = "";
|
||||
public int Currency { get; set; } = 0;
|
||||
public int CurrencyType { get; set; } = 0;
|
||||
public string EquipmentModificationGuid { get; set; } = "";
|
||||
public string EquipmentPrefabName { get; set; } = "";
|
||||
public long FromPlayerId { get; set; } = 1;
|
||||
public int GiftContext { get; set; } = 0;
|
||||
public int GiftRarity { get; set; } = -1;
|
||||
public long Id { get; set; }
|
||||
public int Level { get; set; } = 0;
|
||||
public string Message { get; set; } = "A gift for you <3";
|
||||
public int Platform { get; set; } = -1;
|
||||
public int PlatformsToSpawnOn { get; set; } = -1;
|
||||
public int Xp { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class ConsumeGiftRequest
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public int UnlockedLevel { get; set; } = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public static class HwidDBClasses
|
||||
{
|
||||
/// <summary>
|
||||
/// One machine, and every account that has ever reported from it.
|
||||
/// <para>
|
||||
/// The ban fields live on the record rather than in a second collection so that a
|
||||
/// lookup by hwid answers both questions - who is this machine, and is it banned -
|
||||
/// in a single read, on the path that runs for every client that checks in.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class HwidRecord
|
||||
{
|
||||
[BsonId]
|
||||
public string Hwid { get; set; }
|
||||
public List<long> PlayerIds { get; set; } = new();
|
||||
public DateTime FirstSeen { get; set; }
|
||||
public DateTime LastSeen { get; set; }
|
||||
|
||||
/// <summary>True while the machine itself is banned, separately from any account.</summary>
|
||||
public bool IsBanned { get; set; }
|
||||
|
||||
/// <summary>Why. Required when banning; a blank reason makes a ban unreviewable.</summary>
|
||||
public string BanReason { get; set; }
|
||||
|
||||
/// <summary>Player id of the moderator who issued it, or a system name.</summary>
|
||||
public string BannedBy { get; set; }
|
||||
|
||||
public DateTime? BannedAt { get; set; }
|
||||
|
||||
/// <summary>Null bans permanently.</summary>
|
||||
public DateTime? BanExpiresAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Accounts linked to the machine at the moment of the ban. A snapshot: it shows
|
||||
/// what the moderator was looking at, which is not always what the link table
|
||||
/// says later.
|
||||
/// </summary>
|
||||
public List<long> LinkedAtBan { get; set; } = new();
|
||||
|
||||
public bool BanIsActive(DateTime utcNow) =>
|
||||
IsBanned && (BanExpiresAt == null || BanExpiresAt > utcNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using LiteDB;
|
||||
|
||||
public static partial class ImageMetadataDB
|
||||
{
|
||||
public class FullSavedImage
|
||||
{
|
||||
[BsonId]
|
||||
public int Id { get; set; }
|
||||
public ImageAccessibility Accessibility { get; set; }
|
||||
public bool AccessibilityLocked { get; set; } = false;
|
||||
public int CheerCount { get; set; } = 0;
|
||||
public int CommentCount { get; set; } = 0;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public string? Description { get; set; }
|
||||
public string ImageName { get; set; }
|
||||
public ulong PlayerEventId { get; set; }
|
||||
public ulong PlayerId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public SavedImageTypeEnum Type { get; set; }
|
||||
public List<ulong> TaggedPlayerIds { get; set; } = new();
|
||||
public bool DevLocked { get; set; } = false;
|
||||
}
|
||||
|
||||
public class ImageV6
|
||||
{
|
||||
public ImageAccessibility Accessibility { get; set; }
|
||||
public bool AccessibilityLocked { get; set; } = false;
|
||||
public int CheerCount { get; set; } = 0;
|
||||
public int CommentCount { get; set; } = 0;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public string? Description { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string ImageName { get; set; }
|
||||
public ulong PlayerEventId { get; set; }
|
||||
public ulong PlayerId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public List<ulong> TaggedPlayerIds { get; set; } = new();
|
||||
public SavedImageTypeEnum Type { get; set; }
|
||||
}
|
||||
|
||||
public class ImagesPlayer
|
||||
{
|
||||
public ImageAccessibility Accessibility { get; set; }
|
||||
public bool AccessibilityLocked { get; set; } = false;
|
||||
public int CheerCount { get; set; } = 0;
|
||||
public long ClubId { get; set; } = 0;
|
||||
public int CommentCount { get; set; } = 0;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public string? Description { get; set; }
|
||||
public string ImageName { get; set; }
|
||||
public ulong PlayerEventId { get; set; }
|
||||
public ulong PlayerId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public int SavedImageId { get; set; }
|
||||
public SavedImageTypeEnum SavedImageType { get; set; }
|
||||
}
|
||||
|
||||
public class ImageMetadata
|
||||
{
|
||||
public List<ulong> playerIds { get; set; } = new();
|
||||
public SavedImageTypeEnum savedImageType { get; set; }
|
||||
public long roomId { get; set; }
|
||||
public ImageAccessibility accessibility { get; set; }
|
||||
public string? description { get; set; }
|
||||
}
|
||||
|
||||
public class ModifyDescriptionRequest
|
||||
{
|
||||
public string ImageName { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class Cheer
|
||||
{
|
||||
[BsonId]
|
||||
public int Id { get; set; }
|
||||
public int SavedImageId { get; set; }
|
||||
public ulong PlayerId { get; set; }
|
||||
}
|
||||
|
||||
public class CheerRequest
|
||||
{
|
||||
public int SavedImageId { get; set; }
|
||||
public bool Cheer { get; set; }
|
||||
}
|
||||
|
||||
public enum SavedImageTypeEnum
|
||||
{
|
||||
None,
|
||||
ShareCamera,
|
||||
OutfitThumbnail,
|
||||
RoomThumbnail,
|
||||
ProfileThumbnail,
|
||||
InventionThumbnail,
|
||||
PlayerEventThumbnail,
|
||||
RoomLoadScreen
|
||||
}
|
||||
|
||||
public enum ImageAccessibility
|
||||
{
|
||||
Private = 0,
|
||||
Public = 1,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using LiteDB;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class InstanceDBClasses
|
||||
{
|
||||
public class InstanceEntry
|
||||
{
|
||||
[BsonId]
|
||||
public long RoomInstanceId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public long SubRoomId { get; set; }
|
||||
public bool IsPrivate { get; set; }
|
||||
public bool IsFull { get; set; }
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
public int MaxCapacity { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string Location { get; set; } = "";
|
||||
public string PhotonRoomId { get; set; } = "";
|
||||
public string PhotonRegion { get; set; } = "us";
|
||||
public string PhotonRegionId { get; set; } = "us";
|
||||
public string DataBlob { get; set; } = "";
|
||||
public string PhotonRealtimeAppId { get; set; } = "";
|
||||
public string PhotonVoiceAppId { get; set; } = "";
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public RoomInstance ToRoomInstance() => new RoomInstance
|
||||
{
|
||||
roomInstanceId = RoomInstanceId,
|
||||
roomId = RoomId,
|
||||
subRoomId = SubRoomId,
|
||||
isPrivate = IsPrivate,
|
||||
isFull = IsFull,
|
||||
maxCapacity = MaxCapacity,
|
||||
Name = Name,
|
||||
location = Location,
|
||||
photonRoomId = PhotonRoomId,
|
||||
photonRegion = PhotonRegion,
|
||||
photonRegionId = PhotonRegionId,
|
||||
dataBlob = DataBlob
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
using LiteDB;
|
||||
using static Vanadium.Classes.DBs.DBClasses.RoomDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class InventionDBClasses
|
||||
{
|
||||
public class Invention
|
||||
{
|
||||
[BsonId]
|
||||
public long InventionId { get; set; }
|
||||
public string ReplicationId { get; set; } = Guid.NewGuid().ToString();
|
||||
public long CreatorPlayerId { get; set; }
|
||||
public long CreationRoomId { get; set; } = 1;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string LongDescription { get; set; } = string.Empty;
|
||||
public string ImageName { get; set; } = string.Empty;
|
||||
public int UgcVersion { get; set; } = 1;
|
||||
public bool ForceCannotPublish { get; set; } = false;
|
||||
public int CurrentVersionNumber { get; set; } = 1;
|
||||
public int LatestVersionNumber { get; set; } = 1;
|
||||
public List<InventionVersion> Versions { get; set; } = new();
|
||||
public InventionVersion? CurrentVersion { get; set; }
|
||||
public RoomAccessibility Accessibility { get; set; } = RoomAccessibility.Private;
|
||||
public bool IsPublished { get; set; } = false;
|
||||
public bool IsFeatured { get; set; } = false;
|
||||
public DateTime ModifiedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? FirstPublishedAt { get; set; } = DateTime.MinValue;
|
||||
public int NumPlayersHaveUsedInRoom { get; set; } = 0;
|
||||
public int NumDownloads { get; set; } = 0;
|
||||
public int CheerCount { get; set; } = 0;
|
||||
public int CreatorPermission { get; set; } = 100;
|
||||
public int GeneralPermission { get; set; } = 100;
|
||||
public bool IsAGInvention { get; set; } = false;
|
||||
public bool IsCertifiedInvention { get; set; } = false;
|
||||
public bool IsRecRoomApproved { get; set; } = false;
|
||||
public int Price { get; set; } = 0;
|
||||
public bool AllowTrial { get; set; } = false;
|
||||
public bool HideFromPlayer { get; set; } = false;
|
||||
public string DisplayMetadataJson { get; set; } = string.Empty;
|
||||
public int inkCost { get; set; } = 1;
|
||||
public List<InventionTag> Tags { get; set; } = new();
|
||||
public List<long> CheeredIds { get; set; } = new();
|
||||
public List<long> DownloadIds { get; set; } = new();
|
||||
public List<long> ReferencedInventions { get; set; } = new();
|
||||
}
|
||||
|
||||
public class InventionVersion
|
||||
{
|
||||
public long InventionId { get; set; }
|
||||
public string ReplicationId { get; set; } = Guid.NewGuid().ToString();
|
||||
public int VersionNumber { get; set; } = 1;
|
||||
public string BlobName { get; set; } = string.Empty;
|
||||
public string BlobHash { get; set; } = string.Empty;
|
||||
public int InstantiationCost { get; set; }
|
||||
public int LightsCost { get; set; }
|
||||
public int ChipsCost { get; set; }
|
||||
public int CloudVariablesCost { get; set; }
|
||||
public int AICost { get; set; }
|
||||
public int inkCost { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class InventionData
|
||||
{
|
||||
public bool AllowTrial { get; set; }
|
||||
public int CheerCount { get; set; }
|
||||
public string CreatedAt { get; set; } = string.Empty;
|
||||
public int CreatorPermission { get; set; }
|
||||
public long CreatorPlayerId { get; set; }
|
||||
public int CurrentVersionNumber { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string LongDescription { get; set; } = string.Empty;
|
||||
public int GeneralPermission { get; set; }
|
||||
public bool HideFromPlayer { get; set; }
|
||||
public string ImageName { get; set; } = string.Empty;
|
||||
public long InventionId { get; set; }
|
||||
public bool IsAGInvention { get; set; }
|
||||
public bool IsCertifiedInvention { get; set; }
|
||||
public bool IsPublished { get; set; }
|
||||
public bool IsFeatured { get; set; }
|
||||
public bool IsRecRoomApproved { get; set; }
|
||||
public string ModifiedAt { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public int NumDownloads { get; set; }
|
||||
public int NumPlayersHaveUsedInRoom { get; set; }
|
||||
public int Price { get; set; }
|
||||
public string ReplicationId { get; set; } = string.Empty;
|
||||
public int UgcVersion { get; set; }
|
||||
public bool ForceCannotPublish { get; set; }
|
||||
public int LatestVersionNumber { get; set; }
|
||||
public List<InventionVersion> Versions { get; set; } = new();
|
||||
public InventionVersion? CurrentVersion { get; set; }
|
||||
public int Accessibility { get; set; }
|
||||
public DateTime? FirstPublishedAt { get; set; } = DateTime.MinValue;
|
||||
public long CreationRoomId { get; set; }
|
||||
public string DisplayMetadataJson { get; set; } = string.Empty;
|
||||
public List<long> ReferencedInventions { get; set; } = new();
|
||||
public List<InventionTag> Tags { get; set; } = new();
|
||||
}
|
||||
|
||||
public class InventionTag
|
||||
{
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
public int Type { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class SaveInventionRequest
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("name")]
|
||||
public string name { get; set; } = string.Empty;
|
||||
[System.Text.Json.Serialization.JsonPropertyName("description")]
|
||||
public string description { get; set; } = string.Empty;
|
||||
[System.Text.Json.Serialization.JsonPropertyName("imageName")]
|
||||
public string imageName { get; set; } = string.Empty;
|
||||
[System.Text.Json.Serialization.JsonPropertyName("instantiationCost")]
|
||||
public int instantiationCost { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("lightsCost")]
|
||||
public int lightsCost { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("chipsCost")]
|
||||
public int chipsCost { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("cloudVariablesCost")]
|
||||
public int cloudVariablesCost { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("aiCost")]
|
||||
public int aiCost { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("inkCost")]
|
||||
public int inkCost { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("creationRoomId")]
|
||||
public long creationRoomId { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("inventionDataFilename")]
|
||||
public string inventionDataFilename { get; set; } = string.Empty;
|
||||
[System.Text.Json.Serialization.JsonPropertyName("referencedInventions")]
|
||||
public List<long> referencedInventions { get; set; } = new();
|
||||
[System.Text.Json.Serialization.JsonPropertyName("creatorAccountRole")]
|
||||
public int creatorAccountRole { get; set; }
|
||||
}
|
||||
|
||||
public class AddVersionRequest
|
||||
{
|
||||
public long inventionId { get; set; }
|
||||
public int instantiationCost { get; set; }
|
||||
public int lightsCost { get; set; }
|
||||
public int chipsCost { get; set; }
|
||||
public int cloudVariablesCost { get; set; }
|
||||
public int aiCost { get; set; }
|
||||
public int inkCost { get; set; }
|
||||
public long creationRoomId { get; set; } = -1;
|
||||
public string? inventionDataFilename { get; set; }
|
||||
public List<long>? referencedInventions { get; set; }
|
||||
}
|
||||
|
||||
public class InventionBatchRequest
|
||||
{
|
||||
public List<long> InventionIds { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SetTagsRequest
|
||||
{
|
||||
public long InventionId { get; set; }
|
||||
public List<string> AutoTags { get; set; } = new();
|
||||
public List<string> CustomTags { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SetTagsResponse
|
||||
{
|
||||
public int Result { get; set; }
|
||||
public List<string> Tags { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DetailsResponse
|
||||
{
|
||||
public List<TagData> Tags { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TagData
|
||||
{
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
public int Type { get; set; }
|
||||
}
|
||||
|
||||
public class PersonalDetailsResponse
|
||||
{
|
||||
public bool IsCheering { get; set; } = false;
|
||||
}
|
||||
|
||||
public enum InventionPermissions // moved to its own enum class
|
||||
{
|
||||
Unassigned = 0,
|
||||
LimitedOneUseOnly = 10,
|
||||
DisallowKeyLock = 15,
|
||||
UseOnly = 20,
|
||||
EditAndSave = 40,
|
||||
Publish = 60,
|
||||
Charge = 80,
|
||||
Unlimited = 100
|
||||
}
|
||||
|
||||
public enum InventionReportCategory
|
||||
{
|
||||
RacismOrDiscriminatoryContent = 0,
|
||||
SexualContent = 1,
|
||||
MisleadingNamePictureOrDescription = 3,
|
||||
Other = 4
|
||||
}
|
||||
|
||||
public class InventionResponse
|
||||
{
|
||||
public int StatusCode { get; set; }
|
||||
public Invention Invention { get; set; }
|
||||
public InventionVersion InventionVersion { get; set; }
|
||||
}
|
||||
|
||||
public class BuyInventionResponse
|
||||
{
|
||||
public InventionResponse InventionResponse { get; set; }
|
||||
public StorefrontsDBClasses.BuyForFreeGiftButtonResponse BalanceUpdateResponse { get; set; }
|
||||
}
|
||||
|
||||
public class ReportInvention
|
||||
{
|
||||
public long InventionId { get; set; }
|
||||
public InventionReportCategory ReportCategory { get; set; }
|
||||
public string? Details { get; set; }
|
||||
}
|
||||
|
||||
public class CheerInventionRequest
|
||||
{
|
||||
public bool Cheer { get; set; }
|
||||
public long InventionId { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class LeaderboardDBClasses
|
||||
{
|
||||
public class LeaderboardEntry
|
||||
{
|
||||
[BsonId]
|
||||
public long EntryId { get; set; }
|
||||
public long PlayerId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public int StatChannel { get; set; }
|
||||
public int Score { get; set; }
|
||||
public int FilterType { get; set; }
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class LeaderboardRow
|
||||
{
|
||||
public long playerId { get; set; }
|
||||
public int rank { get; set; }
|
||||
public int score { get; set; }
|
||||
}
|
||||
|
||||
public class GetRanksRequest
|
||||
{
|
||||
public int FilterType { get; set; }
|
||||
public long PlayerId { get; set; }
|
||||
public int RankEnd { get; set; }
|
||||
public int RankStart { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public bool SortAscending { get; set; }
|
||||
public int StatChannel { get; set; }
|
||||
public int Timeframe { get; set; }
|
||||
}
|
||||
|
||||
public class GetRanksResponse
|
||||
{
|
||||
public List<LeaderboardRow> rows { get; set; } = new();
|
||||
}
|
||||
|
||||
public class GetNearbyScoresRequest : GetRanksRequest
|
||||
{
|
||||
public int WindowSize { get; set; }
|
||||
}
|
||||
|
||||
public class GetNearbyScoresResponse
|
||||
{
|
||||
public List<LeaderboardRow> rows { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CheckAndSetStatRequest
|
||||
{
|
||||
public int CurrentStatValue { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public int StatChannel { get; set; }
|
||||
public int StatValue { get; set; }
|
||||
}
|
||||
|
||||
public class CheckAndSetStatResponse
|
||||
{
|
||||
public string? error { get; set; }
|
||||
public bool success { get; set; }
|
||||
public LeaderboardRow? value { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class MatchDBClasses
|
||||
{
|
||||
public class InviteDTO
|
||||
{
|
||||
public long InviteId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int InviteMode { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class PlayerOnlineDTO
|
||||
{
|
||||
public bool SenderIsMyFavoriteFriend { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class PhotonAccessTokenDBClasses
|
||||
{
|
||||
public class SubroomPermissionSet
|
||||
{
|
||||
[BsonId]
|
||||
public long Id { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public long SubRoomId { get; set; }
|
||||
public List<StoredPermissionEntry> Permissions { get; set; } = new();
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class StoredPermissionEntry
|
||||
{
|
||||
public string Permission { get; set; } = "";
|
||||
public int Role { get; set; }
|
||||
public int Type { get; set; }
|
||||
public bool Override { get; set; }
|
||||
public string Value { get; set; } = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class PlayerDBClasses
|
||||
{
|
||||
public class FullPlayer
|
||||
{
|
||||
[BsonId]
|
||||
public long PlayerId { get; set; }
|
||||
public List<mPlatformID> PlatformIds { get; set; } = new();
|
||||
public List<string>? DeviceIds { get; set; } = new();
|
||||
public string? AuthToken { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public List<PlayerRoles> PlayerRoles { get; set; } = new();
|
||||
public Player? Player { get; set; }
|
||||
public string RefreshToken { get; set; } = "";
|
||||
public DateTime RefreshTokenExpires { get; set; } = new DateTime(9000, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
public class Player
|
||||
{
|
||||
public string? Username { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? DisplayEmoji { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
public int AvailableUsernameChanges { get; set; } = 3;
|
||||
public DateTime? LastUsernameChangeAt { get; set; }
|
||||
public bool NameLocked { get; set; } = false;
|
||||
public bool? IsJunior { get; set; }
|
||||
public bool AvoidJuniors { get; set; } = true;
|
||||
public int Level { get; set; } = 1;
|
||||
public int XP { get; set; } = 0;
|
||||
public int XPToday { get; set; } = 0;
|
||||
public DateTime XPTodayDate { get; set; } = DateTime.MinValue;
|
||||
public DateTime LastXPAwardedAt { get; set; } = DateTime.MinValue;
|
||||
public long CurrentXPRoomId { get; set; } = 0;
|
||||
public Dictionary<long, int> RoomXPEarnedToday { get; set; } = new();
|
||||
public string? ProfileImage { get; set; }
|
||||
public string? BannerImage { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public int PronounFlags { get; set; }
|
||||
public int IdentityFlags { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime LastLoginAt { get; set; }
|
||||
public DateTime? Birthday { get; set; }
|
||||
public CurrentAuthSession? CurrentAuthSession { get; set; } = new CurrentAuthSession();
|
||||
public Reputation Reputation { get; set; } = new Reputation();
|
||||
public List<long> VisitedRooms { get; set; } = new();
|
||||
public List<RoomVisit> RoomVisits { get; set; } = new();
|
||||
public List<long> CheeredRooms { get; set; } = new List<long>();
|
||||
public List<long> FavoritedRooms { get; set; } = new List<long>();
|
||||
public PlayerExtra PlayerExtra { get; set; } = new PlayerExtra();
|
||||
public List<PlayerRelationship> Relationships { get; set; } = new List<PlayerRelationship>();
|
||||
public string? DiscordUserId { get; set; }
|
||||
public string? DiscordUsername { get; set; }
|
||||
public bool DiscordLinked { get; set; } = false;
|
||||
public DateTime? DiscordLinkedAt { get; set; }
|
||||
}
|
||||
|
||||
public class RoomVisit
|
||||
{
|
||||
public long RoomId { get; set; }
|
||||
public DateTime LastVisitedAt { get; set; }
|
||||
}
|
||||
|
||||
public class RoomPlayerData
|
||||
{
|
||||
public long RoomId { get; set; }
|
||||
public string Data { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class PlayerDTOBase
|
||||
{
|
||||
public long accountId { get; set; }
|
||||
public DateTime createdAt { get; set; }
|
||||
public string? displayName { get; set; }
|
||||
public string? displayEmoji { get; set; }
|
||||
public bool? isJunior { get; set; }
|
||||
public int platforms { get; set; }
|
||||
public string? profileImage { get; set; }
|
||||
public string? bannerImage { get; set; }
|
||||
public string? username { get; set; }
|
||||
public int personalPronouns { get; set; }
|
||||
public int identityFlags { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerDTO : PlayerDTOBase { }
|
||||
|
||||
public class PlayerMeDTO : PlayerDTOBase
|
||||
{
|
||||
public int availableUsernameChanges { get; set; } = 3;
|
||||
public DateTime? birthday { get; set; }
|
||||
public string? email { get; set; }
|
||||
public string? phone { get; set; }
|
||||
public Reputation? reputation { get; set; }
|
||||
}
|
||||
|
||||
public class CurrentAuthSession
|
||||
{
|
||||
public string? AuthSigningKey { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerRelationship
|
||||
{
|
||||
public long PlayerId { get; set; }
|
||||
public int Favorited { get; set; } = 0;
|
||||
public int Ignored { get; set; } = 0;
|
||||
public int Muted { get; set; } = 0;
|
||||
public int RelationshipType { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class OwnedEquipmentItem
|
||||
{
|
||||
public string PrefabName { get; set; } = "";
|
||||
public string ModificationGuid { get; set; } = "";
|
||||
public bool Favorited { get; set; } = false;
|
||||
public int PlatformMask { get; set; } = -1;
|
||||
public string FriendlyName { get; set; } = "";
|
||||
public string Tooltip { get; set; } = "";
|
||||
public int Rarity { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class UpdateEquipmentRequest
|
||||
{
|
||||
public bool Favorited { get; set; }
|
||||
public string FriendlyName { get; set; } = "";
|
||||
public string ModificationGuid { get; set; } = "";
|
||||
public string PrefabName { get; set; } = "";
|
||||
public int Rarity { get; set; }
|
||||
public string? Tooltip { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerExtra
|
||||
{
|
||||
public Avatar Avatar { get; set; } = new Avatar();
|
||||
public List<string> AvatarItems { get; set; } = new();
|
||||
public List<SavedOutfit> SavedAvatars { get; set; } = new();
|
||||
public ModerationBlockDetails? ModerationBlockDetails { get; set; } = new ModerationBlockDetails();
|
||||
public List<Setting> Settings { get; set; } = new();
|
||||
public Heartbeat Heartbeat { get; set; } = new Heartbeat();
|
||||
public List<PlayerCurrency> Currencies { get; set; } = new();
|
||||
public List<RoomPlayerData> RoomPlayerData { get; set; } = new();
|
||||
public long DormRoomId { get; set; } = 0;
|
||||
public long PlayerClubId { get; set; } = 0;
|
||||
public int PhotoTaggingSetting { get; set; } = 0;
|
||||
public List<MessageData> Messages { get; set; } = new();
|
||||
public Influencer Influencer { get; set; } = new Influencer();
|
||||
|
||||
// anything below is not in original code sonsaN
|
||||
|
||||
public List<OwnedEquipmentItem> OwnedEquipment { get; set; } = new();
|
||||
public int ImagesUploadedToday { get; set; } = 0;
|
||||
public DateTime ImagesUploadedTodayDate { get; set; } = DateTime.MinValue;
|
||||
public List<RoomDBClasses.PermissionEntry> CurrentPermissions { get; set; } = new();
|
||||
public ToxModData? ToxMod { get; set; }
|
||||
public JuniorStatusData? JuniorStatus { get; set; }
|
||||
public DateTime? RoomChangeRestrictedUntil { get; set; }
|
||||
public PersonalDetails PersonalDetails { get; set; } = new PersonalDetails();
|
||||
public long HomeClubhouseId { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class PlayerCurrency
|
||||
{
|
||||
public int Balance { get; set; }
|
||||
public CurrencyType CurrencyType { get; set; }
|
||||
public BalanceType BalanceType { get; set; }
|
||||
}
|
||||
|
||||
public class Avatar
|
||||
{
|
||||
public List<CustomAvatarItem> CustomAvatarItems { get; set; } = new();
|
||||
public string OutfitSelections { get; set; } = "";
|
||||
public string OutfitSelectionsV2 { get; set; } = "";
|
||||
public string FaceFeatures { get; set; } = "";
|
||||
public string SkinColor { get; set; } = "";
|
||||
public string HairColor { get; set; } = "";
|
||||
public string? CustomizationSettings { get; set; }
|
||||
public int DataVersion { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class OutfitSelection
|
||||
{
|
||||
public string? CustomAvatarItemId { get; set; }
|
||||
public int BodyPart { get; set; }
|
||||
public string? BakedUnityAssetFileName { get; set; }
|
||||
public object? AdditionalConfiguration { get; set; }
|
||||
}
|
||||
|
||||
public class CustomAvatarItem
|
||||
{
|
||||
public int BodyPart { get; set; }
|
||||
public string CustomAvatarItemId { get; set; }
|
||||
}
|
||||
|
||||
public class Heartbeat
|
||||
{
|
||||
public string appVersion { get; set; } = ServerConfig.GameVersion.ToString();
|
||||
public DeviceClasses? deviceClass { get; set; } = DeviceClasses.Unknown;
|
||||
public MatchmakingErrorCode? errorCode { get; set; } = null;
|
||||
public bool isOnline { get; set; } = false;
|
||||
public long playerId { get; set; } = 0;
|
||||
public RoomInstance? roomInstance { get; set; } = null;
|
||||
public StatusVisibility statusVisibility { get; set; } = StatusVisibility.Online;
|
||||
public int vrMovementMode { get; set; } = 0;
|
||||
|
||||
public string? photonAuthToken { get; set; }
|
||||
public string? photonRealtimeAppId { get; set; }
|
||||
public string? photonVoiceAppId { get; set; }
|
||||
public string? photonChatAppId { get; set; }
|
||||
public string? photonRegion { get; set; }
|
||||
public string? photonRoomId { get; set; }
|
||||
public string? voiceConnectionInfo { get; set; }
|
||||
public string? voiceServerId { get; set; }
|
||||
public object? experiments { get; set; }
|
||||
}
|
||||
|
||||
public class Subscriptions
|
||||
{
|
||||
public long playerId { get; set; } = 0;
|
||||
public int subscriberCount { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class RoomInstance
|
||||
{
|
||||
public bool encryptVoiceChat { get; set; }
|
||||
public long clubId { get; set; } = 0;
|
||||
public string? dataBlob { get; set; }
|
||||
public long eventId { get; set; } = 0;
|
||||
public bool isFull { get; set; }
|
||||
public bool isInProgress { get; set; }
|
||||
public bool isPrivate { get; set; }
|
||||
public string location { get; set; }
|
||||
public int maxCapacity { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string photonRegion { get; set; }
|
||||
public string photonRegionId { get; set; }
|
||||
public string photonRoomId { get; set; }
|
||||
public string roomCode { get; set; } = "";
|
||||
public long roomId { get; set; }
|
||||
public long roomInstanceId { get; set; }
|
||||
public RoomInstanceType roomInstanceType { get; set; }
|
||||
public long subRoomId { get; set; }
|
||||
}
|
||||
|
||||
public class Reputation
|
||||
{
|
||||
public long AccountId { get; set; }
|
||||
public bool IsCheerful { get; set; }
|
||||
public double Noteriety { get; set; }
|
||||
public CheerCategory SelectedCheer { get; set; }
|
||||
public int CheerCredit { get; set; } = 20;
|
||||
public int CheerGeneral { get; set; }
|
||||
public int CheerHelpful { get; set; }
|
||||
public int CheerCreative { get; set; }
|
||||
public int CheerGreatHost { get; set; }
|
||||
public int CheerSportsman { get; set; }
|
||||
public int SubscriberCount { get; set; }
|
||||
public int SubscribedCount { get; set; }
|
||||
}
|
||||
|
||||
public class ModerationBlockDetails
|
||||
{
|
||||
public ReportCategory ReportCategory { get; set; } = ReportCategory.Moderator;
|
||||
public int Duration { get; set; } = 0;
|
||||
public long GameSessionId { get; set; } = 0;
|
||||
public bool? IsBan { get; set; } = false;
|
||||
public bool? IsHostKick { get; set; } = false;
|
||||
public bool? IsVoiceModAutoban { get; set; } = false; // wow when did this day finally come 9/1/2026 ts feel like heaven boi
|
||||
public bool? IsDeviceBan { get; set; } = false;
|
||||
public bool? IsWarning { get; set; } = false;
|
||||
public string? Message { get; set; } = "";
|
||||
public ulong? PlayerIdReporter { get; set; } = null;
|
||||
public string? VoteKickReason { get; set; } = null;
|
||||
public DateTime? TimeoutStartedAt { get; set; } = null;
|
||||
public string? AssociatedAccountUsername { get; set; } = null;
|
||||
[JsonIgnore]
|
||||
public long ModerationSetUnixTime { get; set; } = 0;
|
||||
[JsonIgnore]
|
||||
public ulong BannedByPlayerId { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class JuniorStatusData
|
||||
{
|
||||
public string? Reason { get; set; }
|
||||
public string? SetBy { get; set; }
|
||||
public DateTime? SetAt { get; set; }
|
||||
}
|
||||
|
||||
public class ToxModData
|
||||
{
|
||||
public int Strikes { get; set; } = 0;
|
||||
public long ActiveBanExpiresUnixTime { get; set; } = 0;
|
||||
public string? ActiveBanCategory { get; set; }
|
||||
public long LastOffenseUnixTime { get; set; } = 0;
|
||||
public List<ToxModOffense> History { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ToxModOffense
|
||||
{
|
||||
public long UnixTime { get; set; }
|
||||
public string? Category { get; set; }
|
||||
public long DurationSeconds { get; set; }
|
||||
public string? Note { get; set; }
|
||||
public string? IssuedBy { get; set; }
|
||||
}
|
||||
|
||||
// gay
|
||||
public class MessageData
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long FromPlayerId { get; set; }
|
||||
public DateTime SentTime { get; set; }
|
||||
public MessageType Type { get; set; }
|
||||
public string? Data { get; set; }
|
||||
public long? RoomId { get; set; }
|
||||
public long? PlayerEventId { get; set; }
|
||||
}
|
||||
|
||||
public enum MessageType // we dont need another MessageType enum, and WebsocketEvents.cs has more than this because this is from a 2022 dump
|
||||
{
|
||||
GameInvite,
|
||||
GameInviteDeclined,
|
||||
GameJoinFailed,
|
||||
PartyActivitySwitch,
|
||||
FriendInvite,
|
||||
VoteToKick,
|
||||
GameInviteV2,
|
||||
PartyActivitySwitchV2,
|
||||
RequestGameInvite = 10,
|
||||
RequestGameInviteDeclined,
|
||||
FriendStatusOnline = 20,
|
||||
TextMessage = 30,
|
||||
FriendRequestAccepted = 40,
|
||||
PlayerCheer = 50,
|
||||
PlayerCheerAnonymous,
|
||||
RoomCoOwnerAdded = 60,
|
||||
RoomCoOwnerRemoved,
|
||||
RoomCoOwnerInvited,
|
||||
CreatorPublishedNewRoom = 70,
|
||||
PlayerAttendingEvent = 80,
|
||||
PlayerEventInvitation,
|
||||
GroupInvitation = 90,
|
||||
PlayerJoinedGroup,
|
||||
CoachMessage = 100
|
||||
}
|
||||
// gay
|
||||
|
||||
public class Setting
|
||||
{
|
||||
public required string Key { get; set; }
|
||||
public required string Value { get; set; }
|
||||
}
|
||||
|
||||
public class mPlatformID
|
||||
{
|
||||
public Platforms Platform { get; set; }
|
||||
public string PlatformId { get; set; }
|
||||
}
|
||||
|
||||
public class CachedLogins
|
||||
{
|
||||
public Platforms platform { get; set; }
|
||||
public string? platformId { get; set; }
|
||||
public long accountId { get; set; }
|
||||
public DateTime? lastLoginTime { get; set; }
|
||||
public bool requirePassword { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerProgressionDTO
|
||||
{
|
||||
public long PlayerId { get; set; }
|
||||
public int Level { get; set; } = 1;
|
||||
public int XP { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class PersonalDetails //invention
|
||||
{
|
||||
public bool? IsCheering { get; set; } = false;
|
||||
public List<long> inventionsCheered { get; set; } = new List<long>();
|
||||
}
|
||||
|
||||
public class OutfitLegacyData
|
||||
{
|
||||
public string? SelectionsV1 { get; set; }
|
||||
public string? SelectionsV2 { get; set; }
|
||||
public string? FaceFeatures { get; set; }
|
||||
public string? SkinColor { get; set; }
|
||||
public string? HairColor { get; set; }
|
||||
}
|
||||
|
||||
public class SavedOutfitRequest
|
||||
{
|
||||
public int DataVersion { get; set; }
|
||||
public OutfitLegacyData? LegacyData { get; set; }
|
||||
public string? CustomizationSettings { get; set; }
|
||||
public List<OutfitSelection>? Selections { get; set; }
|
||||
public int Slot { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public int Accessibility { get; set; }
|
||||
public string? ThumbnailFileName { get; set; }
|
||||
}
|
||||
|
||||
public class SavedOutfit : Avatar // 2023 & under - discontinued now
|
||||
{
|
||||
public int Slot { get; set; }
|
||||
public string? PreviewImageName { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public int Accessibility { get; set; } = 0;
|
||||
public string? ThumbnailFileName { get; set; }
|
||||
public List<OutfitSelection>? Selections { get; set; }
|
||||
}
|
||||
|
||||
public class Influencer
|
||||
{
|
||||
public string CreatorCode { get; set; } = "";
|
||||
public bool IsInfluencer { get; set; } = false;
|
||||
public long SupportingInfluencer { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class DeleteMessagesRequest
|
||||
{
|
||||
public List<long> MessageIds { get; set; } = new();
|
||||
}
|
||||
|
||||
public class Rep
|
||||
{
|
||||
public long AccountId { get; set; }
|
||||
public bool IsCheerful { get; set; }
|
||||
public int Noteriety { get; set; }
|
||||
public CheerCategory SelectedCheer { get; set; }
|
||||
public int CheerCredit { get; set; }
|
||||
public int CheerGeneral { get; set; }
|
||||
public int CheerHelpful { get; set; }
|
||||
public int CheerCreative { get; set; }
|
||||
public int CheerGreatHost { get; set; }
|
||||
public int CheerSportsman { get; set; }
|
||||
}
|
||||
|
||||
public class SetPlayerPhotoTaggingSettingRequest
|
||||
{
|
||||
public PlayerPhotoTaggingSetting Setting { get; set; }
|
||||
}
|
||||
|
||||
public class ImageMetadata
|
||||
{
|
||||
public long? roomId { get; set; }
|
||||
public string? description { get; set; }
|
||||
public int accessibility { get; set; }
|
||||
public List<long>? playerIds { get; set; }
|
||||
public SavedImageTypeEnum savedImageType { get; set; }
|
||||
}
|
||||
|
||||
public class SavedAvatarRequest
|
||||
{
|
||||
public int Slot { get; set; }
|
||||
public string? OutfitSelections { get; set; }
|
||||
public string? OutfitSelectionsV2 { get; set; }
|
||||
public string? FaceFeatures { get; set; }
|
||||
public string? SkinColor { get; set; }
|
||||
public string? HairColor { get; set; }
|
||||
public string? PreviewImageName { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public List<object>? CustomAvatarItems { get; set; }
|
||||
}
|
||||
|
||||
public enum PlayerPhotoTaggingSetting
|
||||
{
|
||||
Anyone,
|
||||
Friends,
|
||||
NoOne
|
||||
}
|
||||
|
||||
public enum SavedImageTypeEnum
|
||||
{
|
||||
General = 0,
|
||||
OutfitThumbnail = 1
|
||||
}
|
||||
|
||||
public enum Platforms
|
||||
{
|
||||
All = -1,
|
||||
Steam,
|
||||
Oculus,
|
||||
PlayStation,
|
||||
Xbox,
|
||||
RecNet,
|
||||
IOS,
|
||||
GooglePlay,
|
||||
Standalone,
|
||||
Pico,
|
||||
Switch
|
||||
}
|
||||
|
||||
public enum PlayerRoles
|
||||
{
|
||||
Screenshare,
|
||||
Moderator,
|
||||
Developer,
|
||||
Keepsake,
|
||||
betastudio,
|
||||
limitsv2,
|
||||
influencer,
|
||||
gameClient
|
||||
}
|
||||
|
||||
public enum ReportCategory
|
||||
{
|
||||
Moderator = -1,
|
||||
Unknown,
|
||||
DEPRECATED_MicrophoneAbuse,
|
||||
Harassment,
|
||||
Cheating,
|
||||
DEPRECATED_ImmatureBehavior,
|
||||
AFK,
|
||||
Misc,
|
||||
Underage,
|
||||
VoteKick = 10,
|
||||
MisleadingPurchases,
|
||||
CoC_Underage = 100,
|
||||
CoC_Sexual,
|
||||
CoC_Discrimination,
|
||||
CoC_Trolling,
|
||||
CoC_NameOrProfile,
|
||||
InappropriateClothing = 200,
|
||||
IssuingInaccurateReports = 1000
|
||||
}
|
||||
|
||||
public enum CheerCategory
|
||||
{
|
||||
None = -1,
|
||||
General = 0,
|
||||
Helpful = 10,
|
||||
Sportmanship = 20,
|
||||
GreatHost = 30,
|
||||
Creative = 40,
|
||||
RecRoomDeveloper = 9000
|
||||
}
|
||||
|
||||
public enum MatchmakingErrorCode
|
||||
{
|
||||
UnknownError = -1,
|
||||
Success = 0,
|
||||
NoSuchGame = 1,
|
||||
PlayerNotOnline = 2,
|
||||
InsufficientSpace = 3,
|
||||
EventNotStarted = 4,
|
||||
EventAlreadyFinished = 5,
|
||||
BlockedFromRoom = 7,
|
||||
JuniorNotAllowed = 11,
|
||||
Banned = 12,
|
||||
AlreadyInBestInstance = 13,
|
||||
InsufficientRelationship = 14,
|
||||
UpdateRequired = 16,
|
||||
AlreadyInTargetInstance,
|
||||
UGCNotAllowed = 19,
|
||||
NoSuchRoom = 20,
|
||||
RoomIsNotActive = 22,
|
||||
RoomBlockedByCreator = 23,
|
||||
RoomIsPrivate = 25,
|
||||
RoomInstanceIsPrivate = 26,
|
||||
DeviceClassNotSupported = 30,
|
||||
DeviceClassNotSupportedByRoomOwner = 31,
|
||||
MovementModeNotSupportedByRoomOwner = 32,
|
||||
EventIsPrivate = 35,
|
||||
RoomInviteExpired = 40,
|
||||
NoAvailableRegion = 45,
|
||||
NotorietyTooPoor = 50,
|
||||
BannedFromRoom = 55,
|
||||
NoSuchRoomPlaylist = 60,
|
||||
RoomPlaylistIsNotActive,
|
||||
RoomPlaylistIsPrivate,
|
||||
NoSuchClub = 70,
|
||||
ClubHasNoClubhouse = 71,
|
||||
ClubIsNotActive = 73,
|
||||
NotAMemberOfClub = 74,
|
||||
BannedFromClub = 75,
|
||||
InstanceJoinNotPermitted = 76,
|
||||
LevelTooLow = 77,
|
||||
DeveloperOnly = 81,
|
||||
MetaJuniorAccountRestriction = 83,
|
||||
AccountDoesNotExist = 85
|
||||
}
|
||||
|
||||
public enum DeviceClasses
|
||||
{
|
||||
Unknown,
|
||||
VR,
|
||||
Screen,
|
||||
Mobile,
|
||||
VRLow,
|
||||
Quest2
|
||||
}
|
||||
|
||||
public enum CurrencyType
|
||||
{
|
||||
Invalid,
|
||||
LaserTagTickets,
|
||||
RecCenterTokens = 2,
|
||||
LostSkullsGold = 100,
|
||||
DraculaSilver,
|
||||
RecRoyale_Season1 = 200,
|
||||
RoomCurrency = 300
|
||||
}
|
||||
|
||||
public enum BalanceType
|
||||
{
|
||||
NonPurchasedNotUsableInP2P = -2,
|
||||
NonPurchasedDefault,
|
||||
SteamPurchased,
|
||||
OculusPurchased,
|
||||
PlayStationPurchased,
|
||||
MicrosoftPurchased,
|
||||
IOSPurchased = 5,
|
||||
GooglePlayPurchased,
|
||||
PlayStationNonPurchasedP2P = 100,
|
||||
NonPlayStationNonPurchasedP2P,
|
||||
NonPurchasedEarnedByP2P = 1000
|
||||
}
|
||||
|
||||
public enum StatusVisibility
|
||||
{
|
||||
Online,
|
||||
Away,
|
||||
Offline,
|
||||
Unknown = 100
|
||||
}
|
||||
|
||||
public enum RoomInstanceType
|
||||
{
|
||||
Public,
|
||||
Private,
|
||||
Dormroom,
|
||||
Event,
|
||||
Meetup,
|
||||
Clubhouse
|
||||
}
|
||||
|
||||
public enum HileType
|
||||
{
|
||||
Obscured,
|
||||
Time,
|
||||
Inject,
|
||||
GiftCount,
|
||||
Engine,
|
||||
UnknownDll,
|
||||
ImageSignature,
|
||||
AvatarHack,
|
||||
NetworkCertificatePublicKey = 100,
|
||||
NetworkCertificateIssuer,
|
||||
NetworkCertificateMissing,
|
||||
NetworkCertificateMismatch,
|
||||
AutosaveChecksumMismatch = 150,
|
||||
AutosaveSubRoomIdMismatch,
|
||||
AutosaveChecksumException,
|
||||
Photon_MissingHash = 200,
|
||||
Photon_CorruptHash,
|
||||
Photon_DifferentHash = 203,
|
||||
AppData_Runtime_LengthMismatch = 300,
|
||||
AppData_Runtime_LastWriteTimeMismatch,
|
||||
AppData_Runtime_FileModified,
|
||||
AppData_Boot_InvalidSignature = 310,
|
||||
AppData_Boot_UnableToVerifySignatures,
|
||||
Config_MissingHash = 320,
|
||||
Config_DifferentHash,
|
||||
Photon_InstantiateTool = 400,
|
||||
Memory_Hash_Mismatch = 500,
|
||||
Driver_Invalid_Signature = 600
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class RRPlus
|
||||
{
|
||||
public class SubscriptionResponse
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public Subscription subscription { get; set; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public ulong? platformAccountSubscribedPlayerId { get; set; }
|
||||
}
|
||||
|
||||
public class Subscription
|
||||
{
|
||||
[JsonPropertyOrder(1)] public int subscriptionId { get; set; }
|
||||
[JsonPropertyOrder(2)] public ulong recNetPlayerId { get; set; }
|
||||
[JsonPropertyOrder(3)] public int platformType { get; set; }
|
||||
[JsonPropertyOrder(4)] public string platformId { get; set; }
|
||||
[JsonPropertyOrder(5)] public string platformPurchaseId { get; set; }
|
||||
[JsonPropertyOrder(6)] public object? @event { get; set; }
|
||||
[JsonPropertyOrder(7)] public int type { get; set; }
|
||||
[JsonPropertyOrder(8)] public int level { get; set; }
|
||||
[JsonPropertyOrder(9)] public int period { get; set; }
|
||||
[JsonPropertyOrder(10)] public DateTime expirationDate { get; set; }
|
||||
[JsonPropertyOrder(11)] public bool isAutoRenewing { get; set; }
|
||||
[JsonPropertyOrder(12)] public DateTime createdAt { get; set; }
|
||||
[JsonPropertyOrder(13)] public DateTime modifiedAt { get; set; }
|
||||
}
|
||||
|
||||
public enum SubscriptionLevel
|
||||
{
|
||||
Gold,
|
||||
Platinum,
|
||||
}
|
||||
|
||||
public enum SubscriptionPeriod
|
||||
{
|
||||
Month,
|
||||
Year,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class ReportsDBClasses
|
||||
{
|
||||
public class BugReport
|
||||
{
|
||||
[BsonId]
|
||||
public string ReportId { get; set; }
|
||||
public long PlayerId { get; set; }
|
||||
public string TriggerId { get; set; }
|
||||
public int TriggerType { get; set; }
|
||||
public DateTime TriggeredAt { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string TraceLogPath { get; set; }
|
||||
public string GameScreenshotPath { get; set; }
|
||||
public bool IsCompleted { get; set; } = false;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class ReportInitializationRequest
|
||||
{
|
||||
public ClientInfo Client { get; set; }
|
||||
public string ReportId { get; set; }
|
||||
}
|
||||
|
||||
public class ClientInfo
|
||||
{
|
||||
public string TriggerId { get; set; }
|
||||
public int TriggerType { get; set; }
|
||||
public DateTime TriggeredAt { get; set; }
|
||||
}
|
||||
|
||||
public class ReportUserInfoRequest
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class RoomCommentsDBClasses
|
||||
{
|
||||
public class RoomComment
|
||||
{
|
||||
[BsonId]
|
||||
public long commentId { get; set; }
|
||||
public long subRoomId { get; set; }
|
||||
public long roomId { get; set; }
|
||||
public long accountId { get; set; }
|
||||
public DateTime createdAt { get; set; } = DateTime.UtcNow;
|
||||
public float positionX { get; set; }
|
||||
public float positionY { get; set; }
|
||||
public float positionZ { get; set; }
|
||||
public string message { get; set; } = "";
|
||||
public int style { get; set; } = 0;
|
||||
public bool unread { get; set; } = true;
|
||||
}
|
||||
|
||||
public class CreateCommentRequest
|
||||
{
|
||||
public string message { get; set; } = "";
|
||||
public long subRoomId { get; set; }
|
||||
public int style { get; set; } = 0;
|
||||
public float positionX { get; set; }
|
||||
public float positionY { get; set; }
|
||||
public float positionZ { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using LiteDB;
|
||||
using System;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class RoomConsumablesDBClasses
|
||||
{
|
||||
/// <summary>
|
||||
/// A creator-made consumable for sale in a room's shop.
|
||||
/// The client identifies these purely by GUID, so the GUID is the primary key.
|
||||
/// </summary>
|
||||
public class RoomConsumable
|
||||
{
|
||||
[BsonId]
|
||||
public Guid RoomConsumableId { get; set; } = Guid.NewGuid();
|
||||
public long RoomId { get; set; }
|
||||
public long CreatorPlayerId { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string? Description { get; set; }
|
||||
public string? ImageName { get; set; }
|
||||
public int Price { get; set; }
|
||||
|
||||
/// <summary>Room currency the item is priced in; null means it is priced in tokens.</summary>
|
||||
public Guid? CurrencyId { get; set; }
|
||||
|
||||
/// <summary>Soft delete — players may still hold deleted items in inventory.</summary>
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime ModifiedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>One player's stack of one consumable.</summary>
|
||||
public class RoomConsumableOwnership
|
||||
{
|
||||
[BsonId]
|
||||
public long Id { get; set; }
|
||||
public long PlayerId { get; set; }
|
||||
public Guid RoomConsumableId { get; set; }
|
||||
public int Count { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Client-generated token guarding consume/purchase races. The client adopts the
|
||||
/// NewConcurrencyCode it sent regardless of what the response echoes back, so this
|
||||
/// must be stored verbatim or the next consume fails with ConcurrencyCodeMismatch.
|
||||
/// </summary>
|
||||
public Guid ConcurrencyCode { get; set; } = Guid.NewGuid();
|
||||
public DateTime ModifiedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A player's balance of one room currency. Vanadium has no room-currency registry yet
|
||||
/// (see RoomConsumablesController.PurchaseCurrency), so this exists to make
|
||||
/// currency-priced purchases work once one is added.
|
||||
/// </summary>
|
||||
public class RoomCurrencyBalance
|
||||
{
|
||||
[BsonId]
|
||||
public long Id { get; set; }
|
||||
public long PlayerId { get; set; }
|
||||
public Guid CurrencyId { get; set; }
|
||||
public int Balance { get; set; }
|
||||
public DateTime ModifiedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using LiteDB;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class RoomDBClasses
|
||||
{
|
||||
public class Room
|
||||
{
|
||||
[BsonId]
|
||||
public long RoomId { get; set; }
|
||||
public bool IsDorm { get; set; }
|
||||
public int MaxPlayerCalculationMode { get; set; }
|
||||
public int MaxPlayers { get; set; }
|
||||
public bool CloningAllowed { get; set; }
|
||||
public bool DisableMicAutoMute { get; set; }
|
||||
public bool DisableRoomComments { get; set; }
|
||||
public bool EncryptVoiceChat { get; set; }
|
||||
public bool ToxmodEnabled { get; set; }
|
||||
public bool LoadScreenLocked { get; set; }
|
||||
public int PersistenceVersion { get; set; }
|
||||
public int? UgcSubVersion { get; set; }
|
||||
public int? UgcVersion { get; set; }
|
||||
public bool AutoLocalizeRoom { get; set; }
|
||||
public bool IsDeveloperOwned { get; set; }
|
||||
public bool IsRRO { get; set; }
|
||||
public bool IsRecRoomApproved { get; set; }
|
||||
public bool IsJuniorCreated { get; set; }
|
||||
public bool IsPlacePlay { get; set; }
|
||||
public bool ExcludeFromLists { get; set; }
|
||||
public bool ExcludeFromSearch { get; set; }
|
||||
public string Name { get; set; }
|
||||
//public string? DisplayName { get; set; } // remove this already, we do NOT need this gng
|
||||
public string? Description { get; set; }
|
||||
public string ImageName { get; set; }
|
||||
public WarningMaskType WarningMask { get; set; }
|
||||
public string? CustomWarning { get; set; }
|
||||
public long CreatorAccountId { get; set; }
|
||||
public RoomState? State { get; set; }
|
||||
public RoomAccessibility Accessibility { get; set; }
|
||||
public bool SupportsLevelVoting { get; set; }
|
||||
public bool SupportsScreens { get; set; }
|
||||
public bool SupportsWalkVR { get; set; }
|
||||
public bool SupportsTeleportVR { get; set; }
|
||||
public bool SupportsVRLow { get; set; }
|
||||
public bool SupportsQuest2 { get; set; }
|
||||
public bool SupportsMobile { get; set; }
|
||||
public bool SupportsJuniors { get; set; }
|
||||
public int MinLevel { get; set; }
|
||||
public int? MinUgcSubVersion { get; set; }
|
||||
public int AgeRating { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime PublishedAt { get; set; }
|
||||
public DateTime? BecameRRStudioRoomAt { get; set; }
|
||||
public Stats Stats { get; set; } = new Stats();
|
||||
public string? RankedEntityId { get; set; }
|
||||
public object? RankingContext { get; set; }
|
||||
public string? DataBlob { get; set; }
|
||||
public string? CurrentSnapshotId { get; set; }
|
||||
public bool NeedsSnapshotId { get; set; }
|
||||
public bool ShouldAutoStageSaves { get; set; }
|
||||
public List<SubRooms> SubRooms { get; set; } = new List<SubRooms>();
|
||||
public List<Roles> Roles { get; set; } = new List<Roles>();
|
||||
public List<Tags> Tags { get; set; } = new List<Tags>();
|
||||
public List<string> PromoImages { get; set; } = new List<string>();
|
||||
public List<PromoExternalContent> PromoExternalContent { get; set; } = new List<PromoExternalContent>();
|
||||
public List<LoadScreens> LoadScreens { get; set; } = new List<LoadScreens>();
|
||||
public List<string> RestrictedCircuitsAllowListNames { get; set; } = new List<string>();
|
||||
public LocalizationContext? LocalizationContext { get; set; }
|
||||
public RoomBanDetails? RoomBanDetails { get; set; }
|
||||
public bool ExperienceEnabled { get; set; }
|
||||
public int ExperienceDailyLimit { get; set; }
|
||||
}
|
||||
|
||||
public class SubRooms
|
||||
{
|
||||
public long SubRoomId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string? DataBlob { get; set; }
|
||||
public bool IsSandbox { get; set; }
|
||||
public int MaxPlayers { get; set; }
|
||||
public RoomAccessibility Accessibility { get; set; }
|
||||
public string UnitySceneId { get; set; }
|
||||
public long SavedByAccountId { get; set; }
|
||||
public bool ShouldAutoStageSaves { get; set; }
|
||||
public long? StagedSubRoomDataSaveId { get; set; }
|
||||
public int LastModeratedSaveModerationState { get; set; }
|
||||
public long? CreatorAccountId { get; set; }
|
||||
public currentSave? CurrentSave { get; set; }
|
||||
public List<PermissionEntry> Permissions { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PermissionEntry
|
||||
{
|
||||
public string Permission { get; set; }
|
||||
public Role Role { get; set; }
|
||||
public int Type { get; set; }
|
||||
public bool Override { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class DbCounters
|
||||
{
|
||||
[BsonId]
|
||||
public string Key { get; set; }
|
||||
public long Value { get; set; }
|
||||
}
|
||||
|
||||
public class currentSave
|
||||
{
|
||||
[BsonId]
|
||||
public long SubRoomDataSaveId { get; set; }
|
||||
public long SubRoomId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public string DataBlob { get; set; }
|
||||
public string DataBlobHash { get; set; }
|
||||
public List<string> ReferencedUnityAssetIds { get; set; } = new();
|
||||
public List<string> UnitySubAssets { get; set; } = new();
|
||||
public List<string> ReferencedUnityAssets { get; set; } = new();
|
||||
public string? UnityAssetId { get; set; }
|
||||
public int PersistenceVersion { get; set; }
|
||||
public int OMVersion { get; set; }
|
||||
public int UgcSubVersion { get; set; }
|
||||
public long? SavedByAccountId { get; set; }
|
||||
public int? SavedOnPlatform { get; set; }
|
||||
public int? SavedOnDeviceClass { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public List<string> Tags { get; set; } = new();
|
||||
public int ModerationState { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class Tags
|
||||
{
|
||||
public string Tag { get; set; } = "";
|
||||
public TagType Type { get; set; }
|
||||
}
|
||||
|
||||
public class PromoExternalContent
|
||||
{
|
||||
public PromoExternalContentType Type { get; set; }
|
||||
public required string Reference { get; set; }
|
||||
}
|
||||
|
||||
public class CloneRoomRequest
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class LoadScreens
|
||||
{
|
||||
public string ImageName { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Subtitle { get; set; }
|
||||
}
|
||||
|
||||
public class Stats
|
||||
{
|
||||
public int CheerCount { get; set; } = 0;
|
||||
public int FavoriteCount { get; set; } = 0;
|
||||
public int VisitorCount { get; set; } = 0;
|
||||
public int VisitCount { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class Roles
|
||||
{
|
||||
public long AccountId { get; set; }
|
||||
public Role Role { get; set; }
|
||||
public Role InvitedRole { get; set; }
|
||||
public long? LastChangedByAccountId { get; set; }
|
||||
}
|
||||
|
||||
public class LocalizationContext
|
||||
{
|
||||
public List<string> LocalizedFields { get; set; } = new();
|
||||
public string? Scope { get; set; }
|
||||
public string? TargetLocale { get; set; }
|
||||
}
|
||||
|
||||
public class RoomBanDetails
|
||||
{
|
||||
public long AccountId { get; set; }
|
||||
public DateTime? BanEndTime { get; set; }
|
||||
public DateTime BanStartTime { get; set; }
|
||||
public long BannedByAccountId { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
public int Status { get; set; }
|
||||
public long? UnbannedByAccountId { get; set; }
|
||||
}
|
||||
|
||||
public class RoomEditPermission
|
||||
{
|
||||
public bool CanEditRoom { get; set; }
|
||||
public string Error { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SubroomSaveResults
|
||||
{
|
||||
public List<currentSave> Results { get; set; } = new();
|
||||
public int TotalCount { get; set; }
|
||||
}
|
||||
|
||||
public class Resultslist<T>
|
||||
{
|
||||
public List<T>? Results { get; set; }
|
||||
public long TotalResults { get; set; }
|
||||
}
|
||||
|
||||
public class RoomFileData
|
||||
{
|
||||
public string? Filename { get; set; }
|
||||
public string? Hash { get; set; }
|
||||
public string? OwnershipProof { get; set; }
|
||||
}
|
||||
|
||||
public class RoomExperience
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public long Experience { get; set; }
|
||||
public string ConcurrencyCode { get; set; }
|
||||
}
|
||||
|
||||
public enum Role : byte
|
||||
{
|
||||
None,
|
||||
Banned,
|
||||
Host = 10,
|
||||
Moderator = 20,
|
||||
CoOwner = 30,
|
||||
TemporaryCoOwner,
|
||||
Creator = 255
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum WarningMaskType
|
||||
{
|
||||
None = 0,
|
||||
Scary = 1,
|
||||
Mature = 2,
|
||||
FlashingLights = 4,
|
||||
IntenseMotion = 8,
|
||||
Violence = 16,
|
||||
Custom = 32,
|
||||
Reports = 64
|
||||
}
|
||||
|
||||
public enum RoomAccessibility
|
||||
{
|
||||
Private,
|
||||
Public,
|
||||
Unlisted,
|
||||
Dev_only,
|
||||
Dev_Unlisted
|
||||
}
|
||||
|
||||
public enum RoomState
|
||||
{
|
||||
Active,
|
||||
PendingJunior = 11,
|
||||
Moderation_PendingReview = 100,
|
||||
Moderation_Closed,
|
||||
MarkedForDelete = 1000
|
||||
}
|
||||
|
||||
public enum TagType
|
||||
{
|
||||
General,
|
||||
Auto,
|
||||
AGOnly,
|
||||
Banned
|
||||
}
|
||||
|
||||
public enum PromoExternalContentType
|
||||
{
|
||||
YouTube
|
||||
}
|
||||
|
||||
public enum JoinMode
|
||||
{
|
||||
PublicMatchmaking,
|
||||
PublicNewInstance,
|
||||
PrivateNewInstance
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using LiteDB;
|
||||
using System;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class RoomKeysDBClasses
|
||||
{
|
||||
public class RoomKey
|
||||
{
|
||||
[BsonId]
|
||||
public long RoomKeyId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? ImageName { get; set; }
|
||||
public int Price { get; set; }
|
||||
public string? PurchaseCurrencyId { get; set; }
|
||||
public string ReplicationId { get; set; } = Guid.NewGuid().ToString();
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class RoomKeyOwnership
|
||||
{
|
||||
[BsonId]
|
||||
public long Id { get; set; }
|
||||
public long PlayerId { get; set; }
|
||||
public long RoomKeyId { get; set; }
|
||||
public bool WasPurchased { get; set; }
|
||||
public DateTime AwardedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class AwardBulkRequest
|
||||
{
|
||||
public long PlayerId { get; set; }
|
||||
public long RoomKeyId { get; set; }
|
||||
}
|
||||
|
||||
public class RevokeRequest
|
||||
{
|
||||
public long PlayerId { get; set; }
|
||||
public long RoomKeyId { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
using LiteDB;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs.DBClasses
|
||||
{
|
||||
public class StorefrontsDBClasses
|
||||
{
|
||||
public class StorefrontSaleData
|
||||
{
|
||||
public int DiscountPercent { get; set; }
|
||||
public DateTime SaleStart { get; set; }
|
||||
public DateTime SaleEnd { get; set; }
|
||||
}
|
||||
|
||||
public class StorefrontPrice
|
||||
{
|
||||
public int CurrencyType { get; set; }
|
||||
public int Price { get; set; }
|
||||
public StorefrontSaleData? StorefrontSaleData { get; set; } = null;
|
||||
public int Type { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class GiftDrop
|
||||
{
|
||||
public string AvatarItemDesc { get; set; } = "";
|
||||
public int AvatarItemType { get; set; } = 0;
|
||||
public string ConsumableItemDesc { get; set; } = "";
|
||||
public int Context { get; set; } = 0;
|
||||
public int Currency { get; set; } = 0;
|
||||
public int CurrencyType { get; set; } = 0;
|
||||
public string EquipmentModificationGuid { get; set; } = "";
|
||||
public string EquipmentPrefabName { get; set; } = "";
|
||||
public string FriendlyName { get; set; } = "";
|
||||
public int GiftDropId { get; set; }
|
||||
public bool IsQuery { get; set; } = false;
|
||||
public string ItemSetFriendlyName { get; set; } = "";
|
||||
public int ItemSetId { get; set; } = 0;
|
||||
public int Rarity { get; set; } = 0;
|
||||
public bool SubscribersOnly { get; set; } = false;
|
||||
public string Tooltip { get; set; } = "";
|
||||
public bool Unique { get; set; } = false;
|
||||
}
|
||||
|
||||
public class StoreItem
|
||||
{
|
||||
public GiftDrop? GiftDrop { get; set; }
|
||||
public bool IsFeatured { get; set; } = false;
|
||||
public List<StorefrontPrice> Prices { get; set; } = new();
|
||||
public int PurchasableItemId { get; set; }
|
||||
public List<StorefrontPrice> SubscriberPrices { get; set; } = new();
|
||||
public int Type { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class GiftDropStoreResponse
|
||||
{
|
||||
public DateTime NextUpdate { get; set; }
|
||||
public List<StoreItem> StoreItems { get; set; } = new();
|
||||
public int StorefrontType { get; set; }
|
||||
public int SubscriberDiscountPercent { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class AdCarouselItem
|
||||
{
|
||||
[BsonId]
|
||||
public int AdCarouselItemId { get; set; }
|
||||
public string Description { get; set; } = "";
|
||||
public string ImageName { get; set; } = "";
|
||||
public int? PurchaseReminderId { get; set; } = null;
|
||||
public List<int> PurchasableItemIds { get; set; } = new List<int>();
|
||||
public bool ShowCustomAvatarItemSearch { get; set; } = false;
|
||||
public string Title { get; set; } = "";
|
||||
}
|
||||
|
||||
public class WishlistItem
|
||||
{
|
||||
[BsonId]
|
||||
public string WishlistItemId { get; set; } = Guid.NewGuid().ToString();
|
||||
public long AccountId { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public int PurchasableItemId { get; set; }
|
||||
}
|
||||
|
||||
public class BuyItemRequest
|
||||
{
|
||||
public string? CouponConsumablePlayerMappingId { get; set; } = null;
|
||||
public int CurrencyType { get; set; }
|
||||
public BuyItemGift? Gift { get; set; } = null;
|
||||
public int PurchasableItemId { get; set; }
|
||||
public int RequestedPrice { get; set; }
|
||||
public int StorefrontType { get; set; }
|
||||
}
|
||||
|
||||
public class BuyItemGift
|
||||
{
|
||||
public long RecipientPlayerId { get; set; }
|
||||
public long? ToPlayerId { get; set; }
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
public class TransferableConsumable
|
||||
{
|
||||
[BsonId]
|
||||
public long Id { get; set; }
|
||||
public long PlayerId { get; set; }
|
||||
public string ConsumableItemDesc { get; set; } = "";
|
||||
public int ActiveDurationMinutes { get; set; } = 5;
|
||||
public int Count { get; set; } = 1;
|
||||
public int InitialCount { get; set; } = 0;
|
||||
public bool IsActive { get; set; } = false;
|
||||
public bool IsTransferable { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class TransferableConsumableGroup
|
||||
{
|
||||
public int ActiveDurationMinutes { get; set; }
|
||||
public string ConsumableItemDesc { get; set; } = "";
|
||||
public int Count { get; set; }
|
||||
public List<DateTime> CreatedAts { get; set; } = new();
|
||||
public List<long> Ids { get; set; } = new();
|
||||
public int InitialCount { get; set; } = 0;
|
||||
public bool IsActive { get; set; } = false;
|
||||
public bool IsTransferable { get; set; } = true;
|
||||
}
|
||||
|
||||
public class BuyForFreeGiftButtonRequest
|
||||
{
|
||||
public int Count { get; set; }
|
||||
public string? CouponConsumablePlayerMappingId { get; set; } = null;
|
||||
public long CreatorPlayerId { get; set; }
|
||||
public int PurchasableItemId { get; set; }
|
||||
}
|
||||
|
||||
public class ConsumableItemDTO
|
||||
{
|
||||
public int ActiveDurationMinutes { get; set; }
|
||||
public string ConsumableItemDesc { get; set; } = "";
|
||||
public int Count { get; set; } = 1;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public long Id { get; set; }
|
||||
public int InitialCount { get; set; } = 0;
|
||||
public bool IsActive { get; set; } = false;
|
||||
public bool IsTransferable { get; set; } = true;
|
||||
}
|
||||
|
||||
public class ConsumableBalanceUpdate
|
||||
{
|
||||
public List<ConsumableItemDTO> Data { get; set; } = new();
|
||||
public int UpdateResponse { get; set; } = 2;
|
||||
}
|
||||
|
||||
public class BuyForFreeGiftButtonResponse
|
||||
{
|
||||
public int Balance { get; set; }
|
||||
public int BalanceType { get; set; } = -2;
|
||||
public List<ConsumableBalanceUpdate> BalanceUpdates { get; set; } = new();
|
||||
public int CurrencyType { get; set; } = 2;
|
||||
}
|
||||
|
||||
public class BalanceUpdate
|
||||
{
|
||||
public List<GiftsDBClasses.GiftDTO> Data { get; set; } = new();
|
||||
public int UpdateResponse { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class BuyItemResponse
|
||||
{
|
||||
public int Balance { get; set; }
|
||||
public int BalanceType { get; set; } = -2;
|
||||
public List<BalanceUpdate> BalanceUpdates { get; set; } = new();
|
||||
public int CurrencyType { get; set; }
|
||||
}
|
||||
|
||||
public class GameRewardRequest
|
||||
{
|
||||
public string RewardType { get; set; } = "";
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
public class RewardSelectionRequest
|
||||
{
|
||||
public int RewardSelectionId { get; set; }
|
||||
public int GiftDropId { get; set; }
|
||||
}
|
||||
|
||||
public class RewardSelectionResponse
|
||||
{
|
||||
public string AvatarItemDesc { get; set; } = "";
|
||||
public int AvatarItemType { get; set; } = 0;
|
||||
public string ConsumableItemDesc { get; set; } = "";
|
||||
public int Context { get; set; }
|
||||
public int Currency { get; set; } = 0;
|
||||
public int CurrencyType { get; set; } = 0;
|
||||
public string EquipmentModificationGuid { get; set; } = "";
|
||||
public string EquipmentPrefabName { get; set; } = "";
|
||||
public string FriendlyName { get; set; } = "";
|
||||
public int GiftDropId { get; set; }
|
||||
public bool IsQuery { get; set; } = false;
|
||||
public string ItemSetFriendlyName { get; set; } = "";
|
||||
public int ItemSetId { get; set; } = 1;
|
||||
public int Rarity { get; set; } = 0;
|
||||
public bool SubscribersOnly { get; set; } = false;
|
||||
public string Tooltip { get; set; } = "";
|
||||
public bool Unique { get; set; } = false;
|
||||
}
|
||||
|
||||
public class SuccessResult
|
||||
{
|
||||
public string error { get; set; } = "";
|
||||
public bool success { get; set; } = true;
|
||||
public object? value { get; set; } = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
using System;
|
||||
using LiteDB;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.EventDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class EventDB
|
||||
{
|
||||
public static LiteDatabase EventDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Events.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<PlayerEvent> Events = EventDBFile.GetCollection<PlayerEvent>("PlayerEvents");
|
||||
public static readonly ILiteCollection<PlayerEventRsvp> Responses = EventDBFile.GetCollection<PlayerEventRsvp>("PlayerEventResponses");
|
||||
|
||||
public static void Setup()
|
||||
{
|
||||
Events.EnsureIndex(x => x.CreatorPlayerId);
|
||||
Events.EnsureIndex(x => x.RoomId);
|
||||
Events.EnsureIndex(x => x.StartTime);
|
||||
Responses.EnsureIndex(x => x.PlayerEventId);
|
||||
Responses.EnsureIndex(x => x.PlayerId);
|
||||
}
|
||||
|
||||
private static long GetNextEventId()
|
||||
{
|
||||
if (Events.Count() == 0) return 1;
|
||||
return Events.Max(x => x.PlayerEventId) + 1;
|
||||
}
|
||||
|
||||
private static long GetNextResponseId()
|
||||
{
|
||||
if (Responses.Count() == 0) return 1;
|
||||
return Responses.Max(x => x.PlayerEventResponseId) + 1;
|
||||
}
|
||||
|
||||
public static PlayerEventResponse CreatePlayerEvent(long creatorPlayerId, PlayerEventRequest request)
|
||||
{
|
||||
if (Sanitize.ContainsSwears(request.Name) || Sanitize.ContainsSwears(request.Description))
|
||||
return new PlayerEventResponse { PlayerEvent = null, Result = 14 };
|
||||
try
|
||||
{
|
||||
string imageName = request.ImageName ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(imageName))
|
||||
{
|
||||
var room = RoomDB.Rooms.FindById(request.RoomId);
|
||||
imageName = room?.ImageName ?? string.Empty;
|
||||
}
|
||||
|
||||
var playerEvent = new PlayerEvent
|
||||
{
|
||||
PlayerEventId = GetNextEventId(),
|
||||
CreatorPlayerId = creatorPlayerId,
|
||||
Name = request.Name ?? string.Empty,
|
||||
Description = request.Description ?? string.Empty,
|
||||
Accessibility = request.Accessibility,
|
||||
ClubId = request.ClubId,
|
||||
RoomId = request.RoomId,
|
||||
SubRoomId = request.SubRoomId,
|
||||
StartTime = DateTime.Parse(request.StartTime),
|
||||
EndTime = DateTime.Parse(request.EndTime),
|
||||
ImageName = imageName,
|
||||
State = 0,
|
||||
AttendeeCount = 0,
|
||||
Tags = (request.Tags ?? new List<string>())
|
||||
.Select(t => new PlayerEventTag { Tag = t, Type = 0 })
|
||||
.ToList(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CanRequestBroadcastPermissions = request.CanRequestBroadcastPermissions,
|
||||
DefaultBroadcastPermissions = request.DefaultBroadcastPermissions,
|
||||
IsMultiInstance = request.IsMultiInstance,
|
||||
SupportMultiInstanceRoomChat = request.SupportMultiInstanceRoomChat
|
||||
};
|
||||
|
||||
Events.Insert(playerEvent);
|
||||
|
||||
return new PlayerEventResponse
|
||||
{
|
||||
PlayerEvent = playerEvent,
|
||||
Result = 0
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[EventDB] Error creating player event: {ex.Message}");
|
||||
return new PlayerEventResponse
|
||||
{
|
||||
PlayerEvent = null,
|
||||
Result = -1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static PlayerEventRsvp? SetResponse(long playerEventId, long playerId, int type)
|
||||
{
|
||||
var existing = Responses.FindOne(x => x.PlayerEventId == playerEventId && x.PlayerId == playerId);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Type = type;
|
||||
Responses.Update(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
var response = new PlayerEventRsvp
|
||||
{
|
||||
PlayerEventResponseId = GetNextResponseId(),
|
||||
PlayerEventId = playerEventId,
|
||||
PlayerId = playerId,
|
||||
Type = type,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
Responses.Insert(response);
|
||||
|
||||
if (type == (int)PlayerEventResponseType.Yes)
|
||||
{
|
||||
var ev = Events.FindById(playerEventId);
|
||||
if (ev != null)
|
||||
{
|
||||
ev.AttendeeCount++;
|
||||
Events.Update(ev);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public static PlayerEventRsvp? GetResponse(long playerEventId, long playerId)
|
||||
{
|
||||
return Responses.FindOne(x => x.PlayerEventId == playerEventId && x.PlayerId == playerId);
|
||||
}
|
||||
|
||||
public static List<PlayerEventRsvp> GetResponsesForEvent(long playerEventId)
|
||||
{
|
||||
return Responses.Find(x => x.PlayerEventId == playerEventId).ToList();
|
||||
}
|
||||
|
||||
public static List<PlayerEventRsvp> GetResponsesForPlayer(long playerId)
|
||||
{
|
||||
return Responses.Find(x => x.PlayerId == playerId).ToList();
|
||||
}
|
||||
|
||||
public static List<PlayerEvent> GetPlayerEvents(long creatorPlayerId)
|
||||
{
|
||||
return Events.Find(x => x.CreatorPlayerId == creatorPlayerId).ToList();
|
||||
}
|
||||
|
||||
public static List<PlayerEvent> GetAttendingEvents(long playerId)
|
||||
{
|
||||
var attendingIds = Responses
|
||||
.Find(x => x.PlayerId == playerId && x.Type == (int)PlayerEventResponseType.Yes)
|
||||
.Select(x => x.PlayerEventId)
|
||||
.ToHashSet();
|
||||
|
||||
return Events
|
||||
.FindAll()
|
||||
.Where(e => attendingIds.Contains(e.PlayerEventId) && e.CreatorPlayerId != playerId)
|
||||
.OrderByDescending(e => e.StartTime)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<PlayerEvent> GetHostingEvents(long playerId)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return Events
|
||||
.Find(x => x.CreatorPlayerId == playerId)
|
||||
.Where(e => e.EndTime >= now)
|
||||
.OrderBy(e => e.StartTime)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<PlayerEvent> GetEventsByRoomId(long roomId)
|
||||
{
|
||||
return Events
|
||||
.FindAll()
|
||||
.Where(e => e.RoomId == roomId)
|
||||
.OrderByDescending(e => e.StartTime)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static PlayerEvent? GetPlayerEventById(long eventId)
|
||||
{
|
||||
return Events.FindById(eventId);
|
||||
}
|
||||
|
||||
public static bool DeletePlayerEvent(long eventId, long creatorPlayerId)
|
||||
{
|
||||
var playerEvent = Events.FindById(eventId);
|
||||
if (playerEvent == null || playerEvent.CreatorPlayerId != creatorPlayerId)
|
||||
return false;
|
||||
|
||||
Responses.DeleteMany(x => x.PlayerEventId == eventId);
|
||||
return Events.Delete(eventId);
|
||||
}
|
||||
|
||||
public static PlayerEvent? EditPlayerEvent(long eventId, long editorPlayerId, Action<PlayerEvent> mutate)
|
||||
{
|
||||
var ev = Events.FindById(eventId);
|
||||
if (ev == null || ev.CreatorPlayerId != editorPlayerId)
|
||||
return null;
|
||||
|
||||
mutate(ev);
|
||||
Events.Update(ev);
|
||||
return ev;
|
||||
}
|
||||
|
||||
public static List<PlayerEvent> GetAllEvents()
|
||||
{
|
||||
return Events.FindAll()
|
||||
.OrderByDescending(e => e.StartTime)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<LivePlayerEvent> GetLiveEvents()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return Events.FindAll()
|
||||
.Where(e => e.StartTime <= now && e.EndTime >= now)
|
||||
.OrderByDescending(e => e.AttendeeCount)
|
||||
.Select(e => new LivePlayerEvent
|
||||
{
|
||||
PlayerEventId = e.PlayerEventId,
|
||||
CreatorPlayerId = e.CreatorPlayerId,
|
||||
Name = e.Name,
|
||||
Description = e.Description,
|
||||
Accessibility = e.Accessibility,
|
||||
ClubId = e.ClubId,
|
||||
RoomId = e.RoomId,
|
||||
SubRoomId = e.SubRoomId,
|
||||
StartTime = e.StartTime,
|
||||
EndTime = e.EndTime,
|
||||
ImageName = e.ImageName,
|
||||
State = e.State,
|
||||
AttendeeCount = e.AttendeeCount,
|
||||
CanRequestBroadcastPermissions = e.CanRequestBroadcastPermissions,
|
||||
DefaultBroadcastPermissions = e.DefaultBroadcastPermissions,
|
||||
IsMultiInstance = e.IsMultiInstance,
|
||||
SupportMultiInstanceRoomChat = e.SupportMultiInstanceRoomChat,
|
||||
Tags = e.Tags,
|
||||
IsFull = Sessions.IsInstanceActuallyFull(e.PlayerEventId),
|
||||
PlayerCount = PlayerDB.Players.FindAll()
|
||||
.Count(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.eventId == e.PlayerEventId
|
||||
&& p.Player.PlayerExtra.Heartbeat.isOnline)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<PlayerEvent> SearchEvents(string? query, string? sort, string? scheduleFilter)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var twoWeeks = now.AddDays(14);
|
||||
|
||||
var all = Events.FindAll().AsEnumerable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(scheduleFilter))
|
||||
{
|
||||
var filter = scheduleFilter.ToLowerInvariant();
|
||||
if (filter == "upcoming")
|
||||
all = all.Where(e => e.StartTime > now && e.StartTime <= twoWeeks);
|
||||
else if (filter == "live")
|
||||
all = all.Where(e => e.StartTime <= now && e.EndTime >= now);
|
||||
else if (filter == "past")
|
||||
all = all.Where(e => e.EndTime < now);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
var q = query.Trim();
|
||||
|
||||
if (q.StartsWith("#"))
|
||||
{
|
||||
var tag = q.Substring(1).ToLowerInvariant();
|
||||
all = all.Where(e => e.Tags != null && e.Tags.Any(t => t.Tag.ToLowerInvariant().Contains(tag)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var ql = q.ToLowerInvariant();
|
||||
all = all.Where(e =>
|
||||
(e.Name ?? "").ToLowerInvariant().Contains(ql) ||
|
||||
(e.Description ?? "").ToLowerInvariant().Contains(ql) ||
|
||||
(e.Tags != null && e.Tags.Any(t => t.Tag.ToLowerInvariant().Contains(ql))));
|
||||
}
|
||||
}
|
||||
|
||||
var results = all.ToList();
|
||||
|
||||
var sortKey = sort?.ToLowerInvariant() ?? "";
|
||||
results = sortKey switch
|
||||
{
|
||||
"starttime" => results.OrderBy(e => e.StartTime).ToList(),
|
||||
"endtime" => results.OrderBy(e => e.EndTime).ToList(),
|
||||
"attendance" => results.OrderByDescending(e => e.AttendeeCount).ToList(),
|
||||
"createdat" => results.OrderByDescending(e => e.CreatedAt).ToList(),
|
||||
_ => results.OrderBy(e => e.StartTime).ToList()
|
||||
};
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using LiteDB;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.FriendsDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class FriendsDB
|
||||
{
|
||||
public static LiteDatabase FriendsDBFile =
|
||||
new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Friends.db") + ";Connection=shared");
|
||||
|
||||
private static ILiteCollection<FriendEntry> Col =>
|
||||
FriendsDBFile.GetCollection<FriendEntry>("Friends");
|
||||
|
||||
private static FriendEntry? FindEntry(long playerId, long otherPlayerId)
|
||||
{
|
||||
return Col.FindOne(x =>
|
||||
x.PlayerID == playerId &&
|
||||
x.Relationship.OtherPlayerID == otherPlayerId);
|
||||
}
|
||||
|
||||
private static FriendEntry GetOrCreate(long playerId, long otherPlayerId)
|
||||
{
|
||||
var entry = FindEntry(playerId, otherPlayerId);
|
||||
if (entry != null)
|
||||
return entry;
|
||||
|
||||
entry = new FriendEntry
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
PlayerID = playerId,
|
||||
Relationship = new RelationshipDetail
|
||||
{
|
||||
Id = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
PlayerID = playerId,
|
||||
OtherPlayerID = otherPlayerId,
|
||||
RelationshipType = RelationshipType.None,
|
||||
Favorited = ReciprocalStatus.None,
|
||||
Ignored = ReciprocalStatus.None,
|
||||
Muted = ReciprocalStatus.None
|
||||
}
|
||||
};
|
||||
|
||||
Col.Insert(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static void Update(FriendEntry entry)
|
||||
{
|
||||
Col.Update(entry);
|
||||
}
|
||||
|
||||
public static List<RelationshipDetail> GetRelationships(long playerId)
|
||||
{
|
||||
return Col.Find(x => x.PlayerID == playerId)
|
||||
.Select(x => x.Relationship)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static RelationshipDetail SendFriendRequest(long fromPlayer, long toPlayer)
|
||||
{
|
||||
if (fromPlayer == toPlayer)
|
||||
return new RelationshipDetail { PlayerID = fromPlayer, OtherPlayerID = toPlayer };
|
||||
|
||||
var my = GetOrCreate(fromPlayer, toPlayer);
|
||||
var their = GetOrCreate(toPlayer, fromPlayer);
|
||||
|
||||
if (my.Relationship.RelationshipType == RelationshipType.Friend)
|
||||
return my.Relationship;
|
||||
|
||||
if (their.Relationship.RelationshipType == RelationshipType.Sent ||
|
||||
my.Relationship.RelationshipType == RelationshipType.Received)
|
||||
{
|
||||
my.Relationship.RelationshipType = RelationshipType.Friend;
|
||||
their.Relationship.RelationshipType = RelationshipType.Friend;
|
||||
|
||||
Update(my);
|
||||
Update(their);
|
||||
|
||||
return my.Relationship;
|
||||
}
|
||||
|
||||
my.Relationship.RelationshipType = RelationshipType.Sent;
|
||||
their.Relationship.RelationshipType = RelationshipType.Received;
|
||||
|
||||
Update(my);
|
||||
Update(their);
|
||||
|
||||
return my.Relationship;
|
||||
}
|
||||
|
||||
public static RelationshipDetail? AcceptFriendRequest(long playerId, long otherPlayerId)
|
||||
{
|
||||
var my = GetOrCreate(playerId, otherPlayerId);
|
||||
var their = GetOrCreate(otherPlayerId, playerId);
|
||||
|
||||
if (their.Relationship.RelationshipType != RelationshipType.Sent ||
|
||||
my.Relationship.RelationshipType != RelationshipType.Received)
|
||||
return null;
|
||||
|
||||
my.Relationship.RelationshipType = RelationshipType.Friend;
|
||||
their.Relationship.RelationshipType = RelationshipType.Friend;
|
||||
|
||||
Update(my);
|
||||
Update(their);
|
||||
|
||||
return my.Relationship;
|
||||
}
|
||||
|
||||
public static RelationshipDetail RemoveFriend(long playerA, long playerB)
|
||||
{
|
||||
var a = GetOrCreate(playerA, playerB);
|
||||
var b = GetOrCreate(playerB, playerA);
|
||||
|
||||
a.Relationship.RelationshipType = RelationshipType.None;
|
||||
b.Relationship.RelationshipType = RelationshipType.None;
|
||||
|
||||
Update(a);
|
||||
Update(b);
|
||||
|
||||
return a.Relationship;
|
||||
}
|
||||
|
||||
public static RelationshipDetail UpdateMute(long ownerId, long targetId, ReciprocalStatus status)
|
||||
{
|
||||
var my = GetOrCreate(ownerId, targetId);
|
||||
var their = GetOrCreate(targetId, ownerId);
|
||||
|
||||
my.Relationship.Muted = status;
|
||||
|
||||
if (status == ReciprocalStatus.None)
|
||||
{
|
||||
if (their.Relationship.Muted == ReciprocalStatus.Mutual)
|
||||
their.Relationship.Muted = ReciprocalStatus.Local;
|
||||
else if (their.Relationship.Muted == ReciprocalStatus.Remote)
|
||||
their.Relationship.Muted = ReciprocalStatus.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (their.Relationship.Muted == ReciprocalStatus.Local || their.Relationship.Muted == ReciprocalStatus.Mutual)
|
||||
their.Relationship.Muted = ReciprocalStatus.Mutual;
|
||||
else
|
||||
their.Relationship.Muted = ReciprocalStatus.Remote;
|
||||
}
|
||||
|
||||
Update(my);
|
||||
Update(their);
|
||||
|
||||
return my.Relationship;
|
||||
}
|
||||
|
||||
public static RelationshipDetail UpdateIgnore(long ownerId, long targetId, ReciprocalStatus status)
|
||||
{
|
||||
var my = GetOrCreate(ownerId, targetId);
|
||||
var their = GetOrCreate(targetId, ownerId);
|
||||
|
||||
my.Relationship.Ignored = status;
|
||||
|
||||
if (status == ReciprocalStatus.None)
|
||||
{
|
||||
if (their.Relationship.Ignored == ReciprocalStatus.Mutual)
|
||||
their.Relationship.Ignored = ReciprocalStatus.Local;
|
||||
else if (their.Relationship.Ignored == ReciprocalStatus.Remote)
|
||||
their.Relationship.Ignored = ReciprocalStatus.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (their.Relationship.Ignored == ReciprocalStatus.Local || their.Relationship.Ignored == ReciprocalStatus.Mutual)
|
||||
their.Relationship.Ignored = ReciprocalStatus.Mutual;
|
||||
else
|
||||
their.Relationship.Ignored = ReciprocalStatus.Remote;
|
||||
}
|
||||
|
||||
Update(my);
|
||||
Update(their);
|
||||
|
||||
return my.Relationship;
|
||||
}
|
||||
|
||||
public static RelationshipDetail SetFavorite(long ownerId, long targetId, ReciprocalStatus status)
|
||||
{
|
||||
var rel = GetOrCreate(ownerId, targetId);
|
||||
|
||||
rel.Relationship.Favorited = status;
|
||||
|
||||
Update(rel);
|
||||
|
||||
return rel.Relationship;
|
||||
}
|
||||
|
||||
public static void DeleteFriends(long playerId)
|
||||
{
|
||||
if (playerId == 0)
|
||||
return;
|
||||
|
||||
Col.DeleteMany(x => x.PlayerID == playerId);
|
||||
|
||||
foreach (var rel in Col.Find(x => x.Relationship.OtherPlayerID == playerId))
|
||||
{
|
||||
rel.Relationship.RelationshipType = RelationshipType.None;
|
||||
rel.Relationship.Muted = ReciprocalStatus.None;
|
||||
rel.Relationship.Ignored = ReciprocalStatus.None;
|
||||
rel.Relationship.Favorited = ReciprocalStatus.None;
|
||||
|
||||
Update(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using LiteDB;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using Vanadium.enums;
|
||||
using static Vanadium.Classes.DBs.DBClasses.GiftsDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class GiftsDB
|
||||
{
|
||||
public static LiteDatabase GiftsDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Gifts.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<Gift> Gifts = GiftsDBFile.GetCollection<Gift>("Gifts");
|
||||
|
||||
static GiftsDB()
|
||||
{
|
||||
Gifts.EnsureIndex(x => x.ToPlayerId);
|
||||
Gifts.EnsureIndex(x => x.Consumed);
|
||||
}
|
||||
|
||||
public static Gift CreateGift(
|
||||
long toPlayerId,
|
||||
long fromPlayerId = 1,
|
||||
string avatarItemDesc = "",
|
||||
int avatarItemType = 0,
|
||||
int balanceType = -2,
|
||||
string consumableItemDesc = "",
|
||||
int currency = 0,
|
||||
int currencyType = 0,
|
||||
string equipmentModificationGuid = "",
|
||||
string equipmentPrefabName = "",
|
||||
GiftType giftContext = GiftType.None,
|
||||
int giftRarity = -1,
|
||||
int level = 0,
|
||||
string message = "A gift for you <3",
|
||||
int platform = -1,
|
||||
int platformsToSpawnOn = -1,
|
||||
int xp = 0)
|
||||
{
|
||||
var gift = new Gift
|
||||
{
|
||||
ToPlayerId = toPlayerId,
|
||||
FromPlayerId = fromPlayerId,
|
||||
AvatarItemDesc = avatarItemDesc,
|
||||
AvatarItemType = avatarItemType,
|
||||
BalanceType = balanceType,
|
||||
ConsumableItemDesc = consumableItemDesc,
|
||||
Currency = currency,
|
||||
CurrencyType = currencyType,
|
||||
EquipmentModificationGuid = equipmentModificationGuid,
|
||||
EquipmentPrefabName = equipmentPrefabName,
|
||||
GiftContext = (int)giftContext,
|
||||
GiftRarity = giftRarity,
|
||||
Level = level,
|
||||
Message = message,
|
||||
Platform = platform,
|
||||
PlatformsToSpawnOn = platformsToSpawnOn,
|
||||
Xp = xp,
|
||||
Consumed = false,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
Gifts.Insert(gift);
|
||||
return gift;
|
||||
}
|
||||
|
||||
public static List<GiftDTO> GetUnconsumedGifts(long playerId)
|
||||
{
|
||||
return Gifts
|
||||
.Find(x => x.ToPlayerId == playerId && !x.Consumed)
|
||||
.Select(MapToDTO)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static Gift? GetGiftById(long giftId)
|
||||
{
|
||||
return Gifts.FindById(giftId);
|
||||
}
|
||||
|
||||
public static bool ConsumeGift(long giftId, long playerId)
|
||||
{
|
||||
var gift = Gifts.FindById(giftId);
|
||||
if (gift == null || gift.ToPlayerId != playerId || gift.Consumed)
|
||||
return false;
|
||||
|
||||
gift.Consumed = true;
|
||||
return Gifts.Update(gift);
|
||||
}
|
||||
|
||||
public static GiftDTO MapToDTO(Gift gift)
|
||||
{
|
||||
return new GiftDTO
|
||||
{
|
||||
AvatarItemDesc = gift.AvatarItemDesc,
|
||||
AvatarItemType = gift.AvatarItemType,
|
||||
BalanceType = gift.BalanceType,
|
||||
ConsumableItemDesc = gift.ConsumableItemDesc,
|
||||
Currency = gift.Currency,
|
||||
CurrencyType = gift.CurrencyType,
|
||||
EquipmentModificationGuid = gift.EquipmentModificationGuid,
|
||||
EquipmentPrefabName = gift.EquipmentPrefabName,
|
||||
FromPlayerId = gift.FromPlayerId,
|
||||
GiftContext = gift.GiftContext,
|
||||
GiftRarity = gift.GiftRarity,
|
||||
Id = gift.Id,
|
||||
Level = gift.Level,
|
||||
Message = gift.Message,
|
||||
Platform = gift.Platform,
|
||||
PlatformsToSpawnOn = gift.PlatformsToSpawnOn,
|
||||
Xp = gift.Xp
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using LiteDB;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.HwidDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
/// <summary>
|
||||
/// Machine identities, the accounts seen on them, and the hardware ban list.
|
||||
/// <para>
|
||||
/// A hardware ban and an account ban are separate things here on purpose. An account ban
|
||||
/// stops one person playing on one account. A hardware ban stops a machine opening any
|
||||
/// account at all, which is a heavier tool with a wider blast radius: shared computers,
|
||||
/// households, and internet cafes all put unrelated people behind one hwid. That is why
|
||||
/// banning a machine also bans the accounts linked to it - so a moderator can see the
|
||||
/// consequence in the account list rather than discovering it later - and why every ban
|
||||
/// records who issued it and why.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class HwidDB
|
||||
{
|
||||
public static LiteDatabase DBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Hwid.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<HwidRecord> Records = DBFile.GetCollection<HwidRecord>("Hwids");
|
||||
|
||||
/// <summary>
|
||||
/// Links a machine to an account and returns the banned accounts already on it.
|
||||
/// Unchanged behaviour: this is the ban-evasion path, and it is deliberately separate
|
||||
/// from the hardware ban list below.
|
||||
/// </summary>
|
||||
public static List<long> LinkAndGetBannedSiblings(string hwid, long playerId)
|
||||
{
|
||||
var record = Records.FindById(hwid);
|
||||
if (record == null)
|
||||
{
|
||||
record = new HwidRecord
|
||||
{
|
||||
Hwid = hwid,
|
||||
PlayerIds = new List<long> { playerId },
|
||||
FirstSeen = DateTime.UtcNow,
|
||||
LastSeen = DateTime.UtcNow
|
||||
};
|
||||
Records.Insert(record);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!record.PlayerIds.Contains(playerId))
|
||||
record.PlayerIds.Add(playerId);
|
||||
record.LastSeen = DateTime.UtcNow;
|
||||
Records.Update(record);
|
||||
}
|
||||
|
||||
return record.PlayerIds
|
||||
.Where(id => id != playerId && PlayerDB.IsBanned(id))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static HwidRecord GetRecord(string hwid)
|
||||
{
|
||||
return string.IsNullOrEmpty(hwid) ? null : Records.FindById(hwid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every machine under an active hardware ban, newest first. This is the review
|
||||
/// queue: a ban list nobody can enumerate is a ban list nobody audits.
|
||||
/// </summary>
|
||||
public static List<HwidRecord> AllBanned()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
return Records.FindAll()
|
||||
.Where(r => r.BanIsActive(now))
|
||||
.OrderByDescending(r => r.BannedAt ?? DateTime.MinValue)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>True while an active hardware ban covers this machine.</summary>
|
||||
public static bool IsHwidBanned(string hwid)
|
||||
{
|
||||
var record = GetRecord(hwid);
|
||||
return record != null && record.BanIsActive(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every machine this account has been seen on. Filtered in memory rather than in a
|
||||
/// query: LiteDB does not reliably translate Contains over a stored list, and this
|
||||
/// runs on moderation paths, not on the client check-in path.
|
||||
/// </summary>
|
||||
public static List<HwidRecord> RecordsForPlayer(long playerId)
|
||||
{
|
||||
return Records.FindAll().Where(r => r.PlayerIds != null && r.PlayerIds.Contains(playerId)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bans one machine. Returns the accounts that were banned as a result, which is the
|
||||
/// number a moderator actually needs to see before they decide this was the right
|
||||
/// call - a machine with eleven accounts on it is a different decision from one with
|
||||
/// a single account.
|
||||
/// <para>
|
||||
/// Refuses a machine it has never seen: a ban on an unknown hwid can never match
|
||||
/// anything, and it clutters the list with rows nobody can review.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static List<long> BanHwid(string hwid, string reason, string bannedBy, DateTime? expiresAt, bool banLinkedAccounts = true)
|
||||
{
|
||||
var record = GetRecord(hwid);
|
||||
if (record == null)
|
||||
return null;
|
||||
|
||||
record.IsBanned = true;
|
||||
record.BanReason = string.IsNullOrWhiteSpace(reason) ? "unspecified" : reason.Trim();
|
||||
record.BannedBy = string.IsNullOrWhiteSpace(bannedBy) ? "unknown" : bannedBy.Trim();
|
||||
record.BannedAt = DateTime.UtcNow;
|
||||
record.BanExpiresAt = expiresAt;
|
||||
record.LinkedAtBan = new List<long>(record.PlayerIds);
|
||||
Records.Update(record);
|
||||
|
||||
var bannedAccounts = new List<long>();
|
||||
|
||||
if (banLinkedAccounts)
|
||||
{
|
||||
foreach (var playerId in record.PlayerIds)
|
||||
{
|
||||
if (PlayerDB.IsBanned(playerId))
|
||||
continue;
|
||||
|
||||
if (PlayerDB.ApplyBan(playerId, record.BanReason))
|
||||
bannedAccounts.Add(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"[AntiCheat] HWID banned by {record.BannedBy}: {hwid} ({record.PlayerIds.Count} linked, {bannedAccounts.Count} newly banned) - {record.BanReason}");
|
||||
|
||||
return bannedAccounts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bans every machine an account has been seen on. This is the form a moderator
|
||||
/// reaches for, because they have an account in front of them and not a hash.
|
||||
/// </summary>
|
||||
public static (List<string> Hwids, List<long> Accounts) BanMachinesOfPlayer(
|
||||
long playerId, string reason, string bannedBy, DateTime? expiresAt)
|
||||
{
|
||||
var hwids = new List<string>();
|
||||
var accounts = new List<long>();
|
||||
|
||||
foreach (var record in RecordsForPlayer(playerId))
|
||||
{
|
||||
var banned = BanHwid(record.Hwid, reason, bannedBy, expiresAt);
|
||||
if (banned == null)
|
||||
continue;
|
||||
|
||||
hwids.Add(record.Hwid);
|
||||
|
||||
foreach (var id in banned)
|
||||
{
|
||||
if (!accounts.Contains(id))
|
||||
accounts.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return (hwids, accounts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lifts a hardware ban. Deliberately does not unban the accounts that were banned
|
||||
/// alongside it: some of them were banned for their own behaviour before the machine
|
||||
/// ever came up, and quietly reinstating those would undo moderation nobody asked to
|
||||
/// undo. Accounts are lifted individually, on purpose, by a person.
|
||||
/// </summary>
|
||||
public static bool UnbanHwid(string hwid, string liftedBy)
|
||||
{
|
||||
var record = GetRecord(hwid);
|
||||
if (record == null || !record.IsBanned)
|
||||
return false;
|
||||
|
||||
record.IsBanned = false;
|
||||
record.BanReason = null;
|
||||
record.BannedBy = null;
|
||||
record.BannedAt = null;
|
||||
record.BanExpiresAt = null;
|
||||
Records.Update(record);
|
||||
|
||||
Console.WriteLine($"[AntiCheat] HWID unbanned by {liftedBy}: {hwid}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
using LiteDB;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
public static partial class ImageMetadataDB
|
||||
{
|
||||
private static readonly string DbPath = Path.Combine("Data", "DBs", "Images.db");
|
||||
|
||||
private static readonly Lazy<LiteDatabase> _dbInstance = new(() => new LiteDatabase("Filename=" + DbPath + ";Connection=shared"));
|
||||
public static LiteDatabase GetDb() => _dbInstance.Value;
|
||||
|
||||
public static FullSavedImage InsertImage(FullSavedImage image)
|
||||
{
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<FullSavedImage>("images");
|
||||
col.EnsureIndex(x => x.Id, true);
|
||||
|
||||
if (image.Id == 0)
|
||||
{
|
||||
var last = col.Query()
|
||||
.OrderByDescending(x => x.Id)
|
||||
.Limit(1)
|
||||
.FirstOrDefault();
|
||||
|
||||
image.Id = (last?.Id ?? 0) + 1;
|
||||
}
|
||||
|
||||
col.Insert(image);
|
||||
return image;
|
||||
}
|
||||
|
||||
public static ulong? GetPlayerIdFromImageName(string imageName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(imageName))
|
||||
return null;
|
||||
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<FullSavedImage>("images");
|
||||
|
||||
col.EnsureIndex(x => x.ImageName);
|
||||
|
||||
var record = col.FindOne(x => x.ImageName == imageName);
|
||||
|
||||
return record?.PlayerId;
|
||||
}
|
||||
|
||||
public static List<FullSavedImage> GetRoomImages(
|
||||
ulong playerId,
|
||||
long roomId,
|
||||
int sort = 0,
|
||||
int filter = 0,
|
||||
int take = 50,
|
||||
int skip = 0)
|
||||
{
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<FullSavedImage>("images");
|
||||
|
||||
var query = col.Query()
|
||||
.Where(x => x.RoomId == roomId &&
|
||||
x.Accessibility == ImageAccessibility.Public);
|
||||
|
||||
switch (filter)
|
||||
{
|
||||
case 2:
|
||||
query = query.Where(x => x.PlayerId == playerId);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
query = query.Where(x => x.CheerCount > 0);
|
||||
break;
|
||||
}
|
||||
|
||||
if (sort == 0 && filter == 1)
|
||||
{
|
||||
query = query.OrderByDescending(x => x.CreatedAt);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = sort switch
|
||||
{
|
||||
1 => query.OrderByDescending(x => x.CheerCount),
|
||||
2 => query.OrderByDescending(x => x.CreatedAt),
|
||||
3 => query.OrderByDescending(x => x.CommentCount),
|
||||
_ => query.OrderBy(x => x.CreatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
return query
|
||||
.Skip(skip)
|
||||
.Limit(take)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
public static bool ModifyDescription(string imageName, long playerId, string description)
|
||||
{
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<FullSavedImage>("images");
|
||||
var image = col.FindOne(x => x.ImageName == imageName);
|
||||
|
||||
if (image == null || (long)image.PlayerId != playerId)
|
||||
return false;
|
||||
|
||||
image.Description = description;
|
||||
col.Update(image);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static List<FullSavedImage> GetGlobalImages()
|
||||
{
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<FullSavedImage>("images");
|
||||
|
||||
return col.Query()
|
||||
.Where(x => x.Accessibility == ImageAccessibility.Public)
|
||||
.OrderByDescending(x => x.CheerCount)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<FullSavedImage> GetGlobalImagesPublic(int skip = 0, int take = 10, DateTime? since = null)
|
||||
{
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<FullSavedImage>("images");
|
||||
|
||||
var query = col.Query()
|
||||
.Where(x => x.Accessibility == ImageAccessibility.Public);
|
||||
|
||||
if (since.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.CreatedAt >= since.Value);
|
||||
}
|
||||
|
||||
return query.OrderByDescending(x => x.CheerCount)
|
||||
.Limit(take)
|
||||
.Skip(skip)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<ImagesPlayer> GetPlayerImages(ulong playeridthatrequesting, ulong playerid)
|
||||
{
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<FullSavedImage>("images");
|
||||
|
||||
var query = col.Query().Where(x => x.PlayerId == playerid);
|
||||
|
||||
if (playeridthatrequesting != playerid)
|
||||
{
|
||||
query = query.Where(x => x.Accessibility == ImageAccessibility.Public);
|
||||
}
|
||||
|
||||
return query.ToEnumerable()
|
||||
.Select(MapToImagesPlayer)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static ImagesPlayer MapToImagesPlayer(FullSavedImage x)
|
||||
{
|
||||
return new ImagesPlayer
|
||||
{
|
||||
Accessibility = x.Accessibility,
|
||||
AccessibilityLocked = x.AccessibilityLocked,
|
||||
CheerCount = x.CheerCount,
|
||||
CommentCount = x.CommentCount,
|
||||
CreatedAt = x.CreatedAt,
|
||||
Description = x.Description,
|
||||
ImageName = x.ImageName,
|
||||
PlayerEventId = x.PlayerEventId,
|
||||
PlayerId = x.PlayerId,
|
||||
RoomId = x.RoomId,
|
||||
SavedImageId = x.Id,
|
||||
SavedImageType = x.Type
|
||||
};
|
||||
}
|
||||
|
||||
public static void UpdateImageCheerCount(int savedImageId, int cheerCount)
|
||||
{
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<FullSavedImage>("images");
|
||||
var img = col.FindById(savedImageId);
|
||||
if (img != null)
|
||||
{
|
||||
img.CheerCount = cheerCount;
|
||||
col.Update(img);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ToggleCheer(int savedImageId, ulong playerId, bool cheer)
|
||||
{
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<Cheer>("cheers");
|
||||
|
||||
var existing = col.Find(x => x.SavedImageId == savedImageId).FirstOrDefault(x => x.PlayerId == playerId);
|
||||
|
||||
if (cheer && existing == null)
|
||||
{
|
||||
var last = col.Query().OrderByDescending(x => x.Id).Limit(1).FirstOrDefault(); // fix for bsonid stuff
|
||||
int newId = (last?.Id ?? 0) + 1;
|
||||
|
||||
col.Insert(new Cheer { Id = newId, SavedImageId = savedImageId, PlayerId = playerId });
|
||||
}
|
||||
else if (!cheer && existing != null)
|
||||
{
|
||||
col.Delete(existing.Id);
|
||||
}
|
||||
|
||||
return col.Find(x => x.SavedImageId == savedImageId).Any(x => x.PlayerId == playerId);
|
||||
}
|
||||
|
||||
public static int GetCheerCount(int savedImageId)
|
||||
{
|
||||
var db = GetDb();
|
||||
return db.GetCollection<Cheer>("cheers").Count(x => x.SavedImageId == savedImageId);
|
||||
}
|
||||
|
||||
public static bool HasUserCheered(int savedImageId, ulong playerId)
|
||||
{
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<Cheer>("cheers");
|
||||
try
|
||||
{
|
||||
return col.Find(x => x.SavedImageId == savedImageId).Any(x => x.PlayerId == playerId);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
db.DropCollection("cheers");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static ImageV6 GetImageByName(string name)
|
||||
{
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<FullSavedImage>("images");
|
||||
var record = col.FindOne(x => x.ImageName == name);
|
||||
|
||||
if (record == null) return null;
|
||||
|
||||
return new ImageV6
|
||||
{
|
||||
Accessibility = record.Accessibility,
|
||||
AccessibilityLocked = record.AccessibilityLocked,
|
||||
CheerCount = record.CheerCount,
|
||||
CommentCount = record.CommentCount,
|
||||
CreatedAt = record.CreatedAt,
|
||||
Description = record.Description,
|
||||
Id = record.Id,
|
||||
ImageName = record.ImageName,
|
||||
PlayerEventId = record.PlayerEventId,
|
||||
PlayerId = record.PlayerId,
|
||||
RoomId = record.RoomId,
|
||||
TaggedPlayerIds = record.TaggedPlayerIds,
|
||||
Type = record.Type,
|
||||
};
|
||||
}
|
||||
|
||||
public static void DeleteImagesFromPlayerId(ulong playerId)
|
||||
{
|
||||
var db = GetDb();
|
||||
var imageCol = db.GetCollection<FullSavedImage>("images");
|
||||
var cheerCol = db.GetCollection<Cheer>("cheers");
|
||||
|
||||
var imagesToDelete = imageCol.Query().Where(x => x.PlayerId == playerId).ToList();
|
||||
|
||||
if (imagesToDelete.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var img in imagesToDelete)
|
||||
{
|
||||
imageCol.Delete(img.Id);
|
||||
cheerCol.DeleteMany(c => c.SavedImageId == img.Id);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[Delete] Removed {imagesToDelete.Count} images and their cheers for PlayerId {playerId}");
|
||||
}
|
||||
|
||||
public static List<ImagesPlayer> GetImagesByIds(List<int> ids)
|
||||
{
|
||||
if (ids == null || ids.Count == 0)
|
||||
return new List<ImagesPlayer>();
|
||||
|
||||
var db = GetDb();
|
||||
var col = db.GetCollection<FullSavedImage>("images");
|
||||
|
||||
return col.Query()
|
||||
.Where(x => ids.Contains(x.Id))
|
||||
.ToEnumerable()
|
||||
.Select(MapToImagesPlayer)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static string SaveImageFile(byte[] data, long playerId, bool tripped = false)
|
||||
{
|
||||
string dir = Path.Combine("Data", "Images", playerId.ToString());
|
||||
if (tripped)
|
||||
{
|
||||
dir = Path.Combine("Data", "BadImages", playerId.ToString());
|
||||
}
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
string fileName = $"{Guid.NewGuid()}.jpg";
|
||||
string path = Path.Combine(dir, fileName);
|
||||
|
||||
File.WriteAllBytes(path, data);
|
||||
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
using LiteDB;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.InstanceDBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class InstanceDB
|
||||
{
|
||||
public static readonly LiteDatabase InstanceDBFile =
|
||||
new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Instances.db") + ";Connection=shared");
|
||||
|
||||
private static readonly ILiteCollection<InstanceEntry> Col =
|
||||
InstanceDBFile.GetCollection<InstanceEntry>("Instances");
|
||||
|
||||
static InstanceDB()
|
||||
{
|
||||
Col.EnsureIndex(x => x.RoomId);
|
||||
Col.EnsureIndex(x => x.SubRoomId);
|
||||
Col.EnsureIndex(x => x.IsDeleted);
|
||||
Col.EnsureIndex(x => x.IsPrivate);
|
||||
}
|
||||
|
||||
// DO NOT TOUCH
|
||||
|
||||
private static readonly string[] RealtimeAppIds = new[] // i made these on my own photon dashboard
|
||||
{
|
||||
"616d6ba8-edb1-47f8-a0de-31c8ce426685",
|
||||
"a7c732cd-5cea-4839-b0b3-ba8e9af14b06",
|
||||
"90d94f73-9797-409d-84ab-ed3992177612",
|
||||
"fac97be5-4239-4309-af46-c35bc163fe5d",
|
||||
"550c2912-37e3-4d4c-ae3f-a5f417fcba40",
|
||||
"70229ffe-5049-4814-9b40-fb86ee99387e",
|
||||
"683dc19a-277b-4cc8-81b2-a7ffdfb3a5cc",
|
||||
"530184ee-bd74-4bc0-b8e9-a21c1504f0a2",
|
||||
"efa6fb5c-94ef-4d73-8fac-7fb8e1e66d19",
|
||||
"53a7e346-0fe9-4320-b8d1-791491c5b220"
|
||||
};
|
||||
|
||||
private static readonly string[] VoiceAppIds = new[] // i made these on my own photon dashboard
|
||||
{
|
||||
"b3a7f09a-b167-4ba1-87bb-2603da5a39be",
|
||||
"7e71f40c-d1fa-4e48-8851-981402e630e5",
|
||||
"3e010413-c8c8-48df-95cf-bf7f2bf430e6",
|
||||
"706df054-e107-4c35-94eb-f374bf607b45",
|
||||
"28846001-24c9-4d2a-9d60-44a3cf5af73f",
|
||||
"341b39d4-5c8f-47a4-a22a-dd5e3b9e3dbc",
|
||||
"9e24d9e9-34af-49c4-b490-d22f67913e6d",
|
||||
"f05846da-20a5-449b-a32d-c8a67071dc41",
|
||||
"e24a71b3-cefd-4f51-8e09-c26f9f173e17",
|
||||
"2ee14608-23c2-4d51-85be-baa1c1bc1c54"
|
||||
};
|
||||
|
||||
private static readonly Random _rng = new Random();
|
||||
|
||||
private static string PickLeastUsed(string[] pool, Func<string, int> countUsage)
|
||||
{
|
||||
var counts = pool.Select(id => (id, count: countUsage(id))).ToList();
|
||||
int min = counts.Min(x => x.count);
|
||||
var candidates = counts.Where(x => x.count == min).Select(x => x.id).ToList();
|
||||
return candidates[_rng.Next(candidates.Count)];
|
||||
}
|
||||
|
||||
public static (string realtimeAppId, string voiceAppId) AssignPhotonIds(long roomInstanceId)
|
||||
{
|
||||
var existing = Get(roomInstanceId);
|
||||
if (existing != null && !string.IsNullOrEmpty(existing.PhotonRealtimeAppId) && !string.IsNullOrEmpty(existing.PhotonVoiceAppId))
|
||||
return (existing.PhotonRealtimeAppId, existing.PhotonVoiceAppId);
|
||||
|
||||
var allActive = Col.Find(x => !x.IsDeleted).ToList();
|
||||
|
||||
string realtime = PickLeastUsed(RealtimeAppIds, id =>
|
||||
allActive.Count(e => e.PhotonRealtimeAppId == id));
|
||||
|
||||
string voice = PickLeastUsed(VoiceAppIds, id =>
|
||||
allActive.Count(e => e.PhotonVoiceAppId == id));
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.PhotonRealtimeAppId = realtime;
|
||||
existing.PhotonVoiceAppId = voice;
|
||||
Col.Update(existing);
|
||||
}
|
||||
|
||||
return (realtime, voice);
|
||||
}
|
||||
|
||||
public static InstanceEntry Upsert(RoomInstance ri, bool isPrivate)
|
||||
{
|
||||
bool isNew = Col.FindById(ri.roomInstanceId) == null;
|
||||
var entry = Col.FindById(ri.roomInstanceId) ?? new InstanceEntry { RoomInstanceId = ri.roomInstanceId };
|
||||
entry.RoomId = ri.roomId;
|
||||
entry.SubRoomId = ri.subRoomId;
|
||||
entry.IsPrivate = isPrivate;
|
||||
entry.IsFull = ri.isFull;
|
||||
entry.MaxCapacity = ri.maxCapacity;
|
||||
entry.Name = ri.Name;
|
||||
entry.Location = ri.location;
|
||||
entry.PhotonRoomId = ri.photonRoomId;
|
||||
entry.PhotonRegion = ri.photonRegion ?? "us";
|
||||
entry.PhotonRegionId = ri.photonRegionId ?? "us";
|
||||
entry.DataBlob = ri.dataBlob ?? "";
|
||||
entry.IsDeleted = false;
|
||||
|
||||
if (isNew || string.IsNullOrEmpty(entry.PhotonRealtimeAppId) || string.IsNullOrEmpty(entry.PhotonVoiceAppId))
|
||||
{
|
||||
var allActive = Col.Find(x => !x.IsDeleted && x.RoomInstanceId != ri.roomInstanceId).ToList();
|
||||
|
||||
entry.PhotonRealtimeAppId = PickLeastUsed(RealtimeAppIds, id =>
|
||||
allActive.Count(e => e.PhotonRealtimeAppId == id));
|
||||
|
||||
entry.PhotonVoiceAppId = PickLeastUsed(VoiceAppIds, id =>
|
||||
allActive.Count(e => e.PhotonVoiceAppId == id));
|
||||
}
|
||||
|
||||
Col.Upsert(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public static InstanceEntry? Get(long roomInstanceId)
|
||||
=> Col.FindOne(x => x.RoomInstanceId == roomInstanceId && !x.IsDeleted);
|
||||
|
||||
public static List<InstanceEntry> GetAllForRoom(long roomId, bool? isPrivate = null)
|
||||
{
|
||||
return Col.Find(x =>
|
||||
x.RoomId == roomId &&
|
||||
!x.IsDeleted &&
|
||||
(isPrivate == null || x.IsPrivate == isPrivate))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static InstanceEntry? GetActivePrivate(long roomId, long subRoomId)
|
||||
=> Col.FindOne(x =>
|
||||
x.RoomId == roomId &&
|
||||
x.SubRoomId == subRoomId &&
|
||||
x.IsPrivate &&
|
||||
!x.IsDeleted);
|
||||
|
||||
public static void MarkFull(long roomInstanceId, bool isFull)
|
||||
{
|
||||
var entry = Col.FindById(roomInstanceId);
|
||||
if (entry == null) return;
|
||||
entry.IsFull = isFull;
|
||||
Col.Update(entry);
|
||||
}
|
||||
|
||||
public static void MarkPrivate(long roomInstanceId)
|
||||
{
|
||||
var entry = Col.FindById(roomInstanceId);
|
||||
if (entry == null) return;
|
||||
entry.IsPrivate = true;
|
||||
Col.Update(entry);
|
||||
}
|
||||
|
||||
public static void MarkPublic(long roomInstanceId)
|
||||
{
|
||||
var entry = Col.FindById(roomInstanceId);
|
||||
if (entry == null) return;
|
||||
entry.IsPrivate = false;
|
||||
Col.Update(entry);
|
||||
}
|
||||
|
||||
public static void Delete(long roomInstanceId)
|
||||
{
|
||||
var entry = Col.FindById(roomInstanceId);
|
||||
if (entry == null) return;
|
||||
entry.IsDeleted = true;
|
||||
Col.Update(entry);
|
||||
}
|
||||
|
||||
public static void PruneEmpty()
|
||||
{
|
||||
var all = Col.Find(x => !x.IsDeleted).ToList();
|
||||
foreach (var entry in all)
|
||||
{
|
||||
int count = PlayerDB.Players
|
||||
.FindAll()
|
||||
.Count(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == entry.RoomInstanceId);
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
entry.IsDeleted = true;
|
||||
Col.Update(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int CountPlayers(long roomInstanceId)
|
||||
=> PlayerDB.Players
|
||||
.FindAll()
|
||||
.Count(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId);
|
||||
|
||||
public static bool IsFull(long roomInstanceId)
|
||||
{
|
||||
var entry = Get(roomInstanceId);
|
||||
if (entry == null) return false;
|
||||
return entry.IsFull || CountPlayers(roomInstanceId) >= entry.MaxCapacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,639 @@
|
||||
using LiteDB;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.InventionDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class InventionDB
|
||||
{
|
||||
public static LiteDatabase InventionDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Inventions.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<Invention> Inventions = InventionDBFile.GetCollection<Invention>("Inventions");
|
||||
public static readonly ILiteCollection<InventionVersion> Versions = InventionDBFile.GetCollection<InventionVersion>("Versions");
|
||||
|
||||
public static void Setup()
|
||||
{
|
||||
Inventions.EnsureIndex(x => x.InventionId);
|
||||
Inventions.EnsureIndex(x => x.CreatorPlayerId);
|
||||
Versions.EnsureIndex(x => x.InventionId);
|
||||
}
|
||||
|
||||
public static object MapVersion(InventionVersion v) => new
|
||||
{
|
||||
v.InventionId,
|
||||
v.ReplicationId,
|
||||
v.VersionNumber,
|
||||
v.BlobName,
|
||||
v.BlobHash,
|
||||
v.InstantiationCost,
|
||||
v.LightsCost,
|
||||
v.ChipsCost,
|
||||
v.CloudVariablesCost,
|
||||
v.AICost,
|
||||
v.inkCost,
|
||||
CreatedAt = v.CreatedAt.ToString("O")
|
||||
};
|
||||
|
||||
public static object MapInventionFull(Invention inv)
|
||||
{
|
||||
var resolvedCurrentVersion = inv.CurrentVersion
|
||||
?? (inv.Versions ?? new List<InventionVersion>())
|
||||
.OrderByDescending(v => v.VersionNumber)
|
||||
.FirstOrDefault();
|
||||
|
||||
return new
|
||||
{
|
||||
inv.InventionId,
|
||||
inv.ReplicationId,
|
||||
inv.CreatorPlayerId,
|
||||
inv.Name,
|
||||
inv.Description,
|
||||
inv.LongDescription,
|
||||
inv.ImageName,
|
||||
inv.CurrentVersionNumber,
|
||||
inv.LatestVersionNumber,
|
||||
inv.UgcVersion,
|
||||
inv.ForceCannotPublish,
|
||||
CurrentVersion = resolvedCurrentVersion != null ? MapVersion(resolvedCurrentVersion) : (object?)null,
|
||||
Versions = (inv.Versions ?? new List<InventionVersion>()).Select(v => MapVersion(v)).ToList(),
|
||||
Accessibility = (int)inv.Accessibility,
|
||||
inv.IsPublished,
|
||||
inv.IsFeatured,
|
||||
inv.IsRecRoomApproved,
|
||||
inv.IsAGInvention,
|
||||
inv.IsCertifiedInvention,
|
||||
ModifiedAt = inv.ModifiedAt.ToString("O"),
|
||||
CreatedAt = inv.CreatedAt.ToString("O"),
|
||||
FirstPublishedAt = inv.FirstPublishedAt.HasValue && inv.FirstPublishedAt.Value != DateTime.MinValue
|
||||
? inv.FirstPublishedAt.Value.ToString("O")
|
||||
: (string?)null,
|
||||
inv.CreationRoomId,
|
||||
inv.NumPlayersHaveUsedInRoom,
|
||||
inv.NumDownloads,
|
||||
inv.CheerCount,
|
||||
inv.CreatorPermission,
|
||||
inv.GeneralPermission,
|
||||
inv.Price,
|
||||
inv.AllowTrial,
|
||||
inv.HideFromPlayer,
|
||||
inv.DisplayMetadataJson,
|
||||
ReferencedInventions = inv.ReferencedInventions ?? new List<long>(),
|
||||
Tags = inv.Tags ?? new List<InventionTag>()
|
||||
};
|
||||
}
|
||||
|
||||
public static InventionData MapInventionToData(Invention inv) => new InventionData
|
||||
{
|
||||
AllowTrial = inv.AllowTrial,
|
||||
CheerCount = inv.CheerCount,
|
||||
CreatedAt = inv.CreatedAt.ToString("O"),
|
||||
CreatorPermission = inv.CreatorPermission,
|
||||
CreatorPlayerId = inv.CreatorPlayerId,
|
||||
CurrentVersionNumber = inv.CurrentVersionNumber,
|
||||
Description = inv.Description,
|
||||
GeneralPermission = inv.GeneralPermission,
|
||||
HideFromPlayer = inv.HideFromPlayer,
|
||||
ImageName = inv.ImageName,
|
||||
InventionId = inv.InventionId,
|
||||
IsAGInvention = inv.IsAGInvention,
|
||||
IsCertifiedInvention = inv.IsCertifiedInvention,
|
||||
IsPublished = inv.IsPublished,
|
||||
IsFeatured = inv.IsFeatured,
|
||||
IsRecRoomApproved = inv.IsRecRoomApproved,
|
||||
ModifiedAt = inv.ModifiedAt.ToString("O"),
|
||||
Name = inv.Name,
|
||||
NumDownloads = inv.NumDownloads,
|
||||
NumPlayersHaveUsedInRoom = inv.NumPlayersHaveUsedInRoom,
|
||||
Price = inv.Price,
|
||||
ReplicationId = inv.ReplicationId,
|
||||
LongDescription = inv.LongDescription,
|
||||
UgcVersion = inv.UgcVersion,
|
||||
ForceCannotPublish = inv.ForceCannotPublish,
|
||||
LatestVersionNumber = inv.LatestVersionNumber,
|
||||
Versions = inv.Versions,
|
||||
CurrentVersion = inv.CurrentVersion,
|
||||
Accessibility = (int)inv.Accessibility,
|
||||
FirstPublishedAt = inv.FirstPublishedAt,
|
||||
CreationRoomId = inv.CreationRoomId,
|
||||
DisplayMetadataJson = inv.DisplayMetadataJson,
|
||||
ReferencedInventions = inv.ReferencedInventions ?? new List<long>(),
|
||||
Tags = inv.Tags ?? new List<InventionTag>()
|
||||
};
|
||||
|
||||
public static Invention? GetInvention(long inventionId)
|
||||
{
|
||||
var invention = Inventions.FindById(inventionId);
|
||||
if (invention == null) return null;
|
||||
|
||||
if (invention.CurrentVersion == null)
|
||||
{
|
||||
var fallback = Versions
|
||||
.Find(v => v.InventionId == inventionId)
|
||||
.OrderByDescending(v => v.VersionNumber)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (fallback != null)
|
||||
{
|
||||
invention.CurrentVersion = fallback;
|
||||
invention.CurrentVersionNumber = fallback.VersionNumber;
|
||||
invention.LatestVersionNumber = fallback.VersionNumber;
|
||||
Inventions.Update(invention);
|
||||
}
|
||||
}
|
||||
|
||||
return invention;
|
||||
}
|
||||
|
||||
public static List<Invention> GetInventionsBatch(List<long> inventionIds)
|
||||
{
|
||||
var inventions = Inventions.Find(x => inventionIds.Contains(x.InventionId)).ToList();
|
||||
|
||||
foreach (var invention in inventions.Where(inv => inv.CurrentVersion == null))
|
||||
{
|
||||
var fallback = Versions
|
||||
.Find(v => v.InventionId == invention.InventionId)
|
||||
.OrderByDescending(v => v.VersionNumber)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (fallback != null)
|
||||
{
|
||||
invention.CurrentVersion = fallback;
|
||||
invention.CurrentVersionNumber = fallback.VersionNumber;
|
||||
invention.LatestVersionNumber = fallback.VersionNumber;
|
||||
Inventions.Update(invention);
|
||||
}
|
||||
}
|
||||
|
||||
return inventions;
|
||||
}
|
||||
|
||||
public static List<InventionData> GetInventionsByPlayer(long playerId)
|
||||
=> Inventions.Find(x => x.DownloadIds.Contains(playerId)).ToList()
|
||||
.Select(inv => MapInventionToData(inv)).ToList();
|
||||
|
||||
public static List<object> GetInventionsByCreator(long creatorId)
|
||||
=> Inventions.Find(x => x.CreatorPlayerId == creatorId).ToList()
|
||||
.Select(inv => MapInventionFull(inv)).ToList();
|
||||
|
||||
public static List<object> GetInventionsByRoom(long roomId)
|
||||
=> Inventions.Find(x => x.CreationRoomId == roomId).ToList()
|
||||
.Select(inv => MapInventionFull(inv)).ToList();
|
||||
|
||||
public static List<object> GetVersionsForInvention(long inventionId)
|
||||
=> Versions.Find(v => v.InventionId == inventionId)
|
||||
.Select(v => MapVersion(v)).ToList();
|
||||
|
||||
public static InventionVersion? GetVersion(long inventionId, int versionNumber)
|
||||
=> Versions.FindOne(x => x.InventionId == inventionId && x.VersionNumber == versionNumber);
|
||||
|
||||
public static InventionVersion? GetLatestVersion(long inventionId)
|
||||
=> Versions.Find(v => v.InventionId == inventionId)
|
||||
.OrderByDescending(v => v.VersionNumber)
|
||||
.FirstOrDefault();
|
||||
|
||||
public static long GetNextInventionId()
|
||||
{
|
||||
if (Inventions.Count() == 0)
|
||||
return 5012352472715840656;
|
||||
return Convert.ToInt64(Inventions.Max(x => x.InventionId)) + 1;
|
||||
}
|
||||
|
||||
public static (Invention invention, InventionVersion version)? SaveInvention(long creatorPlayerId, SaveInventionRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
long inventionId = GetNextInventionId();
|
||||
|
||||
var version = new InventionVersion
|
||||
{
|
||||
InventionId = inventionId,
|
||||
VersionNumber = 1,
|
||||
ReplicationId = Guid.NewGuid().ToString(),
|
||||
BlobName = request.inventionDataFilename ?? string.Empty,
|
||||
BlobHash = string.Empty,
|
||||
ChipsCost = request.chipsCost,
|
||||
CloudVariablesCost = request.cloudVariablesCost,
|
||||
InstantiationCost = request.instantiationCost,
|
||||
LightsCost = request.lightsCost,
|
||||
AICost = request.aiCost,
|
||||
inkCost = request.inkCost,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var invention = new Invention
|
||||
{
|
||||
InventionId = inventionId,
|
||||
ReplicationId = Guid.NewGuid().ToString(),
|
||||
CreatorPlayerId = creatorPlayerId,
|
||||
CreationRoomId = request.creationRoomId,
|
||||
Name = request.name ?? string.Empty,
|
||||
Description = request.description ?? string.Empty,
|
||||
ImageName = request.imageName ?? string.Empty,
|
||||
CurrentVersionNumber = 1,
|
||||
LatestVersionNumber = 1,
|
||||
CurrentVersion = version,
|
||||
Versions = new List<InventionVersion> { version },
|
||||
Accessibility = RoomDBClasses.RoomAccessibility.Private,
|
||||
AllowTrial = true,
|
||||
HideFromPlayer = false,
|
||||
CreatorPermission = (int)InventionPermissions.Unlimited,
|
||||
GeneralPermission = (int)InventionPermissions.Unassigned,
|
||||
inkCost = 1,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
ModifiedAt = DateTime.UtcNow,
|
||||
DownloadIds = new List<long> { creatorPlayerId },
|
||||
ReferencedInventions = request.referencedInventions ?? new List<long>()
|
||||
};
|
||||
|
||||
Inventions.Insert(invention);
|
||||
Versions.Insert(version);
|
||||
|
||||
return (invention, version);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[InventionDB] Error saving invention: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static (Invention invention, InventionVersion version)? AddVersion(Invention invention, AddVersionRequest request)
|
||||
{
|
||||
int nextVersion = invention.CurrentVersionNumber + 1;
|
||||
|
||||
var version = new InventionVersion
|
||||
{
|
||||
InventionId = invention.InventionId,
|
||||
VersionNumber = nextVersion,
|
||||
BlobName = request.inventionDataFilename ?? string.Empty,
|
||||
ChipsCost = request.chipsCost,
|
||||
CloudVariablesCost = request.cloudVariablesCost,
|
||||
InstantiationCost = request.instantiationCost,
|
||||
LightsCost = request.lightsCost,
|
||||
AICost = request.aiCost,
|
||||
inkCost = request.inkCost,
|
||||
ReplicationId = Guid.NewGuid().ToString(),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
invention.CurrentVersionNumber = nextVersion;
|
||||
invention.LatestVersionNumber = nextVersion;
|
||||
invention.CurrentVersion = version;
|
||||
invention.Versions.Add(version);
|
||||
invention.ModifiedAt = DateTime.UtcNow;
|
||||
|
||||
Versions.Insert(version);
|
||||
Inventions.Update(invention);
|
||||
|
||||
return (invention, version);
|
||||
}
|
||||
|
||||
public static Invention? UpdateInvention(long inventionId, long playerId,
|
||||
string? name, string? description, string? imageName,
|
||||
int? price, bool? isPublished, bool? allowTrial,
|
||||
bool? hideFromPlayer, int? generalPermission, int? creatorPermission)
|
||||
{
|
||||
var invention = GetInvention(inventionId);
|
||||
if (invention == null || invention.CreatorPlayerId != playerId) return null;
|
||||
|
||||
if (name != null) invention.Name = name;
|
||||
if (description != null) invention.Description = description;
|
||||
if (imageName != null) invention.ImageName = imageName;
|
||||
if (price.HasValue) invention.Price = price.Value;
|
||||
if (isPublished.HasValue) invention.IsPublished = isPublished.Value;
|
||||
if (allowTrial.HasValue) invention.AllowTrial = allowTrial.Value;
|
||||
if (hideFromPlayer.HasValue) invention.HideFromPlayer = hideFromPlayer.Value;
|
||||
if (generalPermission.HasValue) invention.GeneralPermission = generalPermission.Value;
|
||||
if (creatorPermission.HasValue) invention.CreatorPermission = creatorPermission.Value;
|
||||
|
||||
invention.ModifiedAt = DateTime.UtcNow;
|
||||
Inventions.Update(invention);
|
||||
|
||||
return invention;
|
||||
}
|
||||
|
||||
public static Invention? PublishInvention(long inventionId, long playerId, int permissionLevel, int accessibility, int price)
|
||||
{
|
||||
var invention = GetInvention(inventionId);
|
||||
if (invention == null || invention.CreatorPlayerId != playerId) return null;
|
||||
|
||||
invention.CreatorPermission = 255;
|
||||
invention.GeneralPermission = permissionLevel;
|
||||
invention.Accessibility = (RoomDBClasses.RoomAccessibility)accessibility;
|
||||
invention.Price = price;
|
||||
invention.IsPublished = true;
|
||||
|
||||
invention.ModifiedAt = DateTime.UtcNow;
|
||||
Inventions.Update(invention);
|
||||
|
||||
return invention;
|
||||
}
|
||||
|
||||
public static Invention? UnpublishInvention(long inventionId, long playerId)
|
||||
{
|
||||
var invention = GetInvention(inventionId);
|
||||
if (invention == null || invention.CreatorPlayerId != playerId) return null;
|
||||
|
||||
invention.IsPublished = false;
|
||||
|
||||
invention.ModifiedAt = DateTime.UtcNow;
|
||||
Inventions.Update(invention);
|
||||
|
||||
return invention;
|
||||
}
|
||||
|
||||
public static List<Invention> GetTopTodayInventions(bool featuredOnly, bool includeUnpublished = false)
|
||||
{
|
||||
var query = Inventions.FindAll().AsEnumerable();
|
||||
if (featuredOnly)
|
||||
{
|
||||
query = query.Where(r => r.Tags.Any(t => string.Equals(t.Tag, "featured", StringComparison.OrdinalIgnoreCase) || r.IsFeatured));
|
||||
}
|
||||
if (!includeUnpublished)
|
||||
query = query.Where(r => r.IsPublished);
|
||||
|
||||
return query.OrderByDescending(r => r.CheerCount).ToList();
|
||||
}
|
||||
|
||||
public static (List<Invention> Results, int Total) Search(string query, int skip = 0, int take = 30)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
return (new List<Invention>(), 0);
|
||||
|
||||
var segments = query.Split('|', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.ToList();
|
||||
|
||||
var candidatePool = Inventions.FindAll()
|
||||
.Where(i => i.IsPublished)
|
||||
.Where(i => !i.HideFromPlayer)
|
||||
.Where(i => i.Accessibility == RoomDBClasses.RoomAccessibility.Public)
|
||||
.ToList();
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().ToList();
|
||||
|
||||
var matched = new Dictionary<long, Invention>();
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
var terms = segment.Split(new char[] { ' ', '+' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var nameParts = new List<string>();
|
||||
var tagParts = new List<string>();
|
||||
var authorParts = new List<string>();
|
||||
|
||||
foreach (var term in terms)
|
||||
{
|
||||
if (term.StartsWith("#"))
|
||||
tagParts.Add(term.Substring(1).ToLowerInvariant().Trim());
|
||||
else if (term.StartsWith("@"))
|
||||
authorParts.Add(term.Substring(1).ToLowerInvariant().Trim());
|
||||
else
|
||||
nameParts.Add(term.ToLowerInvariant().Trim());
|
||||
}
|
||||
|
||||
long? resolvedAuthorId = null;
|
||||
|
||||
if (authorParts.Count > 0)
|
||||
{
|
||||
var authorQuery = string.Join(" ", authorParts);
|
||||
|
||||
var exactMatch = allPlayers.FirstOrDefault(p =>
|
||||
string.Equals(p.Player?.Username, authorQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(p.Player?.DisplayName, authorQuery, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (exactMatch != null)
|
||||
{
|
||||
resolvedAuthorId = exactMatch.PlayerId;
|
||||
}
|
||||
else
|
||||
{
|
||||
var fuzzyMatch = allPlayers
|
||||
.Where(p => p.Player != null)
|
||||
.Select(p => new
|
||||
{
|
||||
Player = p,
|
||||
Score = Math.Max(
|
||||
ScoreShit((p.Player!.Username ?? "").ToLowerInvariant(), authorQuery),
|
||||
ScoreShit((p.Player!.DisplayName ?? "").ToLowerInvariant(), authorQuery))
|
||||
})
|
||||
.Where(x => x.Score > 0)
|
||||
.OrderByDescending(x => x.Score)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (fuzzyMatch != null)
|
||||
resolvedAuthorId = fuzzyMatch.Player.PlayerId;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var invention in candidatePool)
|
||||
{
|
||||
if (resolvedAuthorId.HasValue && invention.CreatorPlayerId != resolvedAuthorId.Value)
|
||||
continue;
|
||||
|
||||
if (tagParts.Count > 0)
|
||||
{
|
||||
var inventionTags = (invention.Tags ?? new List<InventionTag>())
|
||||
.Select(t => t.Tag.ToLowerInvariant().Trim())
|
||||
.ToList();
|
||||
|
||||
bool allTagsMatch = tagParts.All(tp =>
|
||||
{
|
||||
if (inventionTags.Contains(tp))
|
||||
return true;
|
||||
|
||||
foreach (var it in inventionTags)
|
||||
{
|
||||
if (it.Contains(tp) || tp.Contains(it))
|
||||
return true;
|
||||
|
||||
int dist = EditDistance(it, tp);
|
||||
int maxLen = Math.Max(it.Length, tp.Length);
|
||||
if (maxLen > 0 && (double)dist / maxLen <= 0.34)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!allTagsMatch)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nameParts.Count > 0)
|
||||
{
|
||||
string inventionNameLower = (invention.Name ?? "").ToLowerInvariant();
|
||||
|
||||
bool nameMatch = nameParts.All(np =>
|
||||
{
|
||||
if (inventionNameLower.Contains(np))
|
||||
return true;
|
||||
|
||||
int dist = EditDistance(inventionNameLower, np);
|
||||
int maxLen = Math.Max(inventionNameLower.Length, np.Length);
|
||||
if (maxLen > 0 && (double)dist / maxLen <= 0.34)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!nameMatch)
|
||||
continue;
|
||||
}
|
||||
|
||||
matched[invention.InventionId] = invention;
|
||||
}
|
||||
}
|
||||
|
||||
var all = matched.Values
|
||||
.OrderByDescending(i => i.CheerCount)
|
||||
.ThenByDescending(i => i.NumDownloads)
|
||||
.ToList();
|
||||
|
||||
return (all.Skip(skip).Take(take).ToList(), all.Count);
|
||||
}
|
||||
|
||||
public static (bool success, bool isCheering, int cheerCount)? CheerInvention(long inventionId, long playerId, bool cheer)
|
||||
{
|
||||
var invention = GetInvention(inventionId);
|
||||
if (invention == null) return null;
|
||||
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player?.Player?.PlayerExtra == null) return null;
|
||||
|
||||
player.Player.PlayerExtra.PersonalDetails ??= new PlayerDBClasses.PersonalDetails();
|
||||
player.Player.PlayerExtra.PersonalDetails.inventionsCheered ??= new List<long>();
|
||||
|
||||
bool alreadyCheering = player.Player.PlayerExtra.PersonalDetails.inventionsCheered.Contains(inventionId);
|
||||
|
||||
if (cheer && !alreadyCheering)
|
||||
{
|
||||
invention.CheerCount++;
|
||||
player.Player.PlayerExtra.PersonalDetails.inventionsCheered.Add(inventionId);
|
||||
player.Player.PlayerExtra.PersonalDetails.IsCheering = true;
|
||||
}
|
||||
else if (!cheer && alreadyCheering)
|
||||
{
|
||||
invention.CheerCount = Math.Max(0, invention.CheerCount - 1);
|
||||
player.Player.PlayerExtra.PersonalDetails.inventionsCheered.Remove(inventionId);
|
||||
player.Player.PlayerExtra.PersonalDetails.IsCheering = false;
|
||||
}
|
||||
|
||||
Inventions.Update(invention);
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
return (true, player.Player.PlayerExtra.PersonalDetails.inventionsCheered.Contains(inventionId), invention.CheerCount);
|
||||
}
|
||||
|
||||
public static bool DeleteInvention(long inventionId, long playerId)
|
||||
{
|
||||
var invention = GetInvention(inventionId);
|
||||
if (invention == null || invention.CreatorPlayerId != playerId) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var cdnPath = Path.Join(Program.dataDir, "cdn", "inv");
|
||||
|
||||
if (Directory.Exists(cdnPath))
|
||||
{
|
||||
foreach (var v in Versions.Find(v => v.InventionId == inventionId).ToList())
|
||||
if (!string.IsNullOrWhiteSpace(v.ReplicationId))
|
||||
foreach (var file in Directory.GetFiles(cdnPath, v.ReplicationId + "*"))
|
||||
try { File.Delete(file); } catch { }
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(invention.ReplicationId))
|
||||
foreach (var file in Directory.GetFiles(cdnPath, invention.ReplicationId + "*"))
|
||||
try { File.Delete(file); } catch { }
|
||||
}
|
||||
|
||||
Versions.DeleteMany(v => v.InventionId == inventionId);
|
||||
Inventions.Delete(inventionId);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[InventionDB] Error deleting invention: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static SetTagsResponse SetTags(long inventionId, SetTagsRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var invention = GetInvention(inventionId);
|
||||
if (invention == null)
|
||||
return new SetTagsResponse { Result = -1 };
|
||||
|
||||
invention.Tags.Clear();
|
||||
|
||||
var allTags = new List<string>();
|
||||
|
||||
foreach (var autoTag in request.AutoTags ?? new List<string>())
|
||||
{
|
||||
invention.Tags.Add(new InventionTag { Tag = autoTag, Type = 0 });
|
||||
allTags.Add(autoTag);
|
||||
}
|
||||
|
||||
foreach (var customTag in request.CustomTags ?? new List<string>())
|
||||
{
|
||||
invention.Tags.Add(new InventionTag { Tag = customTag, Type = 1 });
|
||||
allTags.Add(customTag);
|
||||
}
|
||||
|
||||
Inventions.Update(invention);
|
||||
|
||||
return new SetTagsResponse { Result = 0, Tags = allTags };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[InventionDB] Error setting tags: {ex.Message}");
|
||||
return new SetTagsResponse { Result = -1 };
|
||||
}
|
||||
}
|
||||
|
||||
private static int ScoreShit(string source, string target)
|
||||
{
|
||||
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(target)) return 0;
|
||||
if (source == target) return 1000;
|
||||
if (source.Contains(target) || target.Contains(source)) return 500;
|
||||
|
||||
int dist = EditDistance(source, target);
|
||||
int maxLen = Math.Max(source.Length, target.Length);
|
||||
if (maxLen == 0) return 0;
|
||||
|
||||
double similarity = 1.0 - (double)dist / maxLen;
|
||||
if (similarity >= 0.66) return (int)(similarity * 100);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int EditDistance(string a, string b)
|
||||
{
|
||||
if (string.IsNullOrEmpty(a)) return b?.Length ?? 0;
|
||||
if (string.IsNullOrEmpty(b)) return a.Length;
|
||||
|
||||
int m = a.Length, n = b.Length;
|
||||
var dp = new int[m + 1, n + 1];
|
||||
|
||||
for (int i = 0; i <= m; i++) dp[i, 0] = i;
|
||||
for (int j = 0; j <= n; j++) dp[0, j] = j;
|
||||
|
||||
for (int i = 1; i <= m; i++)
|
||||
{
|
||||
for (int j = 1; j <= n; j++)
|
||||
{
|
||||
dp[i, j] = a[i - 1] == b[j - 1]
|
||||
? dp[i - 1, j - 1]
|
||||
: 1 + Math.Min(dp[i - 1, j - 1], Math.Min(dp[i - 1, j], dp[i, j - 1]));
|
||||
}
|
||||
}
|
||||
|
||||
return dp[m, n];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
using LiteDB;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.LeaderboardDBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
using Vanadium.Controllers;
|
||||
using Vanadium.Utils.NotiController;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class LeaderboardDB
|
||||
{
|
||||
public static LiteDatabase LeaderboardDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Leaderboard.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<LeaderboardEntry> Entries = LeaderboardDBFile.GetCollection<LeaderboardEntry>("Entries");
|
||||
|
||||
public static void Setup()
|
||||
{
|
||||
Entries.EnsureIndex(x => x.PlayerId);
|
||||
Entries.EnsureIndex(x => x.RoomId);
|
||||
Entries.EnsureIndex(x => x.StatChannel);
|
||||
Entries.EnsureIndex(x => x.FilterType);
|
||||
}
|
||||
|
||||
private static long GetNextEntryId()
|
||||
{
|
||||
if (Entries.Count() == 0) return 1;
|
||||
return Entries.Max(x => x.EntryId) + 1;
|
||||
}
|
||||
|
||||
private static int ComputeRank(long roomId, int statChannel, long playerId, int score)
|
||||
{
|
||||
return Entries.Count(x =>
|
||||
x.RoomId == roomId &&
|
||||
x.StatChannel == statChannel &&
|
||||
(x.Score > score || (x.Score == score && x.PlayerId < playerId)));
|
||||
}
|
||||
|
||||
private static bool IsRRORoom(long roomId)
|
||||
{
|
||||
var room = RoomDB.Rooms.FindById(roomId);
|
||||
if (room == null) return false;
|
||||
|
||||
if (room.IsRRO) return true;
|
||||
|
||||
return room.Tags != null && room.Tags.Any(t =>
|
||||
t.Tag.Equals("rro", StringComparison.OrdinalIgnoreCase) ||
|
||||
t.Tag.Equals("recroomoriginal", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static int ClearRoomLeaderboard(long roomId)
|
||||
{
|
||||
return Entries.DeleteMany(x => x.RoomId == roomId);
|
||||
}
|
||||
|
||||
private static async Task BanPlayerForLeaderboardAbuse(long playerId, int type = 0)
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null) return;
|
||||
var msg = "Cheating";
|
||||
|
||||
var mbd = new ModerationBlockDetails
|
||||
{
|
||||
IsBan = true,
|
||||
ReportCategory = ReportCategory.Moderator,
|
||||
Duration = 2147483647,
|
||||
Message = msg,
|
||||
ModerationSetUnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
|
||||
player.Player ??= new Player();
|
||||
player.Player.PlayerExtra ??= new PlayerExtra();
|
||||
player.Player.PlayerExtra.ModerationBlockDetails = mbd;
|
||||
player.Player.PlayerExtra.Heartbeat = new Heartbeat
|
||||
{
|
||||
playerId = playerId,
|
||||
isOnline = false,
|
||||
roomInstance = null,
|
||||
errorCode = 0
|
||||
};
|
||||
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat);
|
||||
await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId));
|
||||
await NotificationsController.SendBanned(playerId, mbd);
|
||||
}
|
||||
|
||||
public static GetRanksResponse GetRanks(GetRanksRequest request)
|
||||
{
|
||||
int rankStart = request.RankStart < 0 ? 0 : request.RankStart;
|
||||
int limit = request.RankEnd - rankStart + 1;
|
||||
|
||||
if (limit <= 0)
|
||||
return new GetRanksResponse { rows = new List<LeaderboardRow>() };
|
||||
|
||||
var entries = Entries
|
||||
.Find(x => x.RoomId == request.RoomId && x.StatChannel == request.StatChannel)
|
||||
.OrderBy(x => request.SortAscending ? x.Score : -x.Score)
|
||||
.ThenBy(x => x.PlayerId)
|
||||
.Skip(rankStart)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
|
||||
var rows = entries.Select((e, i) => new LeaderboardRow
|
||||
{
|
||||
playerId = e.PlayerId,
|
||||
rank = rankStart + i,
|
||||
score = e.Score
|
||||
}).ToList();
|
||||
|
||||
return new GetRanksResponse { rows = rows };
|
||||
}
|
||||
|
||||
public static LeaderboardRow GetPlayerRank(GetRanksRequest request)
|
||||
{
|
||||
var entry = Entries.FindOne(x =>
|
||||
x.PlayerId == request.PlayerId &&
|
||||
x.RoomId == request.RoomId &&
|
||||
x.StatChannel == request.StatChannel);
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
return new LeaderboardRow
|
||||
{
|
||||
playerId = request.PlayerId,
|
||||
rank = ComputeRank(request.RoomId, request.StatChannel, request.PlayerId, entry.Score),
|
||||
score = entry.Score
|
||||
};
|
||||
}
|
||||
|
||||
int total = Entries.Count(x => x.RoomId == request.RoomId && x.StatChannel == request.StatChannel);
|
||||
|
||||
return new LeaderboardRow
|
||||
{
|
||||
playerId = request.PlayerId,
|
||||
rank = total,
|
||||
score = 0
|
||||
};
|
||||
}
|
||||
|
||||
public static GetNearbyScoresResponse GetNearbyScores(GetNearbyScoresRequest request)
|
||||
{
|
||||
int windowSize = request.WindowSize <= 0 ? 10 : request.WindowSize;
|
||||
|
||||
var all = Entries
|
||||
.Find(x => x.RoomId == request.RoomId && x.StatChannel == request.StatChannel)
|
||||
.OrderBy(x => request.SortAscending ? x.Score : -x.Score)
|
||||
.ThenBy(x => x.PlayerId)
|
||||
.ToList();
|
||||
|
||||
if (all.Count == 0)
|
||||
return new GetNearbyScoresResponse { rows = new List<LeaderboardRow>() };
|
||||
|
||||
int center = all.Count;
|
||||
for (int i = 0; i < all.Count; i++)
|
||||
{
|
||||
if (all[i].PlayerId == request.PlayerId)
|
||||
{
|
||||
center = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int half = windowSize / 2;
|
||||
int start = center - half;
|
||||
int end = start + windowSize;
|
||||
|
||||
if (start < 0)
|
||||
{
|
||||
end -= start;
|
||||
start = 0;
|
||||
}
|
||||
|
||||
if (end > all.Count)
|
||||
{
|
||||
end = all.Count;
|
||||
start = end - windowSize;
|
||||
if (start < 0)
|
||||
start = 0;
|
||||
}
|
||||
|
||||
var rows = new List<LeaderboardRow>();
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
rows.Add(new LeaderboardRow
|
||||
{
|
||||
playerId = all[i].PlayerId,
|
||||
rank = i,
|
||||
score = all[i].Score
|
||||
});
|
||||
}
|
||||
|
||||
return new GetNearbyScoresResponse { rows = rows };
|
||||
}
|
||||
|
||||
public static async Task<CheckAndSetStatResponse> CheckAndSetStat(long playerId, CheckAndSetStatRequest request)
|
||||
{
|
||||
//Console.WriteLine($"[LeaderboardDB] CheckAndSetStat called for playerId: {playerId}, roomId: {request.RoomId}, statChannel: {request.StatChannel}, statValue: {request.StatValue}, currentStatValue: {request.CurrentStatValue}");
|
||||
const int RROSuspiciousMin = 1147483647;
|
||||
const int RROSuspiciousMax = 2147383647;
|
||||
|
||||
if (IsRRORoom(request.RoomId) && request.StatValue >= RROSuspiciousMin && request.StatValue <= RROSuspiciousMax)
|
||||
{
|
||||
await BanPlayerForLeaderboardAbuse(playerId);
|
||||
|
||||
var existing = Entries.FindOne(x =>
|
||||
x.PlayerId == playerId &&
|
||||
x.RoomId == request.RoomId &&
|
||||
x.StatChannel == request.StatChannel);
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
existing = new LeaderboardEntry
|
||||
{
|
||||
EntryId = GetNextEntryId(),
|
||||
PlayerId = playerId,
|
||||
RoomId = request.RoomId,
|
||||
StatChannel = request.StatChannel,
|
||||
FilterType = 0,
|
||||
Score = 0,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
Entries.Insert(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.Score = 0;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
Entries.Update(existing);
|
||||
}
|
||||
|
||||
int bannedRank = ComputeRank(request.RoomId, request.StatChannel, playerId, 0);
|
||||
|
||||
return new CheckAndSetStatResponse
|
||||
{
|
||||
error = null,
|
||||
success = true,
|
||||
value = new LeaderboardRow
|
||||
{
|
||||
playerId = playerId,
|
||||
rank = bannedRank,
|
||||
score = 0
|
||||
}
|
||||
};
|
||||
}
|
||||
Console.WriteLine($"[LeaderboardDB] Proceeding with normal stat update for playerId: {playerId}, roomId: {request.RoomId}, statChannel: {request.StatChannel}, statValue: {request.StatValue}, currentStatValue: {request.CurrentStatValue}");
|
||||
|
||||
var entry = Entries.FindOne(x =>
|
||||
x.PlayerId == playerId &&
|
||||
x.RoomId == request.RoomId &&
|
||||
x.StatChannel == request.StatChannel);
|
||||
|
||||
int msg;
|
||||
switch (request.StatChannel)
|
||||
{
|
||||
case 0:
|
||||
msg = 0;
|
||||
break;
|
||||
case 1:
|
||||
msg = 3;
|
||||
break;
|
||||
default:
|
||||
msg = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[LeaderboardDB] RoomId: {request.RoomId}, typeof: {request.RoomId.GetType()}");
|
||||
if (request.RoomId == 4974)
|
||||
{
|
||||
Console.WriteLine($"[LeaderboardDB] PlayerId: {playerId}, typeof: {playerId.GetType()}");
|
||||
int tokensToGrant = request.StatValue - request.CurrentStatValue;
|
||||
Console.WriteLine($"[LeaderboardDB] Granting {tokensToGrant} tokens to player {playerId} for leaderboard performance.");
|
||||
await APIController.SendTokenEarningWebhook(playerId, tokensToGrant.ToString(), msg);
|
||||
if (tokensToGrant >= 1200)
|
||||
{
|
||||
PlayerDB.UpdatePlayerHeartbeat(playerId, null, online: false);
|
||||
APIController.SendNonHileWebhook(playerId, "Tried to claim " + tokensToGrant + " tokens");
|
||||
return new CheckAndSetStatResponse
|
||||
{
|
||||
error = null,
|
||||
success = false,
|
||||
value = null
|
||||
};
|
||||
}
|
||||
var gift = GiftsDB.CreateGift(
|
||||
toPlayerId: playerId,
|
||||
fromPlayerId: 1,
|
||||
currency: tokensToGrant,
|
||||
currencyType: 2,
|
||||
balanceType: -2,
|
||||
giftContext: enums.GiftType.GameRewards_Tokens,
|
||||
giftRarity: 50,
|
||||
message: "Great job! You earned " + tokensToGrant + " tokens for your performance in the leaderboard!",
|
||||
platform: -1,
|
||||
platformsToSpawnOn: -1
|
||||
);
|
||||
|
||||
Console.WriteLine($"[LeaderboardDB] Created gift for player {playerId}.");
|
||||
|
||||
await NotiController.SendEvent(playerId, "31", GiftsDB.MapToDTO(gift));
|
||||
|
||||
Console.WriteLine($"[LeaderboardDB] Sent event notification to player {playerId} for the gift JSON: {GiftsDB.MapToDTO(gift)}");
|
||||
}
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
entry = new LeaderboardEntry
|
||||
{
|
||||
EntryId = GetNextEntryId(),
|
||||
PlayerId = playerId,
|
||||
RoomId = request.RoomId,
|
||||
StatChannel = request.StatChannel,
|
||||
FilterType = 0,
|
||||
Score = request.StatValue,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
Entries.Insert(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
entry.Score = request.StatValue;
|
||||
entry.UpdatedAt = DateTime.UtcNow;
|
||||
Entries.Update(entry);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[LeaderboardDB] Updated leaderboard entry for playerId: {playerId}, roomId: {request.RoomId}, statChannel: {request.StatChannel}, newScore: {entry.Score}");
|
||||
|
||||
int rank = ComputeRank(request.RoomId, request.StatChannel, playerId, entry.Score);
|
||||
|
||||
return new CheckAndSetStatResponse
|
||||
{
|
||||
error = null,
|
||||
success = true,
|
||||
value = new LeaderboardRow
|
||||
{
|
||||
playerId = playerId,
|
||||
rank = rank,
|
||||
score = entry.Score
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using LiteDB;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PhotonAccessTokenDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class PhotonAccessTokenDB
|
||||
{
|
||||
public static LiteDatabase PhotonDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "PhotonAccessToken.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<SubroomPermissionSet> PermissionSets = PhotonDBFile.GetCollection<SubroomPermissionSet>("SubroomPermissions");
|
||||
|
||||
public static void Setup()
|
||||
{
|
||||
PermissionSets.EnsureIndex(x => x.RoomId);
|
||||
PermissionSets.EnsureIndex(x => x.SubRoomId);
|
||||
}
|
||||
|
||||
public static long GetNextId()
|
||||
{
|
||||
if (PermissionSets.Count() == 0) return 1;
|
||||
return PermissionSets.Max(x => x.Id) + 1;
|
||||
}
|
||||
|
||||
public static SubroomPermissionSet? GetPermissions(long roomId, long subRoomId)
|
||||
{
|
||||
return PermissionSets.FindOne(x => x.RoomId == roomId && x.SubRoomId == subRoomId);
|
||||
}
|
||||
|
||||
public static SubroomPermissionSet SetPermissions(long roomId, long subRoomId, List<StoredPermissionEntry> incomingPermissions)
|
||||
{
|
||||
var existing = PermissionSets.FindOne(x => x.RoomId == roomId && x.SubRoomId == subRoomId);
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
existing = new SubroomPermissionSet
|
||||
{
|
||||
Id = GetNextId(),
|
||||
RoomId = roomId,
|
||||
SubRoomId = subRoomId,
|
||||
Permissions = new List<StoredPermissionEntry>(),
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var incoming in incomingPermissions)
|
||||
{
|
||||
var match = existing.Permissions.FirstOrDefault(p =>
|
||||
p.Permission == incoming.Permission && p.Role == incoming.Role && p.Type == incoming.Type);
|
||||
|
||||
if (match == null)
|
||||
{
|
||||
existing.Permissions.Add(new StoredPermissionEntry
|
||||
{
|
||||
Permission = incoming.Permission,
|
||||
Role = incoming.Role,
|
||||
Type = incoming.Type,
|
||||
Override = incoming.Override,
|
||||
Value = incoming.Value
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
match.Override = incoming.Override;
|
||||
match.Value = incoming.Value;
|
||||
}
|
||||
}
|
||||
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
if (existing.Id == 0)
|
||||
existing.Id = GetNextId();
|
||||
|
||||
PermissionSets.Upsert(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
public static void DeletePermissions(long roomId, long subRoomId)
|
||||
{
|
||||
var existing = PermissionSets.FindOne(x => x.RoomId == roomId && x.SubRoomId == subRoomId);
|
||||
if (existing != null)
|
||||
PermissionSets.Delete(existing.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1637 @@
|
||||
using System;
|
||||
using LiteDB;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using Vanadium.enums;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class PlayerDB
|
||||
{
|
||||
private static readonly ConcurrentDictionary<long, SemaphoreSlim> PlayerLocks = new();
|
||||
|
||||
public static SemaphoreSlim GetPlayerLock(long playerId) =>
|
||||
PlayerLocks.GetOrAdd(playerId, _ => new SemaphoreSlim(1, 1));
|
||||
|
||||
public static DateTime AsUtc(DateTime value) =>
|
||||
value.Kind == DateTimeKind.Utc ? value : value.ToUniversalTime();
|
||||
|
||||
public static LiteDatabase PlayerDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Players", "Players.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<FullPlayer> Players = PlayerDBFile.GetCollection<FullPlayer>("Players");
|
||||
|
||||
public static LiteDatabase EquipmentDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Players", "Equipment.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<FullPlayer> EquipmentPlayers = EquipmentDBFile.GetCollection<FullPlayer>("Players");
|
||||
|
||||
public static LiteDatabase SettingsDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Players", "Settings.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<FullPlayer> SettingsPlayers = SettingsDBFile.GetCollection<FullPlayer>("Players");
|
||||
|
||||
private static readonly HttpClient WebhookClient = new HttpClient();
|
||||
private const string WebhookUrl = "https://discord.com/api/webhooks/1512204104384905226/rVgyUjpP7uvRoyuZXZfYtQ-WYgLhqkCA6TWGWSFLwFt8iiSvGTrrzKPSA2-R-bNiXnE6";
|
||||
private static readonly HttpClient SteamApiClient = new HttpClient();
|
||||
|
||||
private static readonly HashSet<string> DefaultEquipmentGuids = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"5dcf8a2e-6fa6-4007-ac1e-73dd53c4c247",
|
||||
"e6899422-ab2f-4bd9-8e98-aef950ec27b6",
|
||||
"4aff20ee-0b20-434b-a030-d3e1764fe4e7",
|
||||
"7ef5d1d9-24db-428c-85e0-107c1bfcc026",
|
||||
"93f5418e-a03d-4074-804c-daa0e374dd9f",
|
||||
"affbab2d-3200-4512-a140-685fdc5570c6"
|
||||
};
|
||||
|
||||
private static readonly int[] XPToNextLevel = {
|
||||
0,
|
||||
30, 40, 50, 65, 80, 100, 120, 145, 170, 200,
|
||||
235, 275, 320, 370, 425, 490, 560, 640, 730, 830,
|
||||
1066, 1599, 2398, 3597, 5395, 8092, 12138, 18207, 27310, 40965, 61447,
|
||||
92170, 138255, 207382, 311073, 466609, 699913, 1049869, 1574803, 2362204,
|
||||
3543306, 5314959, 7972438, 11958657, 17937986, 26906980, 40360472,
|
||||
60540708, 90811064, 136216592
|
||||
};
|
||||
|
||||
public static int GetXPRequiredForLevel(int level)
|
||||
{
|
||||
if (level <= 1) return 0;
|
||||
if (level - 1 < XPToNextLevel.Length) return XPToNextLevel[level - 1];
|
||||
return XPToNextLevel[XPToNextLevel.Length - 1];
|
||||
}
|
||||
|
||||
public static int GetXPForLevel(int level) => GetXPRequiredForLevel(level);
|
||||
|
||||
public static async Task<(bool xpAwarded, bool leveledUp, int newLevel, int newXP)> AwardHeartbeatXP(long playerId)
|
||||
{
|
||||
var playerLock = GetPlayerLock(playerId);
|
||||
await playerLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
return await AwardHeartbeatXPCore(playerId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
playerLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<(bool xpAwarded, bool leveledUp, int newLevel, int newXP)> AwardHeartbeatXPCore(long playerId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player == null) return (false, false, 0, 0);
|
||||
|
||||
if (playerId == 1) return (false, false, player.Player.Level, player.Player.XP);
|
||||
|
||||
var p = player.Player;
|
||||
|
||||
var hb = p.PlayerExtra?.Heartbeat;
|
||||
if (hb == null || !hb.isOnline || hb.roomInstance == null || hb.roomInstance.isPrivate)
|
||||
return (false, false, p.Level, p.XP);
|
||||
|
||||
const int xpPerInterval = 10;
|
||||
const int intervalMinutes = 5;
|
||||
const int roomXPCap = 60;
|
||||
const int dailyXPCap = 150;
|
||||
Console.WriteLine($"Awarding XP for player {playerId} in room {hb.roomInstance.roomInstanceId}");
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (AsUtc(p.XPTodayDate).Date != now.Date)
|
||||
{
|
||||
p.XPToday = 0;
|
||||
p.XPTodayDate = now;
|
||||
p.RoomXPEarnedToday.Clear();
|
||||
p.CurrentXPRoomId = 0;
|
||||
}
|
||||
Console.WriteLine($"Player {playerId} has earned {p.XPToday} XP today, cap is {dailyXPCap}");
|
||||
|
||||
if (p.XPToday >= dailyXPCap)
|
||||
return (false, false, p.Level, p.XP);
|
||||
|
||||
long roomId = hb.roomInstance.roomInstanceId;
|
||||
Console.WriteLine($"Player {playerId} is in room {roomId}, current XP room is {p.CurrentXPRoomId}");
|
||||
|
||||
// Entering a new room (re)starts the interval timer for that room.
|
||||
if (p.CurrentXPRoomId != roomId)
|
||||
{
|
||||
Console.WriteLine($"Player {playerId} has entered a new room {roomId}, resetting interval timer");
|
||||
p.CurrentXPRoomId = roomId;
|
||||
p.LastXPAwardedAt = now;
|
||||
Players.Update(player);
|
||||
Console.WriteLine($"Updated player {playerId} with new room {roomId} and LastXPAwardedAt {now}");
|
||||
return (false, false, p.Level, p.XP);
|
||||
}
|
||||
var lastAwardedAt = AsUtc(p.LastXPAwardedAt);
|
||||
Console.WriteLine($"Player {playerId} has been in room {roomId} since {lastAwardedAt}, now is {now}");
|
||||
|
||||
p.RoomXPEarnedToday.TryGetValue(roomId, out int roomXPEarned);
|
||||
if (roomXPEarned >= roomXPCap)
|
||||
return (false, false, p.Level, p.XP);
|
||||
Console.WriteLine($"Player {playerId} has earned {roomXPEarned} XP in room {roomId} today, cap is {roomXPCap}");
|
||||
|
||||
if ((now - lastAwardedAt).TotalMinutes < intervalMinutes)
|
||||
return (false, false, p.Level, p.XP);
|
||||
Console.WriteLine($"Player {playerId} has been in room {roomId} for {(now - lastAwardedAt).TotalMinutes} minutes, awarding XP");
|
||||
|
||||
int xpGain = Math.Min(xpPerInterval, Math.Min(roomXPCap - roomXPEarned, dailyXPCap - p.XPToday));
|
||||
|
||||
int oldLevel = p.Level;
|
||||
p.XP += xpGain;
|
||||
p.XPToday += xpGain;
|
||||
p.RoomXPEarnedToday[roomId] = roomXPEarned + xpGain;
|
||||
p.LastXPAwardedAt = now;
|
||||
|
||||
bool leveledUp = false;
|
||||
int required = GetXPRequiredForLevel(p.Level + 1);
|
||||
if (required > 0 && p.XP >= required)
|
||||
{
|
||||
p.Level += 1;
|
||||
p.XP = 0;
|
||||
leveledUp = true;
|
||||
|
||||
int tokens = p.Level <= 10 ? 50 : p.Level <= 40 ? 80 : 150;
|
||||
var gift = GiftsDB.CreateGift(
|
||||
toPlayerId: player.PlayerId,
|
||||
fromPlayerId: 0,
|
||||
currency: tokens,
|
||||
currencyType: 2,
|
||||
balanceType: -2,
|
||||
giftContext: (GiftType)GiftType.LevelUp,
|
||||
giftRarity: 50,
|
||||
message: $"Congrats on reaching level {p.Level}!",
|
||||
platform: -1,
|
||||
platformsToSpawnOn: -1
|
||||
);
|
||||
|
||||
/*var giftDto = GiftsDB.MapToDTO(gift);
|
||||
await Vanadium.Controllers.NotificationsController.SendToPlayer(
|
||||
player.PlayerId,
|
||||
Newtonsoft.Json.JsonConvert.SerializeObject(new Vanadium.Classes.WebSocket.WebsocketEvents.Response
|
||||
{
|
||||
Id = "31",
|
||||
Msg = giftDto
|
||||
})
|
||||
);*/
|
||||
await Vanadium.Controllers.APIController.SendTokenEarningWebhook(player.PlayerId, tokens.ToString(), 1);
|
||||
}
|
||||
|
||||
Players.Update(player);
|
||||
Console.WriteLine($"Player {playerId} awarded {xpGain} XP, new level is {p.Level}, new XP is {p.XP}");
|
||||
|
||||
return (true, leveledUp, p.Level, p.XP);
|
||||
}
|
||||
|
||||
private static FullPlayer? GetOrMigrateEquipment(long playerId)
|
||||
{
|
||||
var player = EquipmentPlayers.FindById(playerId);
|
||||
if (player != null) return player;
|
||||
|
||||
var oldPlayer = Players.FindById(playerId);
|
||||
if (oldPlayer?.Player?.PlayerExtra == null) return null;
|
||||
|
||||
var migrated = new FullPlayer
|
||||
{
|
||||
PlayerId = oldPlayer.PlayerId,
|
||||
Player = new Player
|
||||
{
|
||||
PlayerExtra = new PlayerExtra
|
||||
{
|
||||
OwnedEquipment = oldPlayer.Player.PlayerExtra.OwnedEquipment ?? new List<OwnedEquipmentItem>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
EquipmentPlayers.Insert(migrated);
|
||||
|
||||
oldPlayer.Player.PlayerExtra.OwnedEquipment = null;
|
||||
Players.Update(oldPlayer);
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
public static bool GrantEquipmentItem(long playerId, string modificationGuid, string prefabName, string friendlyName = "", string tooltip = "", int rarity = 0)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modificationGuid) || string.IsNullOrWhiteSpace(prefabName))
|
||||
return false;
|
||||
|
||||
var player = GetOrMigrateEquipment(playerId);
|
||||
if (player?.Player?.PlayerExtra == null)
|
||||
return false;
|
||||
|
||||
player.Player.PlayerExtra.OwnedEquipment ??= new List<OwnedEquipmentItem>();
|
||||
|
||||
var alreadyOwned = player.Player.PlayerExtra.OwnedEquipment
|
||||
.Any(e => string.Equals(e.ModificationGuid, modificationGuid, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (alreadyOwned)
|
||||
return false;
|
||||
|
||||
player.Player.PlayerExtra.OwnedEquipment.Add(new OwnedEquipmentItem
|
||||
{
|
||||
ModificationGuid = modificationGuid,
|
||||
PrefabName = prefabName,
|
||||
FriendlyName = friendlyName,
|
||||
Tooltip = tooltip,
|
||||
Rarity = rarity,
|
||||
Favorited = false,
|
||||
PlatformMask = -1
|
||||
});
|
||||
|
||||
return EquipmentPlayers.Update(player);
|
||||
}
|
||||
|
||||
public static void ResetAllHeartbeats()
|
||||
{
|
||||
var all = Players.FindAll().ToList();
|
||||
foreach (var player in all)
|
||||
{
|
||||
if (player?.Player?.PlayerExtra == null) continue;
|
||||
player.Player.PlayerExtra.Heartbeat = new Heartbeat { playerId = player.PlayerId };
|
||||
Players.Update(player);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<RoomDBClasses.PermissionEntry> GetPlayerPermissions(long playerId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
return player?.Player?.PlayerExtra?.CurrentPermissions ?? new List<RoomDBClasses.PermissionEntry>();
|
||||
}
|
||||
|
||||
public static void SetPlayerPermissions(long playerId, List<RoomDBClasses.PermissionEntry> permissions)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player?.PlayerExtra == null) return;
|
||||
player.Player.PlayerExtra.CurrentPermissions = permissions;
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
private static List<OwnedEquipmentItem>? LoadAllEquipmentItems()
|
||||
{
|
||||
string path = Path.Join(Program.dataDir, "APIS", "Items", "Equipment.json");
|
||||
if (!File.Exists(path)) return null;
|
||||
return System.Text.Json.JsonSerializer.Deserialize<List<OwnedEquipmentItem>>(
|
||||
File.ReadAllText(path),
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
|
||||
public static void EnsureDefaultEquipment(FullPlayer player)
|
||||
{
|
||||
if (player?.Player?.PlayerExtra == null) return;
|
||||
|
||||
var eqPlayer = GetOrMigrateEquipment(player.PlayerId);
|
||||
if (eqPlayer?.Player?.PlayerExtra == null) return;
|
||||
|
||||
eqPlayer.Player.PlayerExtra.OwnedEquipment ??= new List<OwnedEquipmentItem>();
|
||||
|
||||
if (eqPlayer.Player.PlayerExtra.OwnedEquipment.Count > 0) return;
|
||||
|
||||
var allItems = LoadAllEquipmentItems();
|
||||
if (allItems == null) return;
|
||||
|
||||
var defaults = allItems
|
||||
.Where(i => DefaultEquipmentGuids.Contains(i.ModificationGuid))
|
||||
.ToList();
|
||||
|
||||
eqPlayer.Player.PlayerExtra.OwnedEquipment.AddRange(defaults);
|
||||
EquipmentPlayers.Update(eqPlayer);
|
||||
}
|
||||
|
||||
public static void EnsureDevEquipment(FullPlayer player)
|
||||
{
|
||||
if (player?.Player?.PlayerExtra == null) return;
|
||||
|
||||
var eqPlayer = GetOrMigrateEquipment(player.PlayerId);
|
||||
if (eqPlayer?.Player?.PlayerExtra == null) return;
|
||||
|
||||
var allItems = LoadAllEquipmentItems();
|
||||
if (allItems == null) return;
|
||||
|
||||
eqPlayer.Player.PlayerExtra.OwnedEquipment ??= new List<OwnedEquipmentItem>();
|
||||
|
||||
var ownedGuids = eqPlayer.Player.PlayerExtra.OwnedEquipment
|
||||
.Select(e => e.ModificationGuid)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var missing = allItems
|
||||
.Where(i => !ownedGuids.Contains(i.ModificationGuid))
|
||||
.ToList();
|
||||
|
||||
if (missing.Count == 0) return;
|
||||
|
||||
eqPlayer.Player.PlayerExtra.OwnedEquipment.AddRange(missing);
|
||||
EquipmentPlayers.Update(eqPlayer);
|
||||
}
|
||||
|
||||
public static bool UpdateEquipmentFavorited(long playerId, List<UpdateEquipmentRequest> updates)
|
||||
{
|
||||
var player = GetOrMigrateEquipment(playerId);
|
||||
if (player?.Player?.PlayerExtra?.OwnedEquipment == null) return false;
|
||||
|
||||
bool changed = false;
|
||||
|
||||
foreach (var update in updates)
|
||||
{
|
||||
var match = player.Player.PlayerExtra.OwnedEquipment.FirstOrDefault(e =>
|
||||
string.Equals(e.FriendlyName, update.FriendlyName, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(e.PrefabName, update.PrefabName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (match == null) continue;
|
||||
|
||||
match.Favorited = update.Favorited;
|
||||
|
||||
if (update.Tooltip != null)
|
||||
match.Tooltip = update.Tooltip;
|
||||
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
EquipmentPlayers.Update(player);
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
public static HashSet<string> GetOwnedEquipmentGuids(long playerId)
|
||||
{
|
||||
var owned = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
void Collect(List<OwnedEquipmentItem>? items)
|
||||
{
|
||||
if (items == null) return;
|
||||
foreach (var e in items)
|
||||
if (!string.IsNullOrWhiteSpace(e.ModificationGuid))
|
||||
owned.Add(e.ModificationGuid);
|
||||
}
|
||||
|
||||
Collect(EquipmentPlayers.FindById(playerId)?.Player?.PlayerExtra?.OwnedEquipment);
|
||||
Collect(Players.FindById(playerId)?.Player?.PlayerExtra?.OwnedEquipment);
|
||||
|
||||
return owned;
|
||||
}
|
||||
|
||||
private static async Task SendNewAccountWebhookAsync(string displayName, long accountId, DateTime createdAt, Platforms platforms, string platformId)
|
||||
{
|
||||
long unix = ((DateTimeOffset)createdAt).ToUnixTimeSeconds();
|
||||
|
||||
var payload = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
embeds = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
title = "New account",
|
||||
description = $"Name: {displayName}, ID: {accountId}, Created: <t:{unix}:F>, Platform Info: {platforms} {platformId}",
|
||||
color = 16753920
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await WebhookClient.PostAsync(WebhookUrl,
|
||||
new StringContent(payload, System.Text.Encoding.UTF8, "application/json"));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public static FullPlayer CreateAccount(Platforms platform, string platformId, bool isJunior) =>
|
||||
CreateAccountInternal(platform, platformId, isJunior, null, null);
|
||||
|
||||
public static FullPlayer? CreateAccountWithId(long desiredPlayerId, string? username, Platforms platform, string platformId, bool isJunior, out string? error)
|
||||
{
|
||||
error = null;
|
||||
|
||||
if (desiredPlayerId <= 0 || desiredPlayerId >= 2147483648L)
|
||||
{
|
||||
error = "Player ID must be a positive number less than 2147483648.";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Players.FindById(desiredPlayerId) != null)
|
||||
{
|
||||
error = "That Player ID is already in use.";
|
||||
return null;
|
||||
}
|
||||
|
||||
return CreateAccountInternal(platform, platformId, isJunior, desiredPlayerId, username);
|
||||
}
|
||||
|
||||
private static FullPlayer CreateAccountInternal(Platforms platform, string platformId, bool isJunior, long? desiredPlayerId, string? desiredUsername)
|
||||
{
|
||||
string username = string.IsNullOrWhiteSpace(desiredUsername) ? NameGen.GetRandomName() : desiredUsername.Trim();
|
||||
var newPlayerData = new Player
|
||||
{
|
||||
Username = username,
|
||||
DisplayName = username,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
LastLoginAt = DateTime.UtcNow,
|
||||
ProfileImage = "DefaultPFP.png",
|
||||
AvailableUsernameChanges = 3,
|
||||
IsJunior = isJunior,
|
||||
Level = 1,
|
||||
XP = 0,
|
||||
Birthday = new DateTime(2000, 1, 1, 1, 0, 0, DateTimeKind.Utc),
|
||||
PlayerExtra = new PlayerExtra()
|
||||
};
|
||||
|
||||
var newFullPlayer = new FullPlayer
|
||||
{
|
||||
PlayerId = desiredPlayerId ?? MakeNewPlayerID(),
|
||||
PlatformIds = new List<mPlatformID>
|
||||
{
|
||||
new mPlatformID { Platform = platform, PlatformId = platformId }
|
||||
},
|
||||
Player = newPlayerData,
|
||||
PlayerRoles = new List<PlayerRoles> { },
|
||||
AuthToken = Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
Players.Insert(newFullPlayer);
|
||||
|
||||
long dormRoomId = RoomDB.CloneDormRoom(newFullPlayer.PlayerId);
|
||||
newFullPlayer.Player.PlayerExtra.DormRoomId = dormRoomId;
|
||||
|
||||
var playerClub = ClubsDB.CreatePlayerClub(newFullPlayer.PlayerId);
|
||||
newFullPlayer.Player.PlayerExtra.PlayerClubId = playerClub.ClubId;
|
||||
|
||||
SettingsPlayers.Insert(new FullPlayer
|
||||
{
|
||||
PlayerId = newFullPlayer.PlayerId,
|
||||
Player = new Player
|
||||
{
|
||||
PlayerExtra = new PlayerExtra
|
||||
{
|
||||
Settings = new List<Setting> // can someone fix orientation, and new accounts
|
||||
{
|
||||
new Setting { Key = "Recroom.AccountCreation.HasStarted", Value = "False" },
|
||||
//new Setting { Key = "Recroom.AccountCreation.HasChosenUsername", Value = "False" },
|
||||
//new Setting { Key = "Recroom.AccountCreation.HasCreatedPassword", Value = "False" },
|
||||
//new Setting { Key = "Recroom.AccountCreation.HasFinished", Value = "False" },
|
||||
//new Setting { Key = "TUTORIAL_COMPLETE_MASK", Value = "1" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
EquipmentPlayers.Insert(new FullPlayer
|
||||
{
|
||||
PlayerId = newFullPlayer.PlayerId,
|
||||
Player = new Player
|
||||
{
|
||||
PlayerExtra = new PlayerExtra
|
||||
{
|
||||
OwnedEquipment = new List<OwnedEquipmentItem>()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
EnsureDefaultEquipment(newFullPlayer);
|
||||
|
||||
Players.Update(newFullPlayer);
|
||||
|
||||
_ = SendNewAccountWebhookAsync(newFullPlayer.Player.DisplayName, newFullPlayer.PlayerId, newFullPlayer.Player.CreatedAt, platform, platformId);
|
||||
|
||||
return newFullPlayer;
|
||||
}
|
||||
|
||||
public static void UpdateDormRoomId(long playerId, long dormRoomId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player?.PlayerExtra == null) return;
|
||||
player.Player.PlayerExtra.DormRoomId = dormRoomId;
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
/*public static void platformidstring()
|
||||
{
|
||||
var col = PlayerDBFile.GetCollection("Players");
|
||||
var docs = col.FindAll().ToList();
|
||||
int count = 0;
|
||||
|
||||
foreach (var doc in docs)
|
||||
{
|
||||
if (!doc.ContainsKey("PlatformIds")) continue;
|
||||
|
||||
var platformIds = doc["PlatformIds"].AsArray;
|
||||
if (platformIds == null) continue;
|
||||
|
||||
bool changed = false;
|
||||
|
||||
foreach (var entry in platformIds)
|
||||
{
|
||||
var pidDoc = entry.AsDocument;
|
||||
if (pidDoc == null) continue;
|
||||
|
||||
var current = pidDoc["PlatformId"];
|
||||
if (current == null || current.IsNull || current.Type == BsonType.String) continue;
|
||||
|
||||
pidDoc["PlatformId"] = new BsonValue(current.RawValue?.ToString() ?? "0");
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) continue;
|
||||
|
||||
col.Update(doc);
|
||||
count++;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Migrated {count} players from ulong platformids to string");
|
||||
}*/
|
||||
|
||||
public static bool GetLogins(Platforms platform, string platformId, out List<CachedLogins> accounts)
|
||||
{
|
||||
var results = Players.FindAll()
|
||||
.Where(p => p.PlatformIds != null &&
|
||||
p.PlatformIds.Any(pid => pid.Platform == platform &&
|
||||
string.Equals(pid.PlatformId.ToString(), platformId, StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderByDescending(x => x.Player.LastLoginAt)
|
||||
.ToList();
|
||||
|
||||
accounts = results.Select(p => new CachedLogins
|
||||
{
|
||||
accountId = p.PlayerId,
|
||||
lastLoginTime = p.Player.LastLoginAt,
|
||||
platform = platform,
|
||||
platformId = platformId
|
||||
}).ToList();
|
||||
|
||||
return accounts.Count > 0;
|
||||
}
|
||||
|
||||
private static PlayerDTOBase MapToDTO(FullPlayer player, bool accountMe)
|
||||
{
|
||||
if (player.Player == null)
|
||||
throw new InvalidOperationException($"The player {player.PlayerId} has null Player data.");
|
||||
|
||||
int platformFlags = player.PlatformIds?.Aggregate(0, (acc, pid) => acc | (int)pid.Platform) ?? 0;
|
||||
var p = player.Player;
|
||||
|
||||
PlayerDTOBase dto = accountMe ? new PlayerMeDTO() : new PlayerDTO();
|
||||
|
||||
dto.accountId = player.PlayerId;
|
||||
dto.username = p.Username;
|
||||
dto.displayName = p.DisplayName;
|
||||
dto.profileImage = p.ProfileImage;
|
||||
dto.isJunior = p.IsJunior;
|
||||
dto.createdAt = p.CreatedAt;
|
||||
dto.bannerImage = p.BannerImage;
|
||||
dto.platforms = platformFlags;
|
||||
dto.personalPronouns = p.PronounFlags;
|
||||
dto.identityFlags = p.IdentityFlags;
|
||||
dto.displayEmoji = p.DisplayEmoji;
|
||||
|
||||
if (accountMe && dto is PlayerMeDTO meDto)
|
||||
{
|
||||
meDto.availableUsernameChanges = p.AvailableUsernameChanges;
|
||||
meDto.birthday = p.Birthday;
|
||||
meDto.email = p.Email;
|
||||
meDto.phone = null;
|
||||
|
||||
var rep = p.Reputation ?? new Reputation();
|
||||
meDto.reputation = new Reputation
|
||||
{
|
||||
AccountId = player.PlayerId,
|
||||
IsCheerful = rep.IsCheerful,
|
||||
Noteriety = rep.Noteriety,
|
||||
SelectedCheer = rep.SelectedCheer,
|
||||
CheerCredit = rep.CheerCredit,
|
||||
CheerGeneral = rep.CheerGeneral,
|
||||
CheerHelpful = rep.CheerHelpful,
|
||||
CheerCreative = rep.CheerCreative,
|
||||
CheerGreatHost = rep.CheerGreatHost,
|
||||
CheerSportsman = rep.CheerSportsman,
|
||||
SubscriberCount = rep.SubscriberCount,
|
||||
SubscribedCount = rep.SubscribedCount
|
||||
};
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public static List<PlayerDTOBase> GetAccountsBulk(List<long> playerIds)
|
||||
{
|
||||
var players = Players.Find(x => playerIds.Contains(x.PlayerId)).ToList();
|
||||
return players.Select(p => MapToDTO(p, false)).OrderBy(a => a.accountId).ToList();
|
||||
}
|
||||
|
||||
public static PlayerDTOBase GetPlayerDTOById(long playerId, bool accountMe = false)
|
||||
{
|
||||
var publicDto = GetAccountsBulk(new List<long> { playerId }).FirstOrDefault();
|
||||
return publicDto;
|
||||
}
|
||||
|
||||
public static List<long> GetInfluencerIds()
|
||||
{
|
||||
return Players.FindAll()
|
||||
.Where(p => p.Player?.PlayerExtra?.Influencer != null
|
||||
&& p.Player.PlayerExtra.Influencer.IsInfluencer)
|
||||
.Select(p => p.PlayerId)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static PlayerMeDTO? GetAccountMe(long accountId)
|
||||
{
|
||||
var player = Players.FindById(accountId);
|
||||
if (player == null) return null;
|
||||
return MapToDTO(player, true) as PlayerMeDTO;
|
||||
}
|
||||
|
||||
public static bool IsBanned(long playerId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player == null) return false;
|
||||
|
||||
return (bool)player.Player.PlayerExtra.ModerationBlockDetails.IsBan;
|
||||
}
|
||||
|
||||
public static bool ApplyBan(long playerId, string message, ReportCategory category = ReportCategory.Moderator)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player == null) return false;
|
||||
|
||||
player.Player ??= new Player();
|
||||
player.Player.PlayerExtra ??= new PlayerExtra();
|
||||
player.Player.PlayerExtra.ModerationBlockDetails = new ModerationBlockDetails
|
||||
{
|
||||
IsBan = true,
|
||||
ReportCategory = category,
|
||||
Duration = 2147483647,
|
||||
Message = message,
|
||||
ModerationSetUnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
Players.Update(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static FullPlayer? GetCurrentPlayer(long playerId)
|
||||
{
|
||||
return Players.FindById(playerId);
|
||||
}
|
||||
|
||||
public static bool SetAvatar(long accountId, Avatar avatar)
|
||||
{
|
||||
var player = Players.FindById(accountId);
|
||||
if (player == null || player.Player == null) return false;
|
||||
player.Player.PlayerExtra.Avatar = avatar;
|
||||
return Players.Update(player);
|
||||
}
|
||||
|
||||
public static void SaveOutfit(long playerId, SavedOutfitRequest request)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player?.PlayerExtra == null) return;
|
||||
|
||||
player.Player.PlayerExtra.SavedAvatars ??= new List<SavedOutfit>();
|
||||
|
||||
var existing = player.Player.PlayerExtra.SavedAvatars.FirstOrDefault(s => s.Slot == request.Slot);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.DataVersion = request.DataVersion;
|
||||
existing.OutfitSelections = request.LegacyData?.SelectionsV1 ?? existing.OutfitSelections;
|
||||
existing.OutfitSelectionsV2 = request.LegacyData?.SelectionsV2 ?? existing.OutfitSelectionsV2;
|
||||
existing.FaceFeatures = request.LegacyData?.FaceFeatures ?? existing.FaceFeatures;
|
||||
existing.SkinColor = request.LegacyData?.SkinColor ?? existing.SkinColor;
|
||||
existing.HairColor = request.LegacyData?.HairColor ?? existing.HairColor;
|
||||
existing.CustomizationSettings = request.CustomizationSettings;
|
||||
existing.Selections = request.Selections;
|
||||
existing.Name = request.Name;
|
||||
existing.Accessibility = request.Accessibility;
|
||||
existing.ThumbnailFileName = request.ThumbnailFileName ?? existing.ThumbnailFileName;
|
||||
}
|
||||
else
|
||||
{
|
||||
player.Player.PlayerExtra.SavedAvatars.Add(new SavedOutfit
|
||||
{
|
||||
Slot = request.Slot,
|
||||
DataVersion = request.DataVersion,
|
||||
OutfitSelections = request.LegacyData?.SelectionsV1 ?? "",
|
||||
OutfitSelectionsV2 = request.LegacyData?.SelectionsV2 ?? "",
|
||||
FaceFeatures = request.LegacyData?.FaceFeatures ?? "",
|
||||
SkinColor = request.LegacyData?.SkinColor ?? "",
|
||||
HairColor = request.LegacyData?.HairColor ?? "",
|
||||
CustomizationSettings = request.CustomizationSettings,
|
||||
Selections = request.Selections,
|
||||
Name = request.Name,
|
||||
Accessibility = request.Accessibility,
|
||||
ThumbnailFileName = request.ThumbnailFileName
|
||||
});
|
||||
}
|
||||
|
||||
if (request.Slot == 0)
|
||||
{
|
||||
player.Player.PlayerExtra.Avatar ??= new Avatar();
|
||||
player.Player.PlayerExtra.Avatar.DataVersion = request.DataVersion;
|
||||
player.Player.PlayerExtra.Avatar.OutfitSelections = request.LegacyData?.SelectionsV1 ?? player.Player.PlayerExtra.Avatar.OutfitSelections;
|
||||
player.Player.PlayerExtra.Avatar.OutfitSelectionsV2 = request.LegacyData?.SelectionsV2 ?? player.Player.PlayerExtra.Avatar.OutfitSelectionsV2;
|
||||
player.Player.PlayerExtra.Avatar.FaceFeatures = request.LegacyData?.FaceFeatures ?? player.Player.PlayerExtra.Avatar.FaceFeatures;
|
||||
player.Player.PlayerExtra.Avatar.SkinColor = request.LegacyData?.SkinColor ?? player.Player.PlayerExtra.Avatar.SkinColor;
|
||||
player.Player.PlayerExtra.Avatar.HairColor = request.LegacyData?.HairColor ?? player.Player.PlayerExtra.Avatar.HairColor;
|
||||
player.Player.PlayerExtra.Avatar.CustomizationSettings = request.CustomizationSettings;
|
||||
if (request.Selections != null)
|
||||
{
|
||||
player.Player.PlayerExtra.Avatar.CustomAvatarItems = request.Selections
|
||||
.Select(s => new CustomAvatarItem
|
||||
{
|
||||
BodyPart = s.BodyPart,
|
||||
CustomAvatarItemId = s.CustomAvatarItemId ?? ""
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
public static List<Setting> GetPlayerSettings(long playerId)
|
||||
{
|
||||
var player = GetOrMigrateSettings(playerId);
|
||||
PlayerDB.SetPlayerSetting("Growth.LastEmailPromptTime", DateTime.UtcNow.ToString("o"), playerId);
|
||||
return player?.Player?.PlayerExtra?.Settings ?? new List<Setting>();
|
||||
}
|
||||
|
||||
private static FullPlayer? GetOrMigrateSettings(long playerId)
|
||||
{
|
||||
var player = SettingsPlayers.FindById(playerId);
|
||||
if (player != null) return player;
|
||||
|
||||
var oldPlayer = Players.FindById(playerId);
|
||||
if (oldPlayer?.Player?.PlayerExtra == null) return null;
|
||||
|
||||
var migrated = new FullPlayer
|
||||
{
|
||||
PlayerId = oldPlayer.PlayerId,
|
||||
Player = new Player
|
||||
{
|
||||
PlayerExtra = new PlayerExtra
|
||||
{
|
||||
Settings = oldPlayer.Player.PlayerExtra.Settings ?? new List<Setting>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SettingsPlayers.Insert(migrated);
|
||||
|
||||
oldPlayer.Player.PlayerExtra.Settings = null;
|
||||
Players.Update(oldPlayer);
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
public static void SetPlayerSetting(string key, string value, long playerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || playerId <= 0) return;
|
||||
if (key is "SplitTestAssignedSegments") return;
|
||||
|
||||
var player = GetOrMigrateSettings(playerId);
|
||||
if (player == null || player.Player == null) return;
|
||||
|
||||
var settings = player.Player.PlayerExtra.Settings;
|
||||
var existingSetting = settings.FirstOrDefault(s => s.Key == key);
|
||||
|
||||
if (existingSetting != null)
|
||||
existingSetting.Value = value;
|
||||
else
|
||||
settings.Add(new Setting { Key = key, Value = value });
|
||||
|
||||
SettingsPlayers.Update(player);
|
||||
}
|
||||
|
||||
public static void DeletePlayerSetting(string key, long playerId) // for 2025
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || playerId <= 0) return;
|
||||
|
||||
var player = GetOrMigrateSettings(playerId);
|
||||
if (player?.Player?.PlayerExtra?.Settings == null) return;
|
||||
|
||||
player.Player.PlayerExtra.Settings.RemoveAll(s => s.Key == key);
|
||||
SettingsPlayers.Update(player);
|
||||
}
|
||||
|
||||
public static List<PlayerProgressionDTO> GetProgressionBulk(List<long> playerIds)
|
||||
{
|
||||
var players = Players.Find(x => playerIds.Contains(x.PlayerId)).ToList();
|
||||
return players.Select(p => new PlayerProgressionDTO
|
||||
{
|
||||
PlayerId = p.PlayerId,
|
||||
Level = p.Player?.Level ?? 1,
|
||||
XP = p.Player?.XP ?? 0
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static List<Reputation> GetReputationBulk(List<long> playerIds)
|
||||
{
|
||||
var players = Players.Find(x => playerIds.Contains(x.PlayerId)).ToList();
|
||||
return players.Select(p =>
|
||||
{
|
||||
var rep = p.Player?.Reputation ?? new Reputation();
|
||||
return new Reputation
|
||||
{
|
||||
AccountId = p.PlayerId,
|
||||
IsCheerful = true,
|
||||
Noteriety = rep.Noteriety,
|
||||
SelectedCheer = rep.SelectedCheer,
|
||||
CheerCredit = rep.CheerCredit,
|
||||
CheerGeneral = rep.CheerGeneral,
|
||||
CheerHelpful = rep.CheerHelpful,
|
||||
CheerCreative = rep.CheerCreative,
|
||||
CheerGreatHost = rep.CheerGreatHost,
|
||||
CheerSportsman = rep.CheerSportsman,
|
||||
SubscriberCount = rep.SubscriberCount,
|
||||
SubscribedCount = rep.SubscribedCount
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static Heartbeat GetPlayerHeartbeat(long playerId)
|
||||
{
|
||||
var player = Players.FindOne(x => x.PlayerId == playerId);
|
||||
var hb = player?.Player?.PlayerExtra?.Heartbeat ?? new Heartbeat();
|
||||
hb.playerId = playerId;
|
||||
return hb;
|
||||
}
|
||||
|
||||
public static List<Heartbeat> GetPlayerHeartbeatsBulk(List<long> playerIds)
|
||||
{
|
||||
var players = Players.Find(x => playerIds.Contains(x.PlayerId)).ToList();
|
||||
return players.Select(p =>
|
||||
{
|
||||
var hb = p.Player?.PlayerExtra?.Heartbeat ?? new Heartbeat();
|
||||
hb.playerId = p.PlayerId;
|
||||
return hb;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static Heartbeat? UpdatePlayerHeartbeat(
|
||||
long playerId,
|
||||
RoomInstance? roomInstance,
|
||||
bool online = true,
|
||||
Platforms platform = Platforms.All,
|
||||
DeviceClasses deviceClasses = DeviceClasses.Unknown)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player == null) return null;
|
||||
|
||||
player.Player.PlayerExtra ??= new PlayerExtra();
|
||||
player.Player.PlayerExtra.Heartbeat ??= new Heartbeat();
|
||||
|
||||
var hb = player.Player.PlayerExtra.Heartbeat;
|
||||
hb.roomInstance = online ? roomInstance : null;
|
||||
hb.errorCode = 0;
|
||||
hb.isOnline = online;
|
||||
|
||||
if (hb.roomInstance != null)
|
||||
hb.roomInstance.dataBlob = "";
|
||||
|
||||
Players.Update(player);
|
||||
return hb;
|
||||
}
|
||||
|
||||
public static PlayerRelationship GetOrCreateRelationship(long ownerId, long targetId)
|
||||
{
|
||||
var player = Players.FindById(ownerId);
|
||||
if (player == null || player.Player == null) return null;
|
||||
|
||||
player.Player.Relationships ??= new List<PlayerRelationship>();
|
||||
|
||||
var rel = player.Player.Relationships.FirstOrDefault(r => r.PlayerId == targetId);
|
||||
|
||||
if (rel == null)
|
||||
{
|
||||
rel = new PlayerRelationship { PlayerId = targetId };
|
||||
player.Player.Relationships.Add(rel);
|
||||
}
|
||||
|
||||
Players.Update(player);
|
||||
return rel;
|
||||
}
|
||||
|
||||
public static bool HasCheeredRoom(long playerId, long roomId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
return player?.Player?.CheeredRooms.Contains(roomId) ?? false;
|
||||
}
|
||||
|
||||
public static bool HasFavoritedRoom(long playerId, long roomId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
return player?.Player?.FavoritedRooms.Contains(roomId) ?? false;
|
||||
}
|
||||
|
||||
public static void UpdateLastLoginAt(long playerId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player == null) return;
|
||||
player.Player.LastLoginAt = DateTime.UtcNow;
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
public static DateTime? GetRoomLastVisitedAt(long playerId, long roomId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player?.RoomVisits == null) return null;
|
||||
return player.Player.RoomVisits.FirstOrDefault(rv => rv.RoomId == roomId)?.LastVisitedAt;
|
||||
}
|
||||
|
||||
public static void UpdateRoomLastVisitedAt(long playerId, long roomId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player == null) return;
|
||||
|
||||
player.Player.RoomVisits ??= new List<RoomVisit>();
|
||||
|
||||
var existing = player.Player.RoomVisits.FirstOrDefault(rv => rv.RoomId == roomId);
|
||||
if (existing != null)
|
||||
existing.LastVisitedAt = DateTime.UtcNow;
|
||||
else
|
||||
player.Player.RoomVisits.Add(new RoomVisit { RoomId = roomId, LastVisitedAt = DateTime.UtcNow });
|
||||
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
public static void AddCheerRoom(long playerId, long roomId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player == null) return;
|
||||
if (!player.Player.CheeredRooms.Contains(roomId))
|
||||
{
|
||||
player.Player.CheeredRooms.Add(roomId);
|
||||
Players.Update(player);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveCheerRoom(long playerId, long roomId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player == null) return;
|
||||
if (player.Player.CheeredRooms.Remove(roomId))
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
public static void AddFavoriteRoom(long playerId, long roomId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player == null) return;
|
||||
if (!player.Player.FavoritedRooms.Contains(roomId))
|
||||
{
|
||||
player.Player.FavoritedRooms.Add(roomId);
|
||||
Players.Update(player);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveFavoriteRoom(long playerId, long roomId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player == null) return;
|
||||
if (player.Player.FavoritedRooms.Remove(roomId))
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
public static string? GetPlayerData(long playerId, long roomId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player?.PlayerExtra?.RoomPlayerData == null) return null;
|
||||
return player.Player.PlayerExtra.RoomPlayerData
|
||||
.FirstOrDefault(d => d.RoomId == roomId)?.Data;
|
||||
}
|
||||
|
||||
public static bool SetPlayerData(long playerId, long roomId, string data)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player?.PlayerExtra == null) return false;
|
||||
|
||||
player.Player.PlayerExtra.RoomPlayerData ??= new List<RoomPlayerData>();
|
||||
|
||||
var existing = player.Player.PlayerExtra.RoomPlayerData.FirstOrDefault(d => d.RoomId == roomId);
|
||||
if (existing != null)
|
||||
existing.Data = data;
|
||||
else
|
||||
player.Player.PlayerExtra.RoomPlayerData.Add(new RoomPlayerData { RoomId = roomId, Data = data });
|
||||
|
||||
return Players.Update(player);
|
||||
}
|
||||
|
||||
public static List<RoomVisit> GetPlayerRoomVisits(long playerId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
return player?.Player?.RoomVisits ?? new List<RoomVisit>();
|
||||
}
|
||||
|
||||
public static void SupportInfluencer(long playerId, long influencerAccountId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player == null) return;
|
||||
|
||||
player.Player.PlayerExtra.Influencer.SupportingInfluencer = influencerAccountId;
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
public static async Task SpreadEarningsToInfluencer(long playerId, int tokensToDivide)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player == null) return;
|
||||
|
||||
var influencerAccountId = player.Player.PlayerExtra.Influencer.SupportingInfluencer;
|
||||
|
||||
var gift = GiftsDB.CreateGift(
|
||||
toPlayerId: influencerAccountId,
|
||||
fromPlayerId: 1,
|
||||
currency: (int)(tokensToDivide * 0.25),
|
||||
currencyType: 2,
|
||||
balanceType: -2,
|
||||
giftContext: (GiftType)110100,
|
||||
giftRarity: 50,
|
||||
message: "Earnings from your supporting player " + player.Player.Username + "!",
|
||||
platform: -1,
|
||||
platformsToSpawnOn: -1
|
||||
);
|
||||
|
||||
//Console.WriteLine($"[PlayerDB] Created gift for influencer {influencerAccountId}.");
|
||||
|
||||
await Utils.NotiController.NotiController.SendEvent(influencerAccountId, "31", GiftsDB.MapToDTO(gift));
|
||||
|
||||
//Console.WriteLine($"[PlayerDB] Sent event notification to influencer {influencerAccountId} for the gift JSON: {GiftsDB.MapToDTO(gift)}");
|
||||
}
|
||||
|
||||
public static void SetPlayerCheer(CheerCategory cheer, long playerId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player == null) return;
|
||||
|
||||
player.Player.Reputation.SelectedCheer = cheer;
|
||||
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
public static long GetMyInfluencer(long playerId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
return player?.Player?.PlayerExtra.Influencer?.SupportingInfluencer ?? 0;
|
||||
}
|
||||
|
||||
public static int GetPlayerPhotoTaggingSetting(long playerId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
return player?.Player?.PlayerExtra?.PhotoTaggingSetting ?? 0;
|
||||
}
|
||||
|
||||
public static int SetPlayerPhotoTaggingSetting(long playerId, int setting)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player?.PlayerExtra == null) return 0;
|
||||
player.Player.PlayerExtra.PhotoTaggingSetting = setting;
|
||||
Players.Update(player);
|
||||
return setting;
|
||||
}
|
||||
|
||||
public static bool TryIncrementDailyImageUpload(long playerId)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player?.PlayerExtra == null) return false;
|
||||
|
||||
var extra = player.Player.PlayerExtra;
|
||||
|
||||
if (AsUtc(extra.ImagesUploadedTodayDate).Date != DateTime.UtcNow.Date)
|
||||
{
|
||||
extra.ImagesUploadedToday = 0;
|
||||
extra.ImagesUploadedTodayDate = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
if (extra.ImagesUploadedToday >= 50) return false;
|
||||
|
||||
extra.ImagesUploadedToday++;
|
||||
Players.Update(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<bool> IsSteamPlayerInSupportedGame(ulong platformId)
|
||||
{
|
||||
if (platformId == 0) return false;
|
||||
|
||||
string apiKey = ServerConfig.SteamWebApiKey;
|
||||
if (string.IsNullOrWhiteSpace(apiKey)) return false;
|
||||
|
||||
string steamId = platformId.ToString();
|
||||
string requestUrl =
|
||||
$"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={Uri.EscapeDataString(apiKey)}&steamids={Uri.EscapeDataString(steamId)}";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await SteamApiClient.GetAsync(requestUrl);
|
||||
if (!response.IsSuccessStatusCode) return false;
|
||||
|
||||
string json = await response.Content.ReadAsStringAsync();
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(json);
|
||||
|
||||
if (!doc.RootElement.TryGetProperty("response", out var responseNode)) return false;
|
||||
if (!responseNode.TryGetProperty("players", out var playersNode)) return false;
|
||||
if (playersNode.ValueKind != System.Text.Json.JsonValueKind.Array || playersNode.GetArrayLength() == 0) return false;
|
||||
|
||||
var player = playersNode[0];
|
||||
if (!player.TryGetProperty("gameid", out var gameIdNode)) return false;
|
||||
|
||||
string? gameIdText = gameIdNode.GetString();
|
||||
return gameIdText == "480" || gameIdText == "471710";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> ValidateSteamUserTicketAsync(string ticketPayload, ulong? platformId)
|
||||
{
|
||||
if (platformId == null || platformId == 0 || string.IsNullOrWhiteSpace(ticketPayload))
|
||||
return false;
|
||||
|
||||
string apiKey = ServerConfig.SteamWebApiKey;
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
return false;
|
||||
|
||||
string? ticketHex = ExtractSteamTicketHex(ticketPayload);
|
||||
if (string.IsNullOrWhiteSpace(ticketHex))
|
||||
return false;
|
||||
|
||||
var firstAttempt = await AuthenticateSteamUserTicketAsync(apiKey, ticketHex, "471710", platformId);
|
||||
if (firstAttempt.isValid)
|
||||
return true;
|
||||
|
||||
var secondAttempt = await AuthenticateSteamUserTicketAsync(apiKey, ticketHex, "480", platformId);
|
||||
if (secondAttempt.isValid)
|
||||
return true;
|
||||
|
||||
if (!firstAttempt.ticketForOtherApp)
|
||||
return false;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static async Task<(bool isValid, bool ticketForOtherApp)> AuthenticateSteamUserTicketAsync(
|
||||
string apiKey,
|
||||
string ticketHex,
|
||||
string appId,
|
||||
ulong? expectedSteamId)
|
||||
{
|
||||
string requestUrl =
|
||||
$"https://api.steampowered.com/ISteamUserAuth/AuthenticateUserTicket/v1/?key={Uri.EscapeDataString(apiKey)}&appid={Uri.EscapeDataString(appId)}&ticket={Uri.EscapeDataString(ticketHex)}";
|
||||
|
||||
Console.WriteLine($"[AUTH] Verifying ticket URL: {requestUrl}");
|
||||
|
||||
try
|
||||
{
|
||||
var response = await SteamApiClient.GetAsync(requestUrl);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return (false, false);
|
||||
|
||||
string json = await response.Content.ReadAsStringAsync();
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(json);
|
||||
|
||||
if (!doc.RootElement.TryGetProperty("response", out var responseNode))
|
||||
return (false, false);
|
||||
|
||||
if (responseNode.TryGetProperty("error", out var errorNode)
|
||||
&& errorNode.TryGetProperty("errorcode", out var errorCodeNode)
|
||||
&& errorCodeNode.TryGetInt32(out int errorCode)
|
||||
&& errorCode == 102)
|
||||
{
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
if (!responseNode.TryGetProperty("params", out var paramsNode))
|
||||
return (false, false);
|
||||
|
||||
if (!paramsNode.TryGetProperty("result", out var resultNode)
|
||||
|| !string.Equals(resultNode.GetString(), "OK", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
if (!paramsNode.TryGetProperty("steamid", out var steamIdNode))
|
||||
return (false, false);
|
||||
|
||||
string? steamIdText = steamIdNode.GetString();
|
||||
if (!ulong.TryParse(steamIdText, out ulong ticketSteamId))
|
||||
return (false, false);
|
||||
|
||||
return (ticketSteamId == expectedSteamId, false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (false, false);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SetAllBirthdays() // temp for now?
|
||||
{
|
||||
var all = Players.FindAll().ToList();
|
||||
foreach (var player in all)
|
||||
{
|
||||
if (player?.Player == null) continue;
|
||||
player.Player.Birthday = new DateTime(2000, 1, 1, 1, 0, 0, DateTimeKind.Utc);
|
||||
Players.Update(player);
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string? ExtractSteamTicketHex(string ticketPayload)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ticketPayload))
|
||||
return null;
|
||||
|
||||
string input = ticketPayload.Trim();
|
||||
|
||||
if ((input.StartsWith("'") && input.EndsWith("'"))
|
||||
|| (input.StartsWith("\"") && input.EndsWith("\"")))
|
||||
{
|
||||
input = input[1..^1];
|
||||
}
|
||||
|
||||
string candidate = input;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(input);
|
||||
if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object
|
||||
&& doc.RootElement.TryGetProperty("Ticket", out var ticketNode)
|
||||
&& ticketNode.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
{
|
||||
candidate = ticketNode.GetString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
var match = Regex.Match(input, "\"Ticket\"\\s*:\\s*\"(?<ticket>[0-9a-fA-F]+)\"", RegexOptions.IgnoreCase);
|
||||
if (match.Success)
|
||||
candidate = match.Groups["ticket"].Value;
|
||||
}
|
||||
|
||||
candidate = candidate.Trim().Trim('\'', '"');
|
||||
|
||||
if (string.IsNullOrWhiteSpace(candidate))
|
||||
return null;
|
||||
|
||||
if (!candidate.All(Uri.IsHexDigit))
|
||||
return null;
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static readonly HttpClient OculusApiClient = new HttpClient();
|
||||
private const string OculusNonceValidateUrl = "https://graph.oculus.com/user_nonce_validate";
|
||||
private static readonly HashSet<int> OculusIgnoreErrorCodes = new() { 1, 2 };
|
||||
private const int OculusMaxAttempts = 3;
|
||||
|
||||
public static async Task<bool> ValidateOculusUserNonceAsync(string? platformAuth, string? platformId)
|
||||
{
|
||||
if (!ServerConfig.OculusAuth)
|
||||
{
|
||||
// Console.WriteLine($"[AUTH] ServerConfig oculus off accepting platformId={platformId} unverified");
|
||||
return true;
|
||||
}
|
||||
Console.WriteLine($"[AUTH] Validating oculus platformId={platformId} nonce={platformAuth}");
|
||||
|
||||
string appSecret = ServerConfig.OculusAppSecret;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(platformId) || !platformId.All(char.IsDigit))
|
||||
{
|
||||
Console.WriteLine($"[AUTH] missing or non numeric oculus platform_id '{platformId}'");
|
||||
return false;
|
||||
}
|
||||
|
||||
var auth = ExtractOculusNonce(platformAuth);
|
||||
if (auth == null)
|
||||
{
|
||||
Console.WriteLine($"[AUTH] Can't extract platformId={platformId}'s nonce!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var form = new Dictionary<string, string>
|
||||
{
|
||||
["nonce"] = auth.Value.nonce,
|
||||
["user_id"] = platformId,
|
||||
["access_token"] = $"OC|{auth.Value.appId}|{appSecret}"
|
||||
};
|
||||
Console.WriteLine($"[AUTH] Validating oculus platformId={platformId} nonce={auth.Value.nonce} appId={auth.Value.appId}");
|
||||
|
||||
var last = (isValid: false, retryable: false, reason: "not attempted");
|
||||
|
||||
for (int attempt = 1; attempt <= OculusMaxAttempts; attempt++)
|
||||
{
|
||||
last = await ValidateOculusNonceOnceAsync(form);
|
||||
Console.WriteLine($"[AUTH] Oculus validation attempt {attempt} for platformId={platformId} result: isValid={last.isValid}, retryable={last.retryable}, reason={last.reason}");
|
||||
|
||||
if (last.isValid)
|
||||
return true;
|
||||
|
||||
if (!last.retryable || attempt == OculusMaxAttempts)
|
||||
break;
|
||||
|
||||
await Task.Delay(attempt * attempt * 250);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[AUTH] Oculus error probably platformId={platformId}: {last.reason}");
|
||||
return false;
|
||||
}
|
||||
private static async Task<(bool isValid, bool retryable, string reason)> ValidateOculusNonceOnceAsync(Dictionary<string, string> form)
|
||||
{
|
||||
HttpResponseMessage response;
|
||||
|
||||
try
|
||||
{
|
||||
response = await OculusApiClient.PostAsync(OculusNonceValidateUrl, new FormUrlEncodedContent(form));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, true, $"request failed: {ex.Message}");
|
||||
}
|
||||
|
||||
string json;
|
||||
|
||||
try
|
||||
{
|
||||
json = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, true, $"reading response failed: {ex.Message}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.ValueKind != System.Text.Json.JsonValueKind.Object)
|
||||
return (false, true, $"HTTP Error {(int)response.StatusCode} (2)");
|
||||
|
||||
if (root.TryGetProperty("error", out var errorNode)
|
||||
&& errorNode.ValueKind == System.Text.Json.JsonValueKind.Object)
|
||||
{
|
||||
int? code = errorNode.TryGetProperty("code", out var codeNode)
|
||||
&& codeNode.TryGetInt32(out int parsedCode) ? parsedCode : null;
|
||||
|
||||
string message = errorNode.TryGetProperty("message", out var messageNode)
|
||||
&& messageNode.ValueKind == System.Text.Json.JsonValueKind.String
|
||||
? messageNode.GetString() ?? "no message" : "no message";
|
||||
|
||||
bool transient = (errorNode.TryGetProperty("is_transient", out var transientNode)
|
||||
&& transientNode.ValueKind == System.Text.Json.JsonValueKind.True)
|
||||
|| (code != null && OculusIgnoreErrorCodes.Contains(code.Value));
|
||||
|
||||
return (false, transient, $"graph error {(code?.ToString() ?? "?")}: {message}");
|
||||
}
|
||||
|
||||
if (!root.TryGetProperty("is_valid", out var validNode)
|
||||
|| validNode.ValueKind != System.Text.Json.JsonValueKind.True)
|
||||
{
|
||||
return (false, false, "nonce rejected");
|
||||
}
|
||||
Console.WriteLine($"[AUTH] Oculus nonce validated successfully for platformId={form["user_id"]} response: {json}");
|
||||
|
||||
return (true, false, string.Empty);
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
return (false, true, $"HTTP Error {(int)response.StatusCode}");
|
||||
}
|
||||
}
|
||||
|
||||
private static (string nonce, string appId)? ExtractOculusNonce(string? platformAuth)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(platformAuth))
|
||||
return null;
|
||||
|
||||
string input = platformAuth.Trim();
|
||||
|
||||
string? nonce = null;
|
||||
string? appId = null;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(input);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
{
|
||||
using var innerDoc = System.Text.Json.JsonDocument.Parse(root.GetString() ?? string.Empty);
|
||||
(nonce, appId) = ReadOculusAuthFields(innerDoc.RootElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
(nonce, appId) = ReadOculusAuthFields(root);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
var nonceMatch = Regex.Match(input, "\"Nonce\"\\s*:\\s*\"(?<nonce>[^\"]+)\"", RegexOptions.IgnoreCase);
|
||||
var appIdMatch = Regex.Match(input, "\"AppId\"\\s*:\\s*\"?(?<appId>\\d+)\"?", RegexOptions.IgnoreCase);
|
||||
|
||||
if (nonceMatch.Success)
|
||||
nonce = nonceMatch.Groups["nonce"].Value;
|
||||
|
||||
if (appIdMatch.Success)
|
||||
appId = appIdMatch.Groups["appId"].Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(nonce) || string.IsNullOrWhiteSpace(appId))
|
||||
return null;
|
||||
|
||||
if (!appId.All(char.IsDigit))
|
||||
return null;
|
||||
|
||||
return (nonce, appId);
|
||||
}
|
||||
|
||||
private static (string? nonce, string? appId) ReadOculusAuthFields(System.Text.Json.JsonElement element)
|
||||
{
|
||||
if (element.ValueKind != System.Text.Json.JsonValueKind.Object)
|
||||
return (null, null);
|
||||
|
||||
string? nonce = element.TryGetProperty("Nonce", out var nonceNode)
|
||||
&& nonceNode.ValueKind == System.Text.Json.JsonValueKind.String
|
||||
? nonceNode.GetString() : null;
|
||||
|
||||
string? appId = null;
|
||||
|
||||
if (element.TryGetProperty("AppId", out var appIdNode))
|
||||
{
|
||||
appId = appIdNode.ValueKind switch
|
||||
{
|
||||
System.Text.Json.JsonValueKind.String => appIdNode.GetString(),
|
||||
System.Text.Json.JsonValueKind.Number => appIdNode.GetRawText(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
return (nonce, appId);
|
||||
}
|
||||
|
||||
public static List<MessageData> GetMessages(long playerId)
|
||||
{
|
||||
var player = Players.FindOne(x => x.PlayerId == playerId);
|
||||
|
||||
if (player == null)
|
||||
return new List<MessageData>();
|
||||
|
||||
if (player.Player.PlayerExtra.Messages == null)
|
||||
player.Player.PlayerExtra.Messages = new List<MessageData>();
|
||||
|
||||
return player.Player.PlayerExtra.Messages;
|
||||
}
|
||||
|
||||
public static MessageData? AddMessage(long toPlayerId, long fromPlayerId, MessageType type, string? data, long? roomId = null, long? playerEventId = null)
|
||||
{
|
||||
var player = Players.FindOne(x => x.PlayerId == toPlayerId);
|
||||
|
||||
if (player == null)
|
||||
return null;
|
||||
|
||||
if (player.Player.PlayerExtra.Messages == null)
|
||||
player.Player.PlayerExtra.Messages = new List<MessageData>();
|
||||
|
||||
var msg = new MessageData()
|
||||
{
|
||||
Id = GenerateRandomMessageId(),
|
||||
FromPlayerId = fromPlayerId,
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = type,
|
||||
Data = data,
|
||||
RoomId = roomId,
|
||||
PlayerEventId = playerEventId
|
||||
};
|
||||
|
||||
player.Player.PlayerExtra.Messages.Add(msg);
|
||||
|
||||
Players.Update(player);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static void DeleteMessage(long playerId, long messageId)
|
||||
{
|
||||
var player = Players.FindOne(x => x.PlayerId == playerId);
|
||||
|
||||
if (player == null)
|
||||
return;
|
||||
|
||||
if (player.Player.PlayerExtra.Messages == null || player.Player.PlayerExtra.Messages.Count == 0)
|
||||
return;
|
||||
|
||||
var message = player.Player.PlayerExtra.Messages.FirstOrDefault(m => m.Id == messageId);
|
||||
|
||||
if (message == null)
|
||||
return;
|
||||
|
||||
player.Player.PlayerExtra.Messages.Remove(message);
|
||||
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
public static void DeleteMessagesBulk(long playerId, IEnumerable<long> messageIds)
|
||||
{
|
||||
var player = Players.FindOne(x => x.PlayerId == playerId);
|
||||
if (player == null)
|
||||
return;
|
||||
if (player.Player.PlayerExtra.Messages == null || player.Player.PlayerExtra.Messages.Count == 0)
|
||||
return;
|
||||
|
||||
var idsToDelete = new HashSet<long>(messageIds);
|
||||
if (idsToDelete.Count == 0)
|
||||
return;
|
||||
|
||||
int removed = player.Player.PlayerExtra.Messages.RemoveAll(m => idsToDelete.Contains(m.Id));
|
||||
if (removed > 0)
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
public static void StoreLoginDeviceInfo(long playerId, DeviceClasses? deviceClass, int? ver)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player?.Player?.PlayerExtra == null) return;
|
||||
|
||||
player.Player.PlayerExtra.Heartbeat ??= new Heartbeat();
|
||||
player.Player.PlayerExtra.Heartbeat.deviceClass = deviceClass ?? DeviceClasses.Unknown;
|
||||
//Console.WriteLine($"[PlayerDB] Outcome of if statement: ${ver.HasValue} and ${ver?.ToString()}");
|
||||
player.Player.PlayerExtra.Heartbeat.appVersion = ver.HasValue ? ver.Value.ToString() : ServerConfig.GameVersion.ToString();
|
||||
//Console.WriteLine($"[PlayerDB] Stored login device info for player {playerId}: deviceClass={player.Player.PlayerExtra.Heartbeat.deviceClass}, appVersion={player.Player.PlayerExtra.Heartbeat.appVersion}");
|
||||
|
||||
Players.Update(player);
|
||||
}
|
||||
|
||||
private static readonly Random random = new Random();
|
||||
private static readonly object randLock = new object();
|
||||
|
||||
private static long MakeNewPlayerID()
|
||||
{
|
||||
long id;
|
||||
do
|
||||
{
|
||||
lock (randLock)
|
||||
id = random.NextInt64(1, 2147483648L);
|
||||
} while (Players.FindById(id) != null);
|
||||
return id;
|
||||
}
|
||||
|
||||
public static long GenerateRandomMessageId()
|
||||
{
|
||||
lock (randLock)
|
||||
{
|
||||
byte[] buffer = new byte[8];
|
||||
random.NextBytes(buffer);
|
||||
return BitConverter.ToInt64(buffer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static class PasswordManager // [PasswordManager] if it was in rec room bruh
|
||||
{
|
||||
public static (bool valid, string? error) ValidatePassword(string password)
|
||||
{
|
||||
if (password.Length < 8)
|
||||
return (false, "Your password cannot be under 8 characters.");
|
||||
|
||||
if (!password.Any(char.IsUpper))
|
||||
return (false, "Password must contain a capital letter.");
|
||||
|
||||
if (!Regex.IsMatch(password, @"[!@#$%^&*()\-_=+\[\]{};':""|,.<>/?`~]"))
|
||||
return (false, "Password must contain a symbol.");
|
||||
|
||||
if (password.Count(char.IsDigit) < 2)
|
||||
return (false, "Password must contain 2 numbers minimum.");
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public static string HashPassword(string password)
|
||||
{
|
||||
byte[] salt = System.Security.Cryptography.RandomNumberGenerator.GetBytes(16);
|
||||
var pbkdf2 = new System.Security.Cryptography.Rfc2898DeriveBytes(password, salt, 100_000, System.Security.Cryptography.HashAlgorithmName.SHA256);
|
||||
byte[] hash = pbkdf2.GetBytes(32);
|
||||
byte[] combined = new byte[48];
|
||||
salt.CopyTo(combined, 0);
|
||||
hash.CopyTo(combined, 16);
|
||||
return Convert.ToBase64String(combined);
|
||||
}
|
||||
|
||||
public static bool VerifyPassword(string password, string storedHash)
|
||||
{
|
||||
byte[] combined = Convert.FromBase64String(storedHash);
|
||||
byte[] salt = combined[..16];
|
||||
byte[] storedBytes = combined[16..];
|
||||
var pbkdf2 = new System.Security.Cryptography.Rfc2898DeriveBytes(password, salt, 100_000, System.Security.Cryptography.HashAlgorithmName.SHA256);
|
||||
byte[] hash = pbkdf2.GetBytes(32);
|
||||
return System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(hash, storedBytes);
|
||||
}
|
||||
|
||||
public static bool ChangePassword(long playerId, string newPassword)
|
||||
{
|
||||
var player = Players.FindById(playerId);
|
||||
if (player == null) return false;
|
||||
|
||||
player.Password = HashPassword(newPassword);
|
||||
return Players.Update(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using LiteDB;
|
||||
using System.IO;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.ReportsDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public static class ReportsDB
|
||||
{
|
||||
public static LiteDatabase ReportsDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Reports.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<BugReport> Reports = ReportsDBFile.GetCollection<BugReport>("BugReports");
|
||||
|
||||
public static void Setup()
|
||||
{
|
||||
Reports.EnsureIndex(x => x.ReportId, unique: true);
|
||||
}
|
||||
|
||||
public static BugReport GetOrCreateReport(string reportId, long playerId)
|
||||
{
|
||||
var report = Reports.FindOne(x => x.ReportId == reportId);
|
||||
if (report == null)
|
||||
{
|
||||
report = new BugReport
|
||||
{
|
||||
ReportId = reportId,
|
||||
PlayerId = playerId
|
||||
};
|
||||
Reports.Insert(report);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
public static BugReport GetReport(string reportId)
|
||||
{
|
||||
return Reports.FindOne(x => x.ReportId == reportId);
|
||||
}
|
||||
|
||||
public static void UpdateReport(BugReport report)
|
||||
{
|
||||
Reports.Update(report);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using LiteDB;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.RoomCommentsDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class RoomCommentsDB
|
||||
{
|
||||
public static LiteDatabase CommentsDBFile =
|
||||
new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "RoomComments.db") + ";Connection=shared");
|
||||
|
||||
private static ILiteCollection<RoomComment> Col =>
|
||||
CommentsDBFile.GetCollection<RoomComment>("RoomComments");
|
||||
|
||||
private static long GetNextCommentId()
|
||||
{
|
||||
long id;
|
||||
do
|
||||
{
|
||||
id = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
} while (Col.FindById(id) != null);
|
||||
return id;
|
||||
}
|
||||
|
||||
public static RoomComment CreateComment(long roomId, long subRoomId, long accountId, string message, int style, float posX, float posY, float posZ)
|
||||
{
|
||||
var comment = new RoomComment
|
||||
{
|
||||
commentId = GetNextCommentId(),
|
||||
roomId = roomId,
|
||||
subRoomId = subRoomId,
|
||||
accountId = accountId,
|
||||
message = message,
|
||||
style = style,
|
||||
positionX = posX,
|
||||
positionY = posY,
|
||||
positionZ = posZ,
|
||||
createdAt = DateTime.UtcNow,
|
||||
unread = true
|
||||
};
|
||||
|
||||
Col.Insert(comment);
|
||||
return comment;
|
||||
}
|
||||
|
||||
public static List<RoomComment> GetComments(long roomId, int count, long minId)
|
||||
{
|
||||
var query = Col.Find(c => c.roomId == roomId);
|
||||
|
||||
if (minId >= 0)
|
||||
query = query.Where(c => c.commentId > minId);
|
||||
|
||||
return query
|
||||
.OrderByDescending(c => c.createdAt)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static RoomComment? GetComment(long commentId)
|
||||
=> Col.FindById(commentId);
|
||||
|
||||
public static bool MarkAsRead(long roomId, long commentId)
|
||||
{
|
||||
var comment = Col.FindOne(c => c.roomId == roomId && c.commentId == commentId);
|
||||
if (comment == null) return false;
|
||||
|
||||
comment.unread = false;
|
||||
return Col.Update(comment);
|
||||
}
|
||||
|
||||
public static bool DeleteComment(long commentId)
|
||||
=> Col.Delete(commentId);
|
||||
|
||||
public static bool DeleteCommentsByRoom(long roomId)
|
||||
{
|
||||
Col.DeleteMany(c => c.roomId == roomId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using LiteDB;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using static Vanadium.Classes.DBs.DBClasses.RoomConsumablesDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class RoomConsumablesDB
|
||||
{
|
||||
public static LiteDatabase RoomConsumablesDBFile = new LiteDatabase(
|
||||
"Filename=" + Path.Join(Program.dataDir, "DBs", "RoomConsumables.db") + ";Connection=shared");
|
||||
|
||||
public static readonly ILiteCollection<RoomConsumable> Consumables =
|
||||
RoomConsumablesDBFile.GetCollection<RoomConsumable>("RoomConsumables");
|
||||
|
||||
public static readonly ILiteCollection<RoomConsumableOwnership> Ownerships =
|
||||
RoomConsumablesDBFile.GetCollection<RoomConsumableOwnership>("RoomConsumableOwnerships");
|
||||
|
||||
public static readonly ILiteCollection<RoomCurrencyBalance> CurrencyBalances =
|
||||
RoomConsumablesDBFile.GetCollection<RoomCurrencyBalance>("RoomCurrencyBalances");
|
||||
|
||||
static RoomConsumablesDB()
|
||||
{
|
||||
Consumables.EnsureIndex(x => x.RoomId);
|
||||
Consumables.EnsureIndex(x => x.CreatorPlayerId);
|
||||
Ownerships.EnsureIndex(x => x.PlayerId);
|
||||
Ownerships.EnsureIndex(x => x.RoomConsumableId);
|
||||
CurrencyBalances.EnsureIndex(x => x.PlayerId);
|
||||
CurrencyBalances.EnsureIndex(x => x.CurrencyId);
|
||||
}
|
||||
|
||||
public static RoomConsumable? GetConsumable(Guid roomConsumableId)
|
||||
{
|
||||
var item = Consumables.FindById(roomConsumableId);
|
||||
return item == null || item.IsDeleted ? null : item;
|
||||
}
|
||||
|
||||
public static List<RoomConsumable> GetConsumablesForRoom(long roomId, int take = 200) =>
|
||||
Consumables.Find(c => c.RoomId == roomId && !c.IsDeleted)
|
||||
.OrderBy(c => c.CreatedAt)
|
||||
.Take(take)
|
||||
.ToList();
|
||||
|
||||
public static int CountConsumablesInRoom(long roomId) =>
|
||||
Consumables.Count(c => c.RoomId == roomId && !c.IsDeleted);
|
||||
|
||||
public static RoomConsumable CreateConsumable(
|
||||
long roomId, long creatorPlayerId, string name, string? description,
|
||||
string? imageName, int price, Guid? currencyId)
|
||||
{
|
||||
var item = new RoomConsumable
|
||||
{
|
||||
RoomConsumableId = Guid.NewGuid(),
|
||||
RoomId = roomId,
|
||||
CreatorPlayerId = creatorPlayerId,
|
||||
Name = name,
|
||||
Description = description,
|
||||
ImageName = imageName,
|
||||
Price = price,
|
||||
CurrencyId = currencyId,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
ModifiedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
Consumables.Insert(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public static bool UpdateConsumable(
|
||||
RoomConsumable item, string name, string? description,
|
||||
string? imageName, int price, Guid? currencyId)
|
||||
{
|
||||
item.Name = name;
|
||||
item.Description = description;
|
||||
item.ImageName = imageName;
|
||||
item.Price = price;
|
||||
item.CurrencyId = currencyId;
|
||||
item.ModifiedAt = DateTime.UtcNow;
|
||||
return Consumables.Update(item);
|
||||
}
|
||||
public static bool DeleteConsumable(Guid roomConsumableId)
|
||||
{
|
||||
var item = Consumables.FindById(roomConsumableId);
|
||||
if (item == null || item.IsDeleted) return false;
|
||||
|
||||
item.IsDeleted = true; // so ppl with it dont break
|
||||
item.ModifiedAt = DateTime.UtcNow;
|
||||
return Consumables.Update(item);
|
||||
}
|
||||
|
||||
public static void DeleteAllConsumablesForRoom(long roomId)
|
||||
{
|
||||
foreach (var item in Consumables.Find(c => c.RoomId == roomId && !c.IsDeleted).ToList())
|
||||
{
|
||||
item.IsDeleted = true;
|
||||
item.ModifiedAt = DateTime.UtcNow;
|
||||
Consumables.Update(item);
|
||||
}
|
||||
}
|
||||
|
||||
public static RoomConsumableOwnership? GetOwnership(long playerId, Guid roomConsumableId) =>
|
||||
Ownerships.FindOne(o => o.PlayerId == playerId && o.RoomConsumableId == roomConsumableId);
|
||||
|
||||
public static List<(RoomConsumableOwnership Ownership, RoomConsumable Item)> GetInventoryForRoom(
|
||||
long playerId, long roomId, int take = 200)
|
||||
{
|
||||
var items = Consumables.Find(c => c.RoomId == roomId && !c.IsDeleted)
|
||||
.OrderBy(c => c.CreatedAt)
|
||||
.ToList();
|
||||
if (items.Count == 0) return new List<(RoomConsumableOwnership, RoomConsumable)>();
|
||||
|
||||
var owned = Ownerships.Find(o => o.PlayerId == playerId)
|
||||
.GroupBy(o => o.RoomConsumableId)
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
|
||||
return items
|
||||
.Where(i => owned.ContainsKey(i.RoomConsumableId))
|
||||
.Take(take)
|
||||
.Select(i => (owned[i.RoomConsumableId], i))
|
||||
.ToList();
|
||||
}
|
||||
public static bool IsOwnedByOthers(RoomConsumable item) =>
|
||||
Ownerships.Exists(o => o.RoomConsumableId == item.RoomConsumableId
|
||||
&& o.Count > 0
|
||||
&& o.PlayerId != item.CreatorPlayerId);
|
||||
|
||||
public static RoomConsumableOwnership AddToInventory(long playerId, Guid roomConsumableId, Guid? newCode)
|
||||
{
|
||||
var own = GetOwnership(playerId, roomConsumableId);
|
||||
bool isNew = own == null;
|
||||
own ??= new RoomConsumableOwnership
|
||||
{
|
||||
PlayerId = playerId,
|
||||
RoomConsumableId = roomConsumableId
|
||||
};
|
||||
|
||||
own.Count += 1;
|
||||
if (newCode is Guid code && code != Guid.Empty) own.ConcurrencyCode = code;
|
||||
own.ModifiedAt = DateTime.UtcNow;
|
||||
|
||||
if (isNew) Ownerships.Insert(own);
|
||||
else Ownerships.Update(own);
|
||||
|
||||
return own;
|
||||
}
|
||||
|
||||
public static void ConsumeOne(RoomConsumableOwnership own, Guid? newCode)
|
||||
{
|
||||
own.Count -= 1;
|
||||
own.ConcurrencyCode = newCode is Guid code && code != Guid.Empty ? code : Guid.NewGuid();
|
||||
own.ModifiedAt = DateTime.UtcNow;
|
||||
Ownerships.Update(own);
|
||||
}
|
||||
|
||||
public static int GetCurrencyBalance(long playerId, Guid currencyId) =>
|
||||
CurrencyBalances.FindOne(b => b.PlayerId == playerId && b.CurrencyId == currencyId)?.Balance ?? 0;
|
||||
|
||||
public static int AddCurrencyBalance(long playerId, Guid currencyId, int amount)
|
||||
{
|
||||
var row = CurrencyBalances.FindOne(b => b.PlayerId == playerId && b.CurrencyId == currencyId);
|
||||
if (row == null)
|
||||
{
|
||||
row = new RoomCurrencyBalance
|
||||
{
|
||||
PlayerId = playerId,
|
||||
CurrencyId = currencyId,
|
||||
Balance = amount,
|
||||
ModifiedAt = DateTime.UtcNow
|
||||
};
|
||||
CurrencyBalances.Insert(row);
|
||||
return row.Balance;
|
||||
}
|
||||
|
||||
row.Balance += amount;
|
||||
row.ModifiedAt = DateTime.UtcNow;
|
||||
CurrencyBalances.Update(row);
|
||||
return row.Balance;
|
||||
}
|
||||
|
||||
/// <summary>Deduct if affordable; returns the new balance, or null when the player is short.</summary>
|
||||
public static int? DeductCurrencyBalance(long playerId, Guid currencyId, int amount)
|
||||
{
|
||||
var row = CurrencyBalances.FindOne(b => b.PlayerId == playerId && b.CurrencyId == currencyId);
|
||||
if (row == null || row.Balance < amount) return null;
|
||||
|
||||
row.Balance -= amount;
|
||||
row.ModifiedAt = DateTime.UtcNow;
|
||||
CurrencyBalances.Update(row);
|
||||
return row.Balance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1392 @@
|
||||
using LiteDB;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PhotonAccessTokenDBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.RoomDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class RoomDB
|
||||
{
|
||||
public static LiteDatabase RoomDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Rooms.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<Room> Rooms = RoomDBFile.GetCollection<Room>("Rooms");
|
||||
public static readonly ILiteCollection<currentSave> SubRoomSaves = RoomDBFile.GetCollection<currentSave>("SubRoomSaves");
|
||||
public static readonly ILiteCollection<DbCounters> Counters = RoomDBFile.GetCollection<DbCounters>("Counters");
|
||||
|
||||
public static void Setup()
|
||||
{
|
||||
Rooms.EnsureIndex(x => x.RoomId);
|
||||
Rooms.EnsureIndex(x => x.CreatorAccountId);
|
||||
Rooms.EnsureIndex(x => x.Name);
|
||||
Rooms.EnsureIndex("TagIndex", "$.Tags[*].Tag");
|
||||
Counters.EnsureIndex(x => x.Key);
|
||||
SubRoomSaves.EnsureIndex(x => x.SubRoomDataSaveId);
|
||||
SubRoomSaves.EnsureIndex(x => x.RoomId);
|
||||
SubRoomSaves.EnsureIndex(x => x.SubRoomId);
|
||||
}
|
||||
|
||||
public static async Task ImportRooms(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Console.WriteLine($"[DB] No import file found at {path}, skipping room import.");
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
string json = await File.ReadAllTextAsync(path);
|
||||
var importedRooms = System.Text.Json.JsonSerializer.Deserialize<List<Room>>(json);
|
||||
if (importedRooms != null)
|
||||
{
|
||||
foreach (var room in importedRooms)
|
||||
await AddRoom(room, log: true, shouldAssignNewIds: true);
|
||||
|
||||
Console.WriteLine($"[DB] Imported {importedRooms.Count} rooms from {path}.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[DB Error] Failed to import rooms from {path}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static Room? SetRoomSubroomRoomFileAgain(
|
||||
long roomId,
|
||||
long subRoomId,
|
||||
string roomFile,
|
||||
string roomDesc,
|
||||
long playerId,
|
||||
int platform = 0,
|
||||
int deviceClass = 0)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null)
|
||||
return null;
|
||||
|
||||
var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId);
|
||||
if (subRoom == null)
|
||||
return room;
|
||||
|
||||
subRoom.DataBlob = roomFile;
|
||||
subRoom.SavedByAccountId = playerId;
|
||||
|
||||
var existingUnityAssetId = !string.IsNullOrEmpty(subRoom.CurrentSave?.UnityAssetId)
|
||||
? subRoom.CurrentSave.UnityAssetId
|
||||
: null;
|
||||
|
||||
var save = new currentSave
|
||||
{
|
||||
SubRoomDataSaveId = GetNextSubRoomSaveId(),
|
||||
RoomId = roomId,
|
||||
SubRoomId = subRoomId,
|
||||
DataBlob = roomFile,
|
||||
Description = roomDesc ?? "",
|
||||
SavedByAccountId = playerId,
|
||||
SavedOnPlatform = platform,
|
||||
SavedOnDeviceClass = deviceClass,
|
||||
UnityAssetId = existingUnityAssetId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
SubRoomSaves.Insert(save);
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static void ExportRooms()
|
||||
{
|
||||
var allRooms = Rooms.FindAll().ToList();
|
||||
var saves = SubRoomSaves.FindAll().ToList();
|
||||
|
||||
foreach (var room in allRooms)
|
||||
{
|
||||
foreach (var sub in room.SubRooms ?? new List<SubRooms>())
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
string outputPath = Path.Combine(Program.dataDir, "exported_rooms.json");
|
||||
File.WriteAllText(outputPath, System.Text.Json.JsonSerializer.Serialize(allRooms));
|
||||
Console.WriteLine($"[DB] Exported {allRooms.Count} rooms → {outputPath}");
|
||||
}
|
||||
|
||||
public static List<Room> GetRoomsByIds(List<long> ids)
|
||||
{
|
||||
if (ids == null || ids.Count == 0) return new List<Room>();
|
||||
return Rooms.Find(room => ids.Contains(room.RoomId)).ToList();
|
||||
}
|
||||
|
||||
public static async Task ClearRooms(bool log = false)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var dormRooms = Rooms.Find(r => r.IsDorm).ToList();
|
||||
Rooms.DeleteAll();
|
||||
foreach (var dorm in dormRooms)
|
||||
Rooms.Insert(dorm);
|
||||
if (log)
|
||||
Console.WriteLine($"[DB] Cleared rooms, preserved {dormRooms.Count} dorm rooms.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[DB Error] Failed to clear rooms: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task AddRoom(Room newRoom, bool log = false, bool shouldAssignNewIds = true)
|
||||
{
|
||||
if (newRoom == null) return;
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (shouldAssignNewIds || newRoom.RoomId <= 0)
|
||||
newRoom.RoomId = GetNextRoomId();
|
||||
|
||||
newRoom.RankedEntityId = newRoom.RoomId.ToString();
|
||||
|
||||
if (newRoom.Stats == null)
|
||||
newRoom.Stats = new Stats();
|
||||
|
||||
if (newRoom.LoadScreens == null)
|
||||
newRoom.LoadScreens = new List<LoadScreens>();
|
||||
|
||||
if (newRoom.SubRooms != null && newRoom.SubRooms.Any())
|
||||
{
|
||||
foreach (var sub in newRoom.SubRooms)
|
||||
{
|
||||
if (shouldAssignNewIds || sub.SubRoomId <= 0)
|
||||
sub.SubRoomId = GetNextSubRoomId();
|
||||
sub.RoomId = newRoom.RoomId;
|
||||
}
|
||||
}
|
||||
|
||||
Rooms.Insert(newRoom);
|
||||
|
||||
if (log)
|
||||
Console.WriteLine($"[DB] Added room: {newRoom.Name} (ID: {newRoom.RoomId})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[DB Error] Failed to add room {newRoom.Name}: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static Room? MoveSubRoomToRoom(
|
||||
long fromRoomId,
|
||||
long subRoomId,
|
||||
long targetRoomId,
|
||||
long playerId)
|
||||
{
|
||||
var sourceRoom = Rooms.FindById(fromRoomId);
|
||||
var targetRoom = Rooms.FindById(targetRoomId);
|
||||
|
||||
if (sourceRoom == null || targetRoom == null)
|
||||
return null;
|
||||
|
||||
var subRoom = sourceRoom.SubRooms?
|
||||
.FirstOrDefault(s => s.SubRoomId == subRoomId);
|
||||
|
||||
if (subRoom == null)
|
||||
return null;
|
||||
|
||||
bool canEditSource = UserCanEditRoom(fromRoomId, playerId);
|
||||
bool canEditTarget = UserCanEditRoom(targetRoomId, playerId);
|
||||
|
||||
if (!canEditSource || !canEditTarget)
|
||||
return null;
|
||||
|
||||
targetRoom.SubRooms ??= new List<SubRooms>();
|
||||
|
||||
long oldSubRoomId = subRoom.SubRoomId;
|
||||
long newSubRoomId = GetNextSubRoomId();
|
||||
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(subRoom);
|
||||
var movedSubRoom = System.Text.Json.JsonSerializer.Deserialize<SubRooms>(json);
|
||||
|
||||
if (movedSubRoom == null)
|
||||
return null;
|
||||
|
||||
movedSubRoom.SubRoomId = newSubRoomId;
|
||||
movedSubRoom.RoomId = targetRoomId;
|
||||
|
||||
targetRoom.SubRooms.Add(movedSubRoom);
|
||||
|
||||
var saves = SubRoomSaves.Find(s =>
|
||||
s.RoomId == fromRoomId &&
|
||||
s.SubRoomId == oldSubRoomId).ToList();
|
||||
|
||||
foreach (var save in saves)
|
||||
{
|
||||
save.RoomId = targetRoomId;
|
||||
save.SubRoomId = newSubRoomId;
|
||||
|
||||
SubRoomSaves.Update(save);
|
||||
}
|
||||
|
||||
sourceRoom.SubRooms.Remove(subRoom);
|
||||
|
||||
Rooms.Update(sourceRoom);
|
||||
Rooms.Update(targetRoom);
|
||||
|
||||
return targetRoom;
|
||||
}
|
||||
|
||||
public static long AddRoomSync(Room newRoom, bool shouldAssignNewIds = true)
|
||||
{
|
||||
if (newRoom == null) return 0;
|
||||
|
||||
if (shouldAssignNewIds || newRoom.RoomId <= 0)
|
||||
newRoom.RoomId = GetNextRoomId();
|
||||
|
||||
newRoom.RankedEntityId = newRoom.RoomId.ToString();
|
||||
|
||||
if (newRoom.Stats == null) newRoom.Stats = new Stats();
|
||||
if (newRoom.LoadScreens == null) newRoom.LoadScreens = new List<LoadScreens>();
|
||||
|
||||
if (newRoom.SubRooms != null && newRoom.SubRooms.Any())
|
||||
{
|
||||
foreach (var sub in newRoom.SubRooms)
|
||||
{
|
||||
if (shouldAssignNewIds || sub.SubRoomId <= 0)
|
||||
sub.SubRoomId = GetNextSubRoomId();
|
||||
sub.RoomId = newRoom.RoomId;
|
||||
}
|
||||
}
|
||||
|
||||
Rooms.Insert(newRoom);
|
||||
Console.WriteLine($"[DB] Added room: {newRoom.Name} (ID: {newRoom.RoomId})");
|
||||
return newRoom.RoomId;
|
||||
}
|
||||
|
||||
public static long GetNextRoomId()
|
||||
{
|
||||
long id;
|
||||
do
|
||||
{
|
||||
id = Random.Shared.NextInt64(100_000_000_000L, long.MaxValue);
|
||||
} while (Rooms.FindById(id) != null);
|
||||
return id;
|
||||
}
|
||||
|
||||
public static long GetNextSubRoomId()
|
||||
{
|
||||
long id;
|
||||
do
|
||||
{
|
||||
id = Random.Shared.NextInt64(100_000_000_000L, long.MaxValue);
|
||||
} while (Rooms.FindAll().Any(r => r.SubRooms != null && r.SubRooms.Any(s => s.SubRoomId == id)));
|
||||
return id;
|
||||
}
|
||||
|
||||
public static long GetNextSubRoomSaveId()
|
||||
{
|
||||
const long saveIdCeiling = int.MaxValue / 16;
|
||||
long id;
|
||||
do
|
||||
{
|
||||
id = Random.Shared.NextInt64(1, saveIdCeiling);
|
||||
} while (SubRoomSaves.FindOne(s => s.SubRoomDataSaveId == id) != null);
|
||||
return id;
|
||||
}
|
||||
|
||||
public static Room? GetRoom(long roomId)
|
||||
{
|
||||
var col = RoomDBFile.GetCollection("Rooms");
|
||||
var doc = col.FindById(new BsonValue(roomId));
|
||||
if (doc == null) return null;
|
||||
try
|
||||
{
|
||||
return BsonMapper.Global.Deserialize<Room>(doc);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[DB] Skipped corrupt room doc {roomId}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Room GetRoomByName(string name)
|
||||
=> Rooms.FindOne(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public static List<Room> GetRoomsByNames(List<string> names)
|
||||
{
|
||||
if (names == null || names.Count == 0) return new List<Room>();
|
||||
return Rooms.Find(room => names.Contains(room.Name, StringComparer.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
public static List<Room> GetRoomsByCreator(long playerId)
|
||||
{
|
||||
var col = RoomDBFile.GetCollection("Rooms");
|
||||
var results = new List<Room>();
|
||||
foreach (var doc in col.FindAll())
|
||||
{
|
||||
try
|
||||
{
|
||||
var room = BsonMapper.Global.Deserialize<Room>(doc);
|
||||
if (room == null)
|
||||
continue;
|
||||
if (room.Accessibility != RoomAccessibility.Public)
|
||||
continue;
|
||||
|
||||
bool isCreator = room.CreatorAccountId == playerId;
|
||||
bool hasOwnerRole = room.Roles?.Any(r =>
|
||||
r.AccountId == playerId &&
|
||||
(r.Role == Role.Creator ||
|
||||
r.Role == Role.Host ||
|
||||
r.Role == Role.CoOwner ||
|
||||
r.Role == Role.TemporaryCoOwner)
|
||||
) ?? false;
|
||||
|
||||
if (!isCreator && !hasOwnerRole)
|
||||
continue;
|
||||
|
||||
results.Add(room);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[oh shittinhs] Skipped corrupt room doc {doc["_id"]}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public static long GetAllRoomsCount() => Rooms.Count();
|
||||
|
||||
public static bool DoesRoomExist(long roomId) => Rooms.FindById(new BsonValue(roomId)) != null;
|
||||
|
||||
public static (List<Room> Results, int Total) GetHotRooms(string? tag, int skip, int take, bool risingSort = false)
|
||||
{
|
||||
var col = RoomDBFile.GetCollection("Rooms");
|
||||
string t = tag?.ToLowerInvariant() ?? "";
|
||||
bool hasTag = !string.IsNullOrWhiteSpace(t);
|
||||
bool isRRO = (t == "rro" || t == "recroomoriginal");
|
||||
var results = new List<Room>();
|
||||
|
||||
foreach (var doc in col.FindAll())
|
||||
{
|
||||
try
|
||||
{
|
||||
var room = BsonMapper.Global.Deserialize<Room>(doc);
|
||||
if (room == null) continue;
|
||||
if (room.IsDorm) continue;
|
||||
if (room.Accessibility != RoomAccessibility.Public) continue;
|
||||
|
||||
if (!hasTag)
|
||||
{
|
||||
bool isRoomRRO = room.Tags != null &&
|
||||
room.Tags.Any(x =>
|
||||
x.Tag.Equals("rro", StringComparison.OrdinalIgnoreCase) ||
|
||||
x.Tag.Equals("recroomoriginal", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (isRoomRRO) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isRRO)
|
||||
{
|
||||
bool hasRRO = room.Tags != null &&
|
||||
room.Tags.Any(x =>
|
||||
x.Tag.Equals("rro", StringComparison.OrdinalIgnoreCase) ||
|
||||
x.Tag.Equals("recroomoriginal", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!hasRRO) continue;
|
||||
}
|
||||
}
|
||||
|
||||
results.Add(room);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<Room> ordered;
|
||||
if (risingSort)
|
||||
{
|
||||
ordered = results
|
||||
.Where(r => r.Stats?.CheerCount >= 5)
|
||||
.OrderByDescending(r => r.CreatedAt);
|
||||
}
|
||||
else if (t == "new")
|
||||
{
|
||||
ordered = results.OrderByDescending(r => r.CreatedAt);
|
||||
}
|
||||
else if (!hasTag)
|
||||
{
|
||||
ordered = results.OrderByDescending(r => r.Stats?.CheerCount ?? 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
ordered = results.OrderByDescending(r => r.Stats?.VisitCount ?? 0);
|
||||
}
|
||||
|
||||
var finalResults = ordered.Skip(skip).Take(take).ToList();
|
||||
return (finalResults, results.Count);
|
||||
}
|
||||
|
||||
public static List<Room> GetBaseRooms()
|
||||
{
|
||||
var col = RoomDBFile.GetCollection("Rooms");
|
||||
var results = new List<Room>();
|
||||
|
||||
foreach (var doc in col.FindAll())
|
||||
{
|
||||
try
|
||||
{
|
||||
var room = BsonMapper.Global.Deserialize<Room>(doc);
|
||||
|
||||
if (room == null)
|
||||
continue;
|
||||
|
||||
if (!room.IsDorm && room.Tags != null && room.Tags.Any(t => t.Tag == "base"))
|
||||
results.Add(room);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[DB] Skipped corrupt room doc {doc["_id"]}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static (List<Room> Results, int Total) Search(string query, bool isBase = false, int skip = 0, int take = 30)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
return (new List<Room>(), 0);
|
||||
|
||||
var segments = query.Split('|', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.ToList();
|
||||
|
||||
var candidatePool = Rooms.FindAll()
|
||||
.Where(r => !r.IsDorm)
|
||||
.Where(r => !r.ExcludeFromSearch)
|
||||
.Where(r => isBase || r.Accessibility == RoomAccessibility.Public)
|
||||
.ToList();
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().ToList();
|
||||
|
||||
var matched = new Dictionary<long, Room>();
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
var terms = segment.Split(new char[] { ' ', '+' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var nameParts = new List<string>();
|
||||
var tagParts = new List<string>();
|
||||
var authorParts = new List<string>();
|
||||
|
||||
foreach (var term in terms)
|
||||
{
|
||||
if (term.StartsWith("#"))
|
||||
tagParts.Add(term.Substring(1).ToLowerInvariant().Trim());
|
||||
else if (term.StartsWith("@"))
|
||||
authorParts.Add(term.Substring(1).ToLowerInvariant().Trim());
|
||||
else
|
||||
nameParts.Add(term.ToLowerInvariant().Trim());
|
||||
}
|
||||
|
||||
long? resolvedAuthorId = null;
|
||||
|
||||
if (authorParts.Count > 0)
|
||||
{
|
||||
var authorQuery = string.Join(" ", authorParts);
|
||||
|
||||
var exactMatch = allPlayers.FirstOrDefault(p =>
|
||||
string.Equals(p.Player?.Username, authorQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(p.Player?.DisplayName, authorQuery, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (exactMatch != null)
|
||||
{
|
||||
resolvedAuthorId = exactMatch.PlayerId;
|
||||
}
|
||||
else
|
||||
{
|
||||
var fuzzyMatch = allPlayers
|
||||
.Where(p => p.Player != null)
|
||||
.Select(p => new
|
||||
{
|
||||
Player = p,
|
||||
Score = Math.Max(
|
||||
ScoreShit((p.Player!.Username ?? "").ToLowerInvariant(), authorQuery),
|
||||
ScoreShit((p.Player!.DisplayName ?? "").ToLowerInvariant(), authorQuery))
|
||||
})
|
||||
.Where(x => x.Score > 0)
|
||||
.OrderByDescending(x => x.Score)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (fuzzyMatch != null)
|
||||
resolvedAuthorId = fuzzyMatch.Player.PlayerId;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var room in candidatePool)
|
||||
{
|
||||
if (resolvedAuthorId.HasValue && room.CreatorAccountId != resolvedAuthorId.Value)
|
||||
continue;
|
||||
|
||||
if (tagParts.Count > 0)
|
||||
{
|
||||
var roomTags = (room.Tags ?? new List<Tags>())
|
||||
.Select(t => t.Tag.ToLowerInvariant().Trim())
|
||||
.ToList();
|
||||
|
||||
bool allTagsMatch = tagParts.All(tp =>
|
||||
{
|
||||
if (roomTags.Contains(tp))
|
||||
return true;
|
||||
|
||||
foreach (var rt in roomTags)
|
||||
{
|
||||
if (rt.Contains(tp) || tp.Contains(rt))
|
||||
return true;
|
||||
|
||||
int dist = EditDistance(rt, tp);
|
||||
int maxLen = Math.Max(rt.Length, tp.Length);
|
||||
if (maxLen > 0 && (double)dist / maxLen <= 0.34)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!allTagsMatch) continue;
|
||||
}
|
||||
|
||||
if (nameParts.Count > 0)
|
||||
{
|
||||
string roomNameLower = (room.Name ?? "").ToLowerInvariant();
|
||||
|
||||
bool nameMatch = nameParts.All(np =>
|
||||
{
|
||||
if (roomNameLower.Contains(np))
|
||||
return true;
|
||||
|
||||
int dist = EditDistance(roomNameLower, np);
|
||||
int maxLen = Math.Max(roomNameLower.Length, np.Length);
|
||||
if (maxLen > 0 && (double)dist / maxLen <= 0.34)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!nameMatch) continue;
|
||||
}
|
||||
|
||||
matched[room.RoomId] = room;
|
||||
}
|
||||
}
|
||||
|
||||
var all = matched.Values
|
||||
.OrderByDescending(r => r.Stats?.CheerCount ?? 0)
|
||||
.ThenByDescending(r => r.Stats?.VisitCount ?? 0)
|
||||
.ToList();
|
||||
|
||||
return (all.Skip(skip).Take(take).ToList(), all.Count);
|
||||
}
|
||||
|
||||
private static int ScoreShit(string source, string target)
|
||||
{
|
||||
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(target)) return 0;
|
||||
if (source == target) return 1000;
|
||||
if (source.Contains(target) || target.Contains(source)) return 500;
|
||||
|
||||
int dist = EditDistance(source, target);
|
||||
int maxLen = Math.Max(source.Length, target.Length);
|
||||
if (maxLen == 0) return 0;
|
||||
|
||||
double similarity = 1.0 - (double)dist / maxLen;
|
||||
if (similarity >= 0.66) return (int)(similarity * 100);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int EditDistance(string a, string b)
|
||||
{
|
||||
if (string.IsNullOrEmpty(a)) return b?.Length ?? 0;
|
||||
if (string.IsNullOrEmpty(b)) return a.Length;
|
||||
|
||||
int m = a.Length, n = b.Length;
|
||||
var dp = new int[m + 1, n + 1];
|
||||
|
||||
for (int i = 0; i <= m; i++) dp[i, 0] = i;
|
||||
for (int j = 0; j <= n; j++) dp[0, j] = j;
|
||||
|
||||
for (int i = 1; i <= m; i++)
|
||||
{
|
||||
for (int j = 1; j <= n; j++)
|
||||
{
|
||||
dp[i, j] = a[i - 1] == b[j - 1]
|
||||
? dp[i - 1, j - 1]
|
||||
: 1 + Math.Min(dp[i - 1, j - 1], Math.Min(dp[i - 1, j], dp[i, j - 1]));
|
||||
}
|
||||
}
|
||||
|
||||
return dp[m, n];
|
||||
}
|
||||
|
||||
public static Room? SetRoomName(long roomId, string roomName)
|
||||
{
|
||||
var existing = Rooms.FindOne(x => x.Name == roomName && x.RoomId != roomId);
|
||||
if (existing != null) return null;
|
||||
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
room.Name = roomName;
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static Room? SetRoomDescription(long roomId, string description)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
room.Description = description;
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static Room? SetRoomImageName(long roomId, string imageName)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
room.ImageName = imageName;
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static Room? SetRoomAccessibility(long roomId, RoomAccessibility accessibility)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
room.Accessibility = accessibility;
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static Room? SetRoomCloning(long roomId, bool cloningEnabled)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
room.CloningAllowed = cloningEnabled;
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static Room? SetRoomRestrictions(long roomId, bool supportsScreens, bool supportsWalkVR, bool supportsTeleportVR, bool supportsJuniors)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
room.SupportsScreens = supportsScreens;
|
||||
room.SupportsWalkVR = supportsWalkVR;
|
||||
room.SupportsTeleportVR = supportsTeleportVR;
|
||||
room.SupportsJuniors = supportsJuniors;
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static Room? SetRoomLoadscreen(long roomId, string? imageName, string? title, string? subtitle)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
room.LoadScreens = new List<LoadScreens>
|
||||
{
|
||||
new LoadScreens
|
||||
{
|
||||
ImageName = imageName ?? "",
|
||||
Title = title ?? "",
|
||||
Subtitle = subtitle ?? ""
|
||||
}
|
||||
};
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static Room? SetRoomWarningMask(long roomId, WarningMaskType warningMask)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
room.WarningMask = warningMask;
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static Room? SetRoomCustomWarning(long roomId, string? customWarning)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
room.CustomWarning = customWarning;
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static Room? SetRoomTags(List<string> tags, long roomId, int type = 0)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
|
||||
var newTags = tags
|
||||
.Select(t => t.Trim().ToLowerInvariant())
|
||||
.Where(t => !string.IsNullOrWhiteSpace(t))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (room.Tags == null) room.Tags = new List<Tags>();
|
||||
|
||||
|
||||
foreach (var tag in newTags)
|
||||
{
|
||||
var existing = room.Tags.FirstOrDefault(t => t.Tag.Equals(tag, StringComparison.OrdinalIgnoreCase));
|
||||
if (existing != null)
|
||||
existing.Type = (TagType)type;
|
||||
else
|
||||
room.Tags.Add(new Tags { Tag = tag, Type = (TagType)type });
|
||||
}
|
||||
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static Room? SetRoomTagsMixed(List<RoomDBClasses.Tags> incomingTags, long roomId)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
|
||||
room.Tags ??= new List<Tags>();
|
||||
|
||||
bool autoTagOnly = incomingTags.Count == 1 && incomingTags[0].Type == TagType.Auto;
|
||||
|
||||
foreach (var incoming in incomingTags)
|
||||
{
|
||||
var tag = incoming.Tag.Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(tag)) continue;
|
||||
|
||||
var existing = room.Tags.FirstOrDefault(t => t.Tag.Equals(tag, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (incoming.Type == TagType.Auto && !autoTagOnly)
|
||||
{
|
||||
if (existing == null)
|
||||
room.Tags.Add(new Tags { Tag = tag, Type = incoming.Type });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existing != null)
|
||||
room.Tags.Remove(existing);
|
||||
else
|
||||
room.Tags.Add(new Tags { Tag = tag, Type = incoming.Type });
|
||||
}
|
||||
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static Room? SetRoomSubroomRoomFile(
|
||||
long roomId, long subRoomId,
|
||||
string dataBlob, string description,
|
||||
long playerId, int platform, int deviceClass)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
|
||||
var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId);
|
||||
if (subRoom == null) return room;
|
||||
|
||||
subRoom.DataBlob = dataBlob;
|
||||
subRoom.SavedByAccountId = playerId;
|
||||
|
||||
var save = new currentSave
|
||||
{
|
||||
SubRoomDataSaveId = GetNextSubRoomSaveId(),
|
||||
RoomId = roomId,
|
||||
SubRoomId = subRoomId,
|
||||
DataBlob = dataBlob,
|
||||
Description = description ?? "",
|
||||
SavedByAccountId = playerId,
|
||||
SavedOnPlatform = platform,
|
||||
SavedOnDeviceClass = deviceClass,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
SubRoomSaves.Insert(save);
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static (List<currentSave> Results, int TotalResults) GetSubRoomSaves(
|
||||
long roomId, long subRoomId, int skip, int take, string? search = null)
|
||||
{
|
||||
take = Math.Min(take, 9999);
|
||||
|
||||
var query = SubRoomSaves.FindAll()
|
||||
.Where(s => s.RoomId == roomId && s.SubRoomId == subRoomId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var q = search.ToLowerInvariant();
|
||||
query = query.Where(s =>
|
||||
(s.DataBlob ?? "").ToLower().Contains(q) ||
|
||||
(s.Description ?? "").ToLower().Contains(q) ||
|
||||
(s.Tags != null && s.Tags.Any(t => t.ToLower().Contains(q)))
|
||||
);
|
||||
}
|
||||
|
||||
var ordered = query.OrderByDescending(s => s.CreatedAt).ToList();
|
||||
|
||||
return (ordered.Skip(skip).Take(take).ToList(), ordered.Count);
|
||||
}
|
||||
|
||||
public static currentSave? GetSubRoomSaveById(long roomId, long subRoomId, long saveId)
|
||||
=> SubRoomSaves.FindOne(s => s.RoomId == roomId && s.SubRoomId == subRoomId && s.SubRoomDataSaveId == saveId);
|
||||
|
||||
public static Room? CreateSubroomForRoom(long roomId, string subroomName)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
|
||||
if (room.SubRooms == null) room.SubRooms = new List<SubRooms>();
|
||||
|
||||
var lastSub = room.SubRooms.OrderByDescending(s => s.SubRoomId).FirstOrDefault();
|
||||
|
||||
var newSub = new SubRooms
|
||||
{
|
||||
RoomId = roomId,
|
||||
Name = subroomName,
|
||||
SubRoomId = GetNextSubRoomId(),
|
||||
Accessibility = RoomAccessibility.Public,
|
||||
UnitySceneId = "a75f7547-79eb-47c6-8986-6767abcb4f92",
|
||||
MaxPlayers = 20
|
||||
};
|
||||
|
||||
room.SubRooms.Add(newSub);
|
||||
Rooms.Update(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
public static bool DeleteSubroom(long roomId, long subRoomId)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return false;
|
||||
|
||||
var sub = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId);
|
||||
if (sub == null) return false;
|
||||
|
||||
room.SubRooms.Remove(sub);
|
||||
|
||||
if (room.SubRooms.Count < 1)
|
||||
{
|
||||
SubRoomSaves.DeleteMany(s => s.RoomId == roomId);
|
||||
Rooms.Delete(roomId);
|
||||
return true;
|
||||
}
|
||||
|
||||
Rooms.Update(room);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ModifySubroom(long roomId, long subRoomId, string name, RoomAccessibility accessibility, int maxPlayers)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return false;
|
||||
|
||||
var sub = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId);
|
||||
if (sub == null) return false;
|
||||
|
||||
sub.Name = name;
|
||||
sub.Accessibility = accessibility;
|
||||
sub.MaxPlayers = maxPlayers;
|
||||
Rooms.Update(room);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void IncrementRoomVisitCount(long roomId)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return;
|
||||
if (room.Stats == null) room.Stats = new Stats();
|
||||
room.Stats.VisitCount++;
|
||||
Rooms.Update(room);
|
||||
}
|
||||
|
||||
public static void IncrementRoomCheerCount(long roomId)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return;
|
||||
room.Stats.CheerCount++;
|
||||
Rooms.Update(room);
|
||||
}
|
||||
|
||||
public static void DecrementRoomCheerCount(long roomId)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null || room.Stats.CheerCount <= 0) return;
|
||||
room.Stats.CheerCount--;
|
||||
Rooms.Update(room);
|
||||
}
|
||||
|
||||
public static void IncrementRoomFavoriteCount(long roomId)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return;
|
||||
room.Stats.FavoriteCount++;
|
||||
Rooms.Update(room);
|
||||
}
|
||||
|
||||
public static void DecrementRoomFavoriteCount(long roomId)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null || room.Stats.FavoriteCount <= 0) return;
|
||||
room.Stats.FavoriteCount--;
|
||||
Rooms.Update(room);
|
||||
}
|
||||
|
||||
public static bool UserCanEditRoom(long roomId, long accountId, bool allowCoOwner = true)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return false;
|
||||
if (room.CreatorAccountId == accountId) return true;
|
||||
|
||||
if (allowCoOwner) {
|
||||
return room.Roles?.Any(r =>
|
||||
r.AccountId == accountId &&
|
||||
(r.Role == Role.CoOwner || r.Role == Role.Creator || r.Role == Role.TemporaryCoOwner)
|
||||
) ?? false;
|
||||
} else {
|
||||
return room.Roles?.Any(r =>
|
||||
r.AccountId == accountId &&
|
||||
r.Role == Role.Creator
|
||||
) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool AddRoleToRoom(long roomId, long accountId, Role role, Role invitedRole = Role.None)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return false;
|
||||
|
||||
room.Roles ??= new List<Roles>();
|
||||
room.Roles.Add(new Roles { AccountId = accountId, Role = role, InvitedRole = invitedRole });
|
||||
Rooms.Update(room);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RemoveRoleFromRoom(long roomId, long accountId)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null || room.Roles == null) return false;
|
||||
room.Roles.RemoveAll(r => r.AccountId == accountId);
|
||||
Rooms.Update(room);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static long? CloneRoom(long roomId, string roomName, long playerId)
|
||||
{
|
||||
var original = GetRoom(roomId);
|
||||
if (original == null)
|
||||
return null;
|
||||
|
||||
if (original.Name == "DormRoom")
|
||||
return null;
|
||||
|
||||
bool isOwner = original.CreatorAccountId == playerId;
|
||||
|
||||
bool isCoOwner = original.Roles?.Any(r =>
|
||||
r.AccountId == playerId &&
|
||||
(r.Role == Role.CoOwner || r.Role == Role.Creator)
|
||||
) ?? false;
|
||||
|
||||
bool canClone = original.CloningAllowed;
|
||||
|
||||
bool isBaseRoom = original.Tags != null &&
|
||||
original.Tags.Any(t => string.Equals(t.Tag, "base", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
bool isRRO = original.Tags != null &&
|
||||
original.Tags.Any(t =>
|
||||
string.Equals(t.Tag, "rro", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(t.Tag, "recroomoriginal", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (isRRO && !isBaseRoom && !canClone)
|
||||
return null;
|
||||
|
||||
if (!isBaseRoom && !isOwner && !isCoOwner && !canClone)
|
||||
return null;
|
||||
|
||||
var originalSubroomOrder = (original.SubRooms ?? new List<SubRooms>())
|
||||
.Select(s => s.SubRoomId)
|
||||
.ToList();
|
||||
|
||||
var latestSavePerSubroom = new Dictionary<long, currentSave>();
|
||||
foreach (var origSubId in originalSubroomOrder)
|
||||
{
|
||||
var latestSave = SubRoomSaves
|
||||
.Find(s => s.RoomId == roomId && s.SubRoomId == origSubId)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
if (latestSave != null)
|
||||
latestSavePerSubroom[origSubId] = latestSave;
|
||||
}
|
||||
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(original);
|
||||
var clone = System.Text.Json.JsonSerializer.Deserialize<Room>(json);
|
||||
|
||||
if (clone == null)
|
||||
return null;
|
||||
|
||||
clone.RoomId = 0;
|
||||
clone.Name = roomName;
|
||||
clone.CreatedAt = DateTime.UtcNow;
|
||||
clone.ImageName = "DefaultRoomImage.jpg";
|
||||
clone.Accessibility = RoomAccessibility.Private;
|
||||
clone.CloningAllowed = false;
|
||||
clone.IsRRO = false;
|
||||
clone.IsDeveloperOwned = false;
|
||||
clone.IsDorm = false;
|
||||
clone.PromoImages = new List<string>();
|
||||
clone.Stats = new Stats();
|
||||
clone.CreatorAccountId = playerId;
|
||||
|
||||
clone.Tags = (clone.Tags ?? new List<Tags>())
|
||||
.Where(t =>
|
||||
!string.Equals(t.Tag, "base", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(t.Tag, "rro", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
clone.Roles = new List<Roles>
|
||||
{
|
||||
new Roles
|
||||
{
|
||||
AccountId = playerId,
|
||||
Role = Role.Creator,
|
||||
InvitedRole = Role.Creator
|
||||
}
|
||||
};
|
||||
|
||||
if (clone.SubRooms != null)
|
||||
{
|
||||
foreach (var sub in clone.SubRooms)
|
||||
{
|
||||
sub.SubRoomId = GetNextSubRoomId();
|
||||
sub.RoomId = clone.RoomId;
|
||||
sub.SavedByAccountId = playerId;
|
||||
if (sub.CreatorAccountId != null)
|
||||
sub.CreatorAccountId = playerId;
|
||||
}
|
||||
}
|
||||
|
||||
long newRoomId = AddRoomSync(clone, shouldAssignNewIds: false);
|
||||
|
||||
var insertedRoom = GetRoom(newRoomId);
|
||||
|
||||
if (insertedRoom?.SubRooms != null)
|
||||
{
|
||||
foreach (var sub in insertedRoom.SubRooms)
|
||||
sub.RoomId = newRoomId;
|
||||
|
||||
Rooms.Update(insertedRoom);
|
||||
}
|
||||
|
||||
var clonedSubrooms = insertedRoom?.SubRooms ?? new List<SubRooms>();
|
||||
|
||||
for (int i = 0; i < originalSubroomOrder.Count && i < clonedSubrooms.Count; i++)
|
||||
{
|
||||
long originalSubroomId = originalSubroomOrder[i];
|
||||
long clonedSubroomId = clonedSubrooms[i].SubRoomId;
|
||||
|
||||
if (latestSavePerSubroom.TryGetValue(originalSubroomId, out var latestOriginalSave))
|
||||
{
|
||||
var clonedSave = new currentSave
|
||||
{
|
||||
SubRoomDataSaveId = GetNextSubRoomSaveId(),
|
||||
RoomId = newRoomId,
|
||||
SubRoomId = clonedSubroomId,
|
||||
DataBlob = latestOriginalSave.DataBlob,
|
||||
DataBlobHash = latestOriginalSave.DataBlobHash,
|
||||
ReferencedUnityAssetIds = latestOriginalSave.ReferencedUnityAssetIds != null
|
||||
? new List<string>(latestOriginalSave.ReferencedUnityAssetIds)
|
||||
: new List<string>(),
|
||||
UnitySubAssets = latestOriginalSave.UnitySubAssets != null
|
||||
? new List<string>(latestOriginalSave.UnitySubAssets)
|
||||
: new List<string>(),
|
||||
ReferencedUnityAssets = latestOriginalSave.ReferencedUnityAssets != null
|
||||
? new List<string>(latestOriginalSave.ReferencedUnityAssets)
|
||||
: new List<string>(),
|
||||
UnityAssetId = latestOriginalSave.UnityAssetId,
|
||||
PersistenceVersion = latestOriginalSave.PersistenceVersion,
|
||||
OMVersion = latestOriginalSave.OMVersion,
|
||||
UgcSubVersion = latestOriginalSave.UgcSubVersion,
|
||||
SavedByAccountId = playerId,
|
||||
SavedOnPlatform = latestOriginalSave.SavedOnPlatform,
|
||||
SavedOnDeviceClass = latestOriginalSave.SavedOnDeviceClass,
|
||||
Description = $"Cloned from room ^{original.Name}",
|
||||
Tags = latestOriginalSave.Tags != null
|
||||
? new List<string>(latestOriginalSave.Tags)
|
||||
: new List<string>(),
|
||||
ModerationState = latestOriginalSave.ModerationState,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
SubRoomSaves.Insert(clonedSave);
|
||||
|
||||
var room = Rooms.FindById(newRoomId);
|
||||
var sub = room?.SubRooms?.FirstOrDefault(s => s.SubRoomId == clonedSubroomId);
|
||||
|
||||
if (sub != null)
|
||||
{
|
||||
sub.DataBlob = clonedSave.DataBlob;
|
||||
sub.SavedByAccountId = playerId;
|
||||
sub.CurrentSave = clonedSave;
|
||||
Rooms.Update(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < originalSubroomOrder.Count && i < clonedSubrooms.Count; i++)
|
||||
{
|
||||
long originalSubroomId = originalSubroomOrder[i];
|
||||
long clonedSubroomId = clonedSubrooms[i].SubRoomId;
|
||||
|
||||
var originalPermissions = PhotonAccessTokenDB.GetPermissions(roomId, originalSubroomId);
|
||||
if (originalPermissions != null && originalPermissions.Permissions.Count > 0)
|
||||
{
|
||||
var copiedEntries = originalPermissions.Permissions.Select(p => new PhotonAccessTokenDBClasses.StoredPermissionEntry
|
||||
{
|
||||
Permission = p.Permission,
|
||||
Role = p.Role,
|
||||
Type = p.Type,
|
||||
Override = p.Override,
|
||||
Value = p.Value
|
||||
}).ToList();
|
||||
|
||||
PhotonAccessTokenDB.SetPermissions(newRoomId, clonedSubroomId, copiedEntries);
|
||||
}
|
||||
}
|
||||
|
||||
return newRoomId;
|
||||
}
|
||||
|
||||
public static SubRooms? CloneSubroom(long roomId, long subRoomId, long playerId)
|
||||
{
|
||||
var room = Rooms.FindById(roomId);
|
||||
if (room == null) return null;
|
||||
|
||||
var original = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId);
|
||||
if (original == null) return null;
|
||||
|
||||
string GenerateUniqueName()
|
||||
{
|
||||
string candidate;
|
||||
do
|
||||
{
|
||||
int suffix = Random.Shared.Next(10000, 999999);
|
||||
candidate = $"{original.Name}_{suffix}";
|
||||
} while (room.SubRooms!.Any(s => s.Name.Equals(candidate, StringComparison.OrdinalIgnoreCase)));
|
||||
return candidate;
|
||||
}
|
||||
|
||||
var newSub = new SubRooms
|
||||
{
|
||||
SubRoomId = GetNextSubRoomId(),
|
||||
RoomId = roomId,
|
||||
Name = GenerateUniqueName(),
|
||||
Accessibility = original.Accessibility,
|
||||
IsSandbox = original.IsSandbox,
|
||||
MaxPlayers = original.MaxPlayers,
|
||||
UnitySceneId = original.UnitySceneId,
|
||||
DataBlob = original.DataBlob,
|
||||
SavedByAccountId = playerId,
|
||||
ShouldAutoStageSaves = original.ShouldAutoStageSaves,
|
||||
StagedSubRoomDataSaveId = original.StagedSubRoomDataSaveId,
|
||||
LastModeratedSaveModerationState = original.LastModeratedSaveModerationState,
|
||||
CreatorAccountId = playerId
|
||||
};
|
||||
|
||||
var latestSave = SubRoomSaves
|
||||
.Find(s => s.RoomId == roomId && s.SubRoomId == subRoomId)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (latestSave != null)
|
||||
{
|
||||
var clonedSave = new currentSave
|
||||
{
|
||||
SubRoomDataSaveId = GetNextSubRoomSaveId(),
|
||||
RoomId = roomId,
|
||||
SubRoomId = newSub.SubRoomId,
|
||||
DataBlob = latestSave.DataBlob,
|
||||
DataBlobHash = latestSave.DataBlobHash,
|
||||
ReferencedUnityAssetIds = latestSave.ReferencedUnityAssetIds != null ? new List<string>(latestSave.ReferencedUnityAssetIds) : new List<string>(),
|
||||
UnitySubAssets = latestSave.UnitySubAssets != null ? new List<string>(latestSave.UnitySubAssets) : new List<string>(),
|
||||
ReferencedUnityAssets = latestSave.ReferencedUnityAssets != null ? new List<string>(latestSave.ReferencedUnityAssets) : new List<string>(),
|
||||
UnityAssetId = latestSave.UnityAssetId,
|
||||
PersistenceVersion = latestSave.PersistenceVersion,
|
||||
OMVersion = latestSave.OMVersion,
|
||||
UgcSubVersion = latestSave.UgcSubVersion,
|
||||
SavedByAccountId = playerId,
|
||||
SavedOnPlatform = latestSave.SavedOnPlatform,
|
||||
SavedOnDeviceClass = latestSave.SavedOnDeviceClass,
|
||||
// Description = "$"Cloned from subroom ^{original.Name}"",
|
||||
Description = "",
|
||||
Tags = latestSave.Tags != null ? new List<string>(latestSave.Tags) : new List<string>(),
|
||||
ModerationState = latestSave.ModerationState,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
SubRoomSaves.Insert(clonedSave);
|
||||
newSub.CurrentSave = clonedSave;
|
||||
newSub.DataBlob = clonedSave.DataBlob;
|
||||
}
|
||||
|
||||
room.SubRooms!.Add(newSub);
|
||||
Rooms.Update(room);
|
||||
return newSub;
|
||||
}
|
||||
|
||||
public static long CloneDormRoom(long playerId)
|
||||
{
|
||||
var baseRoom = GetRoom(1);
|
||||
if (baseRoom == null) throw new Exception("Base dorm room not found.");
|
||||
if (baseRoom.SubRooms == null || !baseRoom.SubRooms.Any()) throw new Exception("Base dorm room has no subrooms.");
|
||||
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(baseRoom);
|
||||
var clone = System.Text.Json.JsonSerializer.Deserialize<Room>(json);
|
||||
if (clone == null) throw new Exception("Failed to clone dorm room.");
|
||||
|
||||
clone.RoomId = 0;
|
||||
clone.CreatorAccountId = playerId;
|
||||
clone.CloningAllowed = false;
|
||||
clone.Name = "DormRoom";
|
||||
clone.ImageName = "DefaultRoomImage.jpg";
|
||||
clone.CreatedAt = DateTime.UtcNow;
|
||||
clone.Accessibility = RoomAccessibility.Private;
|
||||
clone.IsRRO = false;
|
||||
clone.IsDorm = true;
|
||||
clone.Stats = new Stats();
|
||||
clone.Roles = new List<Roles>
|
||||
{
|
||||
new Roles { AccountId = playerId, Role = Role.Creator, InvitedRole = Role.Creator }
|
||||
};
|
||||
|
||||
if (clone.SubRooms != null)
|
||||
{
|
||||
foreach (var sub in clone.SubRooms)
|
||||
{
|
||||
sub.SubRoomId = GetNextSubRoomId();
|
||||
sub.RoomId = 0;
|
||||
sub.SavedByAccountId = playerId;
|
||||
sub.CreatorAccountId = playerId;
|
||||
}
|
||||
}
|
||||
|
||||
return AddRoomSync(clone);
|
||||
}
|
||||
|
||||
public static void DeleteRoom(long roomId)
|
||||
{
|
||||
SubRoomSaves.DeleteMany(s => s.RoomId == roomId);
|
||||
Rooms.Delete(new BsonValue(roomId));
|
||||
}
|
||||
|
||||
public static void DeleteAllRoomsFromPlayerId(long creatorId)
|
||||
{
|
||||
var rooms = Rooms.Find(r => r.CreatorAccountId == creatorId).ToList();
|
||||
foreach (var room in rooms)
|
||||
{
|
||||
SubRoomSaves.DeleteMany(s => s.RoomId == room.RoomId);
|
||||
Rooms.Delete(room.RoomId);
|
||||
}
|
||||
Console.WriteLine($"[DB] Deleted {rooms.Count} rooms for creator {creatorId}.");
|
||||
}
|
||||
|
||||
public static Room? GetPlayerDormRoom(long playerId)
|
||||
{
|
||||
return Rooms.FindOne(r => r.IsDorm && r.CreatorAccountId == playerId);
|
||||
}
|
||||
|
||||
public static object? GetRoomSaveById(
|
||||
ulong roomId,
|
||||
ulong subroomId,
|
||||
ulong subRoomDataSaveId,
|
||||
int target,
|
||||
int version)
|
||||
{
|
||||
var save = SubRoomSaves.FindOne(s =>
|
||||
s.RoomId == (long)roomId &&
|
||||
s.SubRoomId == (long)subroomId &&
|
||||
s.SubRoomDataSaveId == (long)subRoomDataSaveId);
|
||||
|
||||
if (save == null)
|
||||
return null;
|
||||
|
||||
return new
|
||||
{
|
||||
save.SubRoomDataSaveId,
|
||||
save.RoomId,
|
||||
save.SubRoomId,
|
||||
save.DataBlob,
|
||||
save.Description,
|
||||
save.CreatedAt,
|
||||
Target = target,
|
||||
Version = version
|
||||
};
|
||||
}
|
||||
|
||||
public static RoomDBClasses.SubroomSaveResults? GetRoomSaves(ulong roomId, ulong subroomId, int skip = 0, int take = 20)
|
||||
{
|
||||
var saves = SubRoomSaves.Find(x => x.RoomId == (long)roomId && x.SubRoomId == (long)subroomId)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.ToList();
|
||||
|
||||
var paged = saves
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToList();
|
||||
|
||||
return new SubroomSaveResults
|
||||
{
|
||||
Results = paged,
|
||||
TotalCount = saves.Count
|
||||
};
|
||||
}
|
||||
|
||||
public static void FixDuplicateDorms()
|
||||
{
|
||||
var dormRooms = Rooms.Find(r => r.IsDorm).ToList();
|
||||
var grouped = dormRooms.GroupBy(r => r.CreatorAccountId).Where(g => g.Count() > 1);
|
||||
|
||||
foreach (var group in grouped)
|
||||
{
|
||||
var dormToKeep = group.OrderBy(r => r.CreatedAt).First();
|
||||
foreach (var room in group.Where(r => r.RoomId != dormToKeep.RoomId))
|
||||
{
|
||||
SubRoomSaves.DeleteMany(s => s.RoomId == room.RoomId);
|
||||
Rooms.Delete(room.RoomId);
|
||||
Console.WriteLine($"[DB] Deleted duplicate dorm {room.RoomId} for creator {room.CreatorAccountId}");
|
||||
}
|
||||
Console.WriteLine($"[DB] Kept dorm {dormToKeep.RoomId} for creator {group.Key}");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool DoesPlayerDormExist(long playerId)
|
||||
{
|
||||
var dorm = Rooms.FindOne(r => r.CreatorAccountId == playerId && r.IsDorm);
|
||||
return dorm != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using LiteDB;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Vanadium.Classes;
|
||||
using static Vanadium.Classes.DBs.DBClasses.RoomKeysDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class RoomKeysDB
|
||||
{
|
||||
public static LiteDatabase RoomKeysDBFile = new LiteDatabase(
|
||||
"Filename=" + Path.Join(Program.dataDir, "DBs", "RoomKeys.db") + ";Connection=shared");
|
||||
|
||||
public static readonly ILiteCollection<RoomKey> RoomKeys =
|
||||
RoomKeysDBFile.GetCollection<RoomKey>("RoomKeys");
|
||||
|
||||
public static readonly ILiteCollection<RoomKeyOwnership> Ownerships =
|
||||
RoomKeysDBFile.GetCollection<RoomKeyOwnership>("RoomKeyOwnerships");
|
||||
|
||||
public static void Setup()
|
||||
{
|
||||
RoomKeys.EnsureIndex(x => x.RoomKeyId);
|
||||
RoomKeys.EnsureIndex(x => x.RoomId);
|
||||
Ownerships.EnsureIndex(x => x.PlayerId);
|
||||
Ownerships.EnsureIndex(x => x.RoomKeyId);
|
||||
}
|
||||
|
||||
public static long GetNextRoomKeyId()
|
||||
{
|
||||
if (RoomKeys.Count() == 0) return 1;
|
||||
return RoomKeys.Max(x => x.RoomKeyId) + 1;
|
||||
}
|
||||
|
||||
public static RoomKey? CreateRoomKey(long roomId, string name, string? description, int price, string? purchaseCurrencyId)
|
||||
{
|
||||
var key = new RoomKey
|
||||
{
|
||||
RoomKeyId = GetNextRoomKeyId(),
|
||||
RoomId = roomId,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Price = price,
|
||||
PurchaseCurrencyId = string.IsNullOrEmpty(purchaseCurrencyId) ? null : purchaseCurrencyId,
|
||||
ReplicationId = Guid.NewGuid().ToString(),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
RoomKeys.Insert(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
public static RoomKey? GetRoomKey(long roomKeyId) => RoomKeys.FindById(roomKeyId);
|
||||
|
||||
public static List<RoomKey> GetRoomKeysByRoom(long roomId) =>
|
||||
RoomKeys.Find(k => k.RoomId == roomId).ToList();
|
||||
|
||||
public static List<RoomKey> GetRoomKeysByPlayer(long playerId)
|
||||
{
|
||||
var ownedIds = Ownerships
|
||||
.Find(o => o.PlayerId == playerId)
|
||||
.Select(o => o.RoomKeyId)
|
||||
.ToHashSet();
|
||||
|
||||
return RoomKeys.Find(k => ownedIds.Contains(k.RoomKeyId)).ToList();
|
||||
}
|
||||
|
||||
public static bool UpdateRoomKeyAll(long roomKeyId, string name, string? description, int price, string? purchaseCurrencyId)
|
||||
{
|
||||
var key = RoomKeys.FindById(roomKeyId);
|
||||
if (key == null) return false;
|
||||
|
||||
key.Name = name;
|
||||
key.Description = description;
|
||||
key.Price = price;
|
||||
key.PurchaseCurrencyId = string.IsNullOrEmpty(purchaseCurrencyId) ? null : purchaseCurrencyId;
|
||||
|
||||
return RoomKeys.Update(key);
|
||||
}
|
||||
|
||||
public static bool UpdateRoomKeyName(long roomKeyId, string name)
|
||||
{
|
||||
var key = RoomKeys.FindById(roomKeyId);
|
||||
if (key == null) return false;
|
||||
key.Name = name;
|
||||
return RoomKeys.Update(key);
|
||||
}
|
||||
|
||||
public static bool UpdateRoomKeyDescription(long roomKeyId, string description)
|
||||
{
|
||||
var key = RoomKeys.FindById(roomKeyId);
|
||||
if (key == null) return false;
|
||||
key.Description = description;
|
||||
return RoomKeys.Update(key);
|
||||
}
|
||||
|
||||
public static bool UpdateRoomKeyPrice(long roomKeyId, int price, string? purchaseCurrencyId)
|
||||
{
|
||||
var key = RoomKeys.FindById(roomKeyId);
|
||||
if (key == null) return false;
|
||||
key.Price = price;
|
||||
key.PurchaseCurrencyId = string.IsNullOrEmpty(purchaseCurrencyId) ? null : purchaseCurrencyId;
|
||||
return RoomKeys.Update(key);
|
||||
}
|
||||
|
||||
public static bool DeleteRoomKey(long roomKeyId)
|
||||
{
|
||||
Ownerships.DeleteMany(o => o.RoomKeyId == roomKeyId);
|
||||
return RoomKeys.Delete(roomKeyId);
|
||||
}
|
||||
|
||||
public static bool PlayerOwnsKey(long playerId, long roomKeyId) =>
|
||||
Ownerships.FindOne(o => o.PlayerId == playerId && o.RoomKeyId == roomKeyId) != null;
|
||||
|
||||
public static bool HasPurchasedKey(long playerId, long roomKeyId) =>
|
||||
Ownerships.FindOne(o => o.PlayerId == playerId && o.RoomKeyId == roomKeyId && o.WasPurchased) != null;
|
||||
|
||||
public static bool AwardKey(long playerId, long roomKeyId)
|
||||
{
|
||||
if (PlayerOwnsKey(playerId, roomKeyId)) return false;
|
||||
|
||||
Ownerships.Insert(new RoomKeyOwnership
|
||||
{
|
||||
PlayerId = playerId,
|
||||
RoomKeyId = roomKeyId,
|
||||
WasPurchased = false,
|
||||
AwardedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool GrantPurchasedKey(long playerId, long roomKeyId)
|
||||
{
|
||||
if (PlayerOwnsKey(playerId, roomKeyId)) return false;
|
||||
|
||||
Ownerships.Insert(new RoomKeyOwnership
|
||||
{
|
||||
PlayerId = playerId,
|
||||
RoomKeyId = roomKeyId,
|
||||
WasPurchased = true,
|
||||
AwardedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RevokeKey(long playerId, long roomKeyId)
|
||||
{
|
||||
var ownership = Ownerships.FindOne(o => o.PlayerId == playerId && o.RoomKeyId == roomKeyId);
|
||||
if (ownership == null) return false;
|
||||
return Ownerships.Delete(ownership.Id);
|
||||
}
|
||||
|
||||
public static void DeleteAllKeysForRoom(long roomId)
|
||||
{
|
||||
var keyIds = RoomKeys.Find(k => k.RoomId == roomId).Select(k => k.RoomKeyId).ToList();
|
||||
foreach (var id in keyIds)
|
||||
{
|
||||
Ownerships.DeleteMany(o => o.RoomKeyId == id);
|
||||
RoomKeys.Delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using LiteDB;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.StorefrontsDBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
|
||||
namespace Vanadium.Classes.DBs
|
||||
{
|
||||
public class StorefrontsDB
|
||||
{
|
||||
public static LiteDatabase StorefrontsDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Storefronts.db") + ";Connection=shared");
|
||||
public static readonly ILiteCollection<WishlistItem> Wishlists = StorefrontsDBFile.GetCollection<WishlistItem>("Wishlists");
|
||||
public static readonly ILiteCollection<AdCarouselItem> AdCarousel = StorefrontsDBFile.GetCollection<AdCarouselItem>("AdCarousel");
|
||||
public static readonly ILiteCollection<TransferableConsumable> TransferableConsumables = StorefrontsDBFile.GetCollection<TransferableConsumable>("TransferableConsumables");
|
||||
|
||||
static StorefrontsDB()
|
||||
{
|
||||
Wishlists.EnsureIndex(x => x.AccountId);
|
||||
Wishlists.EnsureIndex(x => x.PurchasableItemId);
|
||||
TransferableConsumables.EnsureIndex(x => x.PlayerId);
|
||||
}
|
||||
|
||||
public static GiftDropStoreResponse? GetGiftDropStore(int storefrontType)
|
||||
{
|
||||
string fileName = $"{storefrontType}.json";
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "Data", "Storefronts", fileName);
|
||||
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
var result = System.Text.Json.JsonSerializer.Deserialize<GiftDropStoreResponse>(json,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static StoreItem? GetStoreItemById(int storefrontType, int purchasableItemId)
|
||||
{
|
||||
var store = GetGiftDropStore(storefrontType);
|
||||
return store?.StoreItems.FirstOrDefault(x => x.PurchasableItemId == purchasableItemId);
|
||||
}
|
||||
|
||||
public static GiftDrop? GetGiftDropById(int storefrontType, int giftDropId)
|
||||
{
|
||||
var store = GetGiftDropStore(storefrontType);
|
||||
return store?.StoreItems.FirstOrDefault(x => x.GiftDrop?.GiftDropId == giftDropId)?.GiftDrop;
|
||||
}
|
||||
|
||||
public static GiftDrop? GetGiftDropByIdAcrossAllStores(int giftDropId)
|
||||
{
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
var drop = GetGiftDropById(i, giftDropId);
|
||||
if (drop != null)
|
||||
return drop;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<WishlistItem> GetWishlist(long accountId)
|
||||
{
|
||||
return Wishlists.Find(x => x.AccountId == accountId).ToList();
|
||||
}
|
||||
|
||||
public static WishlistItem? AddToWishlist(long accountId, int purchasableItemId)
|
||||
{
|
||||
var existing = Wishlists.FindOne(x => x.AccountId == accountId && x.PurchasableItemId == purchasableItemId);
|
||||
if (existing != null)
|
||||
return existing;
|
||||
|
||||
var item = new WishlistItem
|
||||
{
|
||||
WishlistItemId = Guid.NewGuid().ToString(),
|
||||
AccountId = accountId,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
PurchasableItemId = purchasableItemId
|
||||
};
|
||||
|
||||
Wishlists.Insert(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public static bool RemoveFromWishlist(long accountId, int purchasableItemId)
|
||||
{
|
||||
var item = Wishlists.FindOne(x => x.AccountId == accountId && x.PurchasableItemId == purchasableItemId);
|
||||
if (item == null)
|
||||
return false;
|
||||
return Wishlists.Delete(item.WishlistItemId);
|
||||
}
|
||||
|
||||
public static List<AdCarouselItem>? GetAdCarouselItems()
|
||||
{
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "Data", "Storefronts", "AdCarousel.json");
|
||||
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<List<AdCarouselItem>>(json,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static PlayerCurrency? DeductCurrency(long playerId, int currencyType, int amount)
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player?.Player?.PlayerExtra?.Currencies == null)
|
||||
return null;
|
||||
|
||||
var currency = player.Player.PlayerExtra.Currencies
|
||||
.FirstOrDefault(x => (int)x.CurrencyType == currencyType);
|
||||
|
||||
if (currency == null || currency.Balance < amount)
|
||||
return null;
|
||||
|
||||
currency.Balance -= amount;
|
||||
PlayerDB.Players.Update(player);
|
||||
return currency;
|
||||
}
|
||||
|
||||
public static PlayerCurrency? AddCurrency(long playerId, int currencyType, int amount, BalanceType balanceType = BalanceType.NonPurchasedNotUsableInP2P)
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player?.Player == null)
|
||||
return null;
|
||||
|
||||
player.Player.PlayerExtra ??= new PlayerExtra();
|
||||
player.Player.PlayerExtra.Currencies ??= new List<PlayerCurrency>();
|
||||
|
||||
var currency = player.Player.PlayerExtra.Currencies
|
||||
.FirstOrDefault(x => (int)x.CurrencyType == currencyType);
|
||||
|
||||
if (currency == null)
|
||||
{
|
||||
currency = new PlayerCurrency
|
||||
{
|
||||
Balance = 0,
|
||||
CurrencyType = (CurrencyType)currencyType,
|
||||
BalanceType = balanceType
|
||||
};
|
||||
player.Player.PlayerExtra.Currencies.Add(currency);
|
||||
}
|
||||
|
||||
currency.Balance += amount;
|
||||
PlayerDB.Players.Update(player);
|
||||
return currency;
|
||||
}
|
||||
|
||||
public static int GetBalance(long playerId, int currencyType)
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player?.Player?.PlayerExtra?.Currencies == null)
|
||||
return 0;
|
||||
|
||||
var currency = player.Player.PlayerExtra.Currencies
|
||||
.FirstOrDefault(x => (int)x.CurrencyType == currencyType);
|
||||
|
||||
return currency?.Balance ?? 0;
|
||||
}
|
||||
|
||||
public static List<TransferableConsumableGroup> GetTransferableConsumables(long playerId)
|
||||
{
|
||||
var items = TransferableConsumables
|
||||
.Find(x => x.PlayerId == playerId && x.IsTransferable)
|
||||
.ToList();
|
||||
|
||||
return items
|
||||
.GroupBy(x => x.ConsumableItemDesc)
|
||||
.Select(g =>
|
||||
{
|
||||
var first = g.First();
|
||||
return new TransferableConsumableGroup
|
||||
{
|
||||
ActiveDurationMinutes = first.ActiveDurationMinutes,
|
||||
ConsumableItemDesc = g.Key,
|
||||
Count = first.Count,
|
||||
InitialCount = first.InitialCount,
|
||||
IsActive = first.IsActive,
|
||||
IsTransferable = first.IsTransferable,
|
||||
CreatedAts = g.Select(x => x.CreatedAt).ToList(),
|
||||
Ids = g.Select(x => x.Id).ToList()
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<ConsumableItemDTO> GrantTransferableConsumables(long playerId, string consumableItemDesc, int count, int activeDurationMinutes)
|
||||
{
|
||||
var granted = new List<ConsumableItemDTO>();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var item = new TransferableConsumable
|
||||
{
|
||||
PlayerId = playerId,
|
||||
ConsumableItemDesc = consumableItemDesc,
|
||||
ActiveDurationMinutes = activeDurationMinutes,
|
||||
Count = 1,
|
||||
InitialCount = 0,
|
||||
IsActive = false,
|
||||
IsTransferable = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
TransferableConsumables.Insert(item);
|
||||
|
||||
granted.Add(new ConsumableItemDTO
|
||||
{
|
||||
Id = item.Id,
|
||||
ConsumableItemDesc = item.ConsumableItemDesc,
|
||||
ActiveDurationMinutes = item.ActiveDurationMinutes,
|
||||
Count = item.Count,
|
||||
InitialCount = item.InitialCount,
|
||||
IsActive = item.IsActive,
|
||||
IsTransferable = item.IsTransferable,
|
||||
CreatedAt = item.CreatedAt
|
||||
});
|
||||
}
|
||||
|
||||
return granted;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Vanadium.Classes;
|
||||
|
||||
namespace Vanadium.Utils
|
||||
{
|
||||
public static class ImageSignatureValidation
|
||||
{
|
||||
public static string GenerateCacheKey(string path, int? w, int? h, bool crop)
|
||||
{
|
||||
string rawKey = $"{path}_{w}_{h}_{crop}";
|
||||
byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(rawKey));
|
||||
return Convert.ToHexString(hashBytes);
|
||||
}
|
||||
|
||||
public static string SignPayload(byte[] payload, string sig)
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportParameters(ServerConfig.RsaParams);
|
||||
|
||||
byte[] signatureBytes = rsa.SignData(payload, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1);
|
||||
string base64Signature = Convert.ToBase64String(signatureBytes);
|
||||
|
||||
return $"key-id=KEY:RSA:{sig}.rec.net; data={base64Signature};";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
namespace Vanadium.Classes
|
||||
{
|
||||
public class NameGen
|
||||
{
|
||||
public static Random random = new Random();
|
||||
|
||||
public static List<string> Nouns = new List<string>
|
||||
{
|
||||
"Aardvark", "Alpaca", "Ant", "Armadillo", "Badger", "Bat", "Bear", "Bee", "Buffalo", "Butterfly",
|
||||
"Capybara", "Cat", "Caterpillar", "Chameleon", "Cheetah", "Chicken", "Chimpanzee", "Cobra",
|
||||
"Coyote", "Crane", "Cricket", "Crow", "Deer", "Dog", "Dolphin", "Donkey", "Dove", "Duck", "Eagle",
|
||||
"Echidna", "Elephant", "Elk", "Emu", "Ferret", "Flamingo", "Fish", "Fox", "Frog", "Gazelle", "Giraffe",
|
||||
"Goat", "Goose", "Gorilla", "Hamster", "Hedgehog", "Hippo", "Horse", "Hyena", "Iguana", "Jaguar",
|
||||
"Jellyfish",
|
||||
"Kangaroo",
|
||||
"Kitten",
|
||||
"Koala",
|
||||
"Lemming",
|
||||
"Leopard",
|
||||
"Lion",
|
||||
"Lizard",
|
||||
"Llama",
|
||||
"Marmoset",
|
||||
"Monkey",
|
||||
"Moose",
|
||||
"Mouse",
|
||||
"Mule",
|
||||
"Newt",
|
||||
"Octopus",
|
||||
"Opposum",
|
||||
"Ostrich",
|
||||
"Otter",
|
||||
"Owl",
|
||||
"Oyster",
|
||||
"Panda",
|
||||
"Panther",
|
||||
"Parrot",
|
||||
"Penguin",
|
||||
"Pig",
|
||||
"Pigeon",
|
||||
"Piranha",
|
||||
"Platypus",
|
||||
"Pony",
|
||||
"Possum",
|
||||
"Puppy",
|
||||
"Quail",
|
||||
"Rabbit",
|
||||
"Raven",
|
||||
"Salmon",
|
||||
"Scorpion",
|
||||
"Seal",
|
||||
"Shark",
|
||||
"Sheep",
|
||||
"Sloth",
|
||||
"Snail",
|
||||
"Squid",
|
||||
"Squirrel",
|
||||
"Stork",
|
||||
"Tapir",
|
||||
"Tiger",
|
||||
"Tortoise",
|
||||
"Tuna",
|
||||
"Turtle",
|
||||
"Urchin",
|
||||
"Viper",
|
||||
"Vulture",
|
||||
"Walrus",
|
||||
"Whale",
|
||||
"Wombat",
|
||||
"Yak",
|
||||
"Zebra"
|
||||
};
|
||||
|
||||
public static List<string> Adjectives = new List<string>
|
||||
{
|
||||
"Adamant",
|
||||
"Adorable",
|
||||
"Adventurous",
|
||||
"Agreeable",
|
||||
"Aimless",
|
||||
"Alert",
|
||||
"Amused",
|
||||
"Aromatic",
|
||||
"Bashful",
|
||||
"Beautiful",
|
||||
"Bored",
|
||||
"Brave",
|
||||
"Bulbous",
|
||||
"Busy",
|
||||
"Calm",
|
||||
"Carefree",
|
||||
"Careless",
|
||||
"Caring",
|
||||
"Charming",
|
||||
"Cheerful",
|
||||
"Clever",
|
||||
"Clumsy",
|
||||
"Courageous",
|
||||
"Cowardly",
|
||||
"Cozy",
|
||||
"Crabby",
|
||||
"Cranky",
|
||||
"Crawling",
|
||||
"Creaky",
|
||||
"Creative",
|
||||
"Crispy",
|
||||
"Decisive",
|
||||
"Deep",
|
||||
"Delightful",
|
||||
"Determined",
|
||||
"Diligent",
|
||||
"Dull",
|
||||
"Eager",
|
||||
"Elated",
|
||||
"Emotional",
|
||||
"Enchanting",
|
||||
"Encouraging",
|
||||
"Endless",
|
||||
"Energetic",
|
||||
"Enthusiastic",
|
||||
"Excited",
|
||||
"Exuberant",
|
||||
"Fair",
|
||||
"Faithful",
|
||||
"Fantastic",
|
||||
"Fastidious",
|
||||
"Fine",
|
||||
"Fluttering",
|
||||
"Fragrant",
|
||||
"Friendly",
|
||||
"Fulsome",
|
||||
"Funny",
|
||||
"Fussy",
|
||||
"Fuzzy",
|
||||
"Generous",
|
||||
"Gentle",
|
||||
"Glassy",
|
||||
"Gloomy",
|
||||
"Glorious",
|
||||
"Glowing",
|
||||
"Good",
|
||||
"Grand",
|
||||
"Great",
|
||||
"Greedy",
|
||||
"Grimy",
|
||||
"Happy",
|
||||
"Hardworking",
|
||||
"Hasty",
|
||||
"Healthy",
|
||||
"Heavy",
|
||||
"Helpful",
|
||||
"Hilarious",
|
||||
"Hopeful",
|
||||
"Icy",
|
||||
"Important",
|
||||
"Inquisitive",
|
||||
"Jolly",
|
||||
"Joyful",
|
||||
"Joyous",
|
||||
"Kind",
|
||||
"Lazy",
|
||||
"Lively",
|
||||
"Loud",
|
||||
"Lovely",
|
||||
"Loyal",
|
||||
"Lucky",
|
||||
"Luminous",
|
||||
"Lumpy",
|
||||
"Majestic",
|
||||
"Meek",
|
||||
"Melodic",
|
||||
"Mighty",
|
||||
"Moody",
|
||||
"Nice",
|
||||
"Nimble",
|
||||
"Odd",
|
||||
"Optimistic",
|
||||
"Perfect",
|
||||
"Pervasive",
|
||||
"Pleasant",
|
||||
"Plucky",
|
||||
"Plush",
|
||||
"Polite",
|
||||
"Practical",
|
||||
"Proud",
|
||||
"Quick",
|
||||
"Quiet",
|
||||
"Rapid",
|
||||
"Redolent",
|
||||
"Reliable",
|
||||
"Relieved",
|
||||
"Royal",
|
||||
"Rusty",
|
||||
"Scared",
|
||||
"Selfish",
|
||||
"Sensible",
|
||||
"Sensitive",
|
||||
"Shining",
|
||||
"Shrill",
|
||||
"Silly",
|
||||
"Sincere",
|
||||
"Sizzling",
|
||||
"Sleepy",
|
||||
"Smiling",
|
||||
"Smooth",
|
||||
"Snug",
|
||||
"Soaring",
|
||||
"Sparkling",
|
||||
"Speedy",
|
||||
"Spiky",
|
||||
"Splendid",
|
||||
"Spoiled",
|
||||
"Steaming",
|
||||
"Still",
|
||||
"Strict",
|
||||
"Stuffed",
|
||||
"Sturdy",
|
||||
"Successful",
|
||||
"Surprised",
|
||||
"Swift",
|
||||
"Taciturn",
|
||||
"Tense",
|
||||
"Thankful",
|
||||
"Thoughtful",
|
||||
"Thrifty",
|
||||
"Tough",
|
||||
"Tricky",
|
||||
"Truthful",
|
||||
"Ubiquitous",
|
||||
"Unusual",
|
||||
"Versatile",
|
||||
"Victorious",
|
||||
"Wild",
|
||||
"Wise",
|
||||
"Witty",
|
||||
"Wonderful",
|
||||
"Worried",
|
||||
"Wrinkly",
|
||||
"Zany",
|
||||
"Zealous"
|
||||
};
|
||||
|
||||
public static string GetRandomName()
|
||||
{
|
||||
string adjective = Adjectives[random.Next(Adjectives.Count)];
|
||||
string noun = Nouns[random.Next(Nouns.Count)];
|
||||
int number = random.Next(1000, 9999);
|
||||
return adjective + noun + number;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Vanadium.Classes.Rooms
|
||||
{
|
||||
public class LoadingScreenTip
|
||||
{
|
||||
public int PlatformMask { get; set; } = -1;
|
||||
public string Title { get; set; }
|
||||
public string Message { get; set; }
|
||||
public List<string> RoomNames { get; set; }
|
||||
public string ImageName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Vanadium.Classes.DBs;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using Vanadium.Controllers;
|
||||
|
||||
namespace Vanadium.Classes.Rooms
|
||||
{
|
||||
public class RoomUploadResponse
|
||||
{
|
||||
internal bool success;
|
||||
internal string value;
|
||||
|
||||
//public RoomController.RoomDataResponse Room { get; set; }
|
||||
public RoomDBClasses.SubRooms SubRoomDataSave { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Vanadium.Classes
|
||||
{
|
||||
public class Sanitize
|
||||
{
|
||||
public string Context { get; set; } = "RoomChat";
|
||||
public int Intent { get; set; } = 1;
|
||||
public bool PreRemoveBlockedCharacters { get; set; } = true;
|
||||
public string ReplacementChar { get; set; } = "*";
|
||||
public string Value { get; set; } = "";
|
||||
public int ruleset { get; set; } = 0;
|
||||
|
||||
private const char MaskChar = '*';
|
||||
|
||||
|
||||
private static readonly HashSet<string> Whitelist = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"password",
|
||||
"pass",
|
||||
"class",
|
||||
"assistant",
|
||||
"account"
|
||||
};
|
||||
|
||||
private static readonly List<(string Word, Regex Pattern)> SwearPatterns;
|
||||
|
||||
static Sanitize()
|
||||
{
|
||||
var swears = File.ReadAllLines("Data/APIS/swears.txt")
|
||||
.Select(x => x.Trim().ToLowerInvariant())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
SwearPatterns = swears
|
||||
.Select(word => (word, BuildFuzzyRegex(word)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static bool ContainsSwears(string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return false;
|
||||
|
||||
if (ServerConfig.AllowSwears)
|
||||
return false;
|
||||
|
||||
var normalized = Normalize(message);
|
||||
|
||||
return SwearPatterns.Any(p => p.Pattern.IsMatch(normalized));
|
||||
}
|
||||
|
||||
public static string SanitizeMessage(string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return message;
|
||||
|
||||
var tokens = Regex.Split(message, @"(\W+)");
|
||||
var sb = new StringBuilder(message.Length);
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (!IsWord(token))
|
||||
{
|
||||
sb.Append(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalized = Normalize(token);
|
||||
|
||||
if (normalized.Length < 3)
|
||||
{
|
||||
sb.Append(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Whitelist.Contains(normalized))
|
||||
{
|
||||
sb.Append(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool replaced = false;
|
||||
|
||||
foreach (var (_, pattern) in SwearPatterns)
|
||||
{
|
||||
if (pattern.IsMatch(normalized))
|
||||
{
|
||||
sb.Append(new string(MaskChar, token.Length));
|
||||
//Console.WriteLine($"[Sanitize] Censored word in message: '{token}'");
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!replaced)
|
||||
sb.Append(token);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static bool IsWord(string input) =>
|
||||
input.Any(char.IsLetterOrDigit);
|
||||
|
||||
private static Regex BuildFuzzyRegex(string word)
|
||||
{
|
||||
var pattern = string.Join(".{0,1}",
|
||||
word.Select(c => Regex.Escape(c.ToString())));
|
||||
|
||||
return new Regex(
|
||||
$"^{pattern}$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled
|
||||
);
|
||||
}
|
||||
|
||||
private static string Normalize(string input)
|
||||
{
|
||||
input = input.ToLowerInvariant();
|
||||
|
||||
input = Regex.Replace(input, @"[0@]", "o");
|
||||
input = Regex.Replace(input, @"[1!|]", "i");
|
||||
input = Regex.Replace(input, @"[$5]", "s");
|
||||
input = Regex.Replace(input, @"3", "e");
|
||||
input = Regex.Replace(input, @"7", "t");
|
||||
|
||||
input = Regex.Replace(input, @"[^a-z]", "");
|
||||
|
||||
input = Regex.Replace(input, @"(.)\1{2,}", "$1$1");
|
||||
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Vanadium.Classes
|
||||
{
|
||||
public class ServerConfig
|
||||
{
|
||||
private static string ConfigPath => Path.Combine(Environment.CurrentDirectory, "Configs", "ServerConfig.json");
|
||||
|
||||
private static ConfigData Load()
|
||||
{
|
||||
if (!File.Exists(ConfigPath))
|
||||
throw new FileNotFoundException("ServerConfig.json not found (this should never happen) " + ConfigPath);
|
||||
return JsonSerializer.Deserialize<ConfigData>(File.ReadAllText(ConfigPath),
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
||||
}
|
||||
|
||||
public static void Reload() => _ = Load();
|
||||
|
||||
public static object Bracket => new List<object>();
|
||||
|
||||
public static string BaseURL => Load().BaseURL;
|
||||
public static int GameVersion => Load().GameVersion;
|
||||
public static string ImagesWebhook => Load().ImagesWebhook;
|
||||
public static string InventionsReportWebhook => Load().InventionsReportWebhook;
|
||||
public static string AntiCheatWebhook => Load().AntiCheatWebhook;
|
||||
public static string PhotonWebhook => Load().PhotonWebhook;
|
||||
public static string BotToken => Load().BotToken;
|
||||
public static string LinkBotToken => Load().LinkBotToken;
|
||||
public static string CountBotToken => Load().CountBotToken;
|
||||
public static string RoomCommentsWebhook => Load().RoomCommentsWebhook;
|
||||
public static string BotStatus => Load().BotStatus;
|
||||
public static string SteamWebApiKey => Load().SteamWebApiKey;
|
||||
public static string OculusAppSecret => Load().OculusAppSecret;
|
||||
public static bool OculusAuth => Load().OculusAuth;
|
||||
public static bool EquipmentAntiCheatBanEnabled => Load().EquipmentAntiCheatBanEnabled;
|
||||
public static bool AutoAccount => Load().AutoAccount;
|
||||
public static bool CA_Auth => Load().CA_Auth;
|
||||
public static bool UseStatSig => Load().UseStatSig;
|
||||
public static bool PostingEnabled => Load().PostingEnabled;
|
||||
public static bool AllowSwears => Load().AllowSwears;
|
||||
public static bool DevsOnlyToken => Load().DevsOnlyToken;
|
||||
|
||||
/// <summary>Master switch for the Bluetint patch requirement. Off changes nothing.</summary>
|
||||
public static bool RequirePatchToPlay => Load().RequirePatchToPlay;
|
||||
|
||||
/// <summary>Seconds a fresh session has to check in before it is kicked.</summary>
|
||||
public static int PatchGraceSeconds => Load().PatchGraceSeconds;
|
||||
|
||||
/// <summary>Shown at the refused login and on the kick dialog.</summary>
|
||||
public static string PatchRequiredMessage => Load().PatchRequiredMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Require the patch to keep checking in, not just announce itself once. Only turn
|
||||
/// this on once you know the shipped client re-submits on a timer; if it does not,
|
||||
/// this kicks every honest player a few minutes into their session.
|
||||
/// </summary>
|
||||
public static bool PatchRequiresPeriodicCheckIn => Load().PatchRequiresPeriodicCheckIn;
|
||||
|
||||
/// <summary>Require proof of the patch in the login request itself. Needs client support.</summary>
|
||||
public static bool RequirePatchHeader => Load().RequirePatchHeader;
|
||||
|
||||
public static string PatchHeaderSecret => Load().PatchHeaderSecret;
|
||||
public static bool LetPCPlay => Load().LetPCPlay;
|
||||
public static long PCMaximumIdForPlay => Load().PCMaximumIdForPlay;
|
||||
|
||||
public static HashSet<ulong> WhitelistedSteamIds => Load().WhitelistedSteamIds.ToHashSet();
|
||||
public static HashSet<ulong> BlacklistedPlatformIds => Load().BlacklistedPlatformIds.ToHashSet();
|
||||
|
||||
public static bool IsSteamIdWhitelisted(ulong steamId) => true;
|
||||
|
||||
public static RSAParameters RsaParams
|
||||
{
|
||||
get
|
||||
{
|
||||
var cfg = Load();
|
||||
return new RSAParameters
|
||||
{
|
||||
Modulus = Convert.FromBase64String(cfg.RsaModulus),
|
||||
Exponent = Convert.FromBase64String(cfg.RsaExponent),
|
||||
P = Convert.FromBase64String(cfg.RsaP),
|
||||
Q = Convert.FromBase64String(cfg.RsaQ),
|
||||
DP = Convert.FromBase64String(cfg.RsaDP),
|
||||
DQ = Convert.FromBase64String(cfg.RsaDQ),
|
||||
InverseQ = Convert.FromBase64String(cfg.RsaInverseQ),
|
||||
D = Convert.FromBase64String(cfg.RsaD)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigData
|
||||
{
|
||||
public string BaseURL { get; set; } = "";
|
||||
public int GameVersion { get; set; }
|
||||
public string ImagesWebhook { get; set; } = "";
|
||||
public string InventionsReportWebhook { get; set; } = "";
|
||||
public string AntiCheatWebhook { get; set; } = "";
|
||||
public string PhotonWebhook { get; set; } = "";
|
||||
public string RoomCommentsWebhook { get; set; } = "";
|
||||
public string BotToken { get; set; } = "";
|
||||
public string LinkBotToken { get; set; } = "";
|
||||
public string CountBotToken { get; set; } = "";
|
||||
public string BotStatus { get; set; } = "";
|
||||
public string SteamWebApiKey { get; set; } = "";
|
||||
public string OculusAppSecret { get; set; } = "";
|
||||
public bool OculusAuth { get; set; } = true;
|
||||
public bool EquipmentAntiCheatBanEnabled { get; set; }
|
||||
public bool AutoAccount { get; set; }
|
||||
public bool CA_Auth { get; set; }
|
||||
public bool UseStatSig { get; set; }
|
||||
public bool PostingEnabled { get; set; }
|
||||
public bool AllowSwears { get; set; }
|
||||
public bool DevsOnlyToken { get; set; }
|
||||
public bool RequirePatchToPlay { get; set; } = false;
|
||||
public int PatchGraceSeconds { get; set; } = 180;
|
||||
public string PatchRequiredMessage { get; set; } = "No skidding. Download the Bluetint patch";
|
||||
public bool PatchRequiresPeriodicCheckIn { get; set; } = false;
|
||||
public bool RequirePatchHeader { get; set; } = false;
|
||||
public string PatchHeaderSecret { get; set; } = "";
|
||||
public List<ulong> WhitelistedSteamIds { get; set; } = new();
|
||||
public List<ulong> BlacklistedPlatformIds { get; set; } = new();
|
||||
public string PhotonRealtimeId { get; set; } = "";
|
||||
public string PhotonVoiceId { get; set; } = "";
|
||||
public string PhotonRegion { get; set; } = "us";
|
||||
public string RsaModulus { get; set; } = "";
|
||||
public string RsaExponent { get; set; } = "";
|
||||
public string RsaP { get; set; } = "";
|
||||
public string RsaQ { get; set; } = "";
|
||||
public string RsaDP { get; set; } = "";
|
||||
public string RsaDQ { get; set; } = "";
|
||||
public string RsaInverseQ { get; set; } = "";
|
||||
public string RsaD { get; set; } = "";
|
||||
public bool LetPCPlay { get; set; } = false;
|
||||
public long PCMaximumIdForPlay { get; set; } = 3000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* rsa key
|
||||
|
||||
sq/7i1nBoHMmlUInLsaGlqrVgwbmE/u+O5RLfJMCkGvHhfPrBz8tOEmujbKFuCQOWZDA8tqD3s6aWo/mSRh7NGfs80eArL/LIs8sebwZkPQx0buRbtVgWggqtI68lZQQIY4jUTCEgYcl9jNoXD+9zCt5p/NUoplg0plga1QhyHfEnGmSVt8bNsx5Zynz+Gg+8GNkxvcCwiGNcH7Se1v+mx0m/ZENFl7OGlOA9go7aG0HaTayeo2Hc7vGfVLUxw3PNiEbC5MJcTPoqYY9dQnRBaPdMAwz3coXur2//Lf3lA+ijWIFhpnAAuciLYRRcbR7l3b/ikRqC3xdvsVQ9/3CvQ==
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,379 @@
|
||||
using Vanadium.Classes.DBs;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.InstanceDBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.RoomDBClasses;
|
||||
using static Vanadium.Classes.ServerConfig;
|
||||
|
||||
namespace Vanadium.Classes
|
||||
{
|
||||
public class Sessions // yo
|
||||
{
|
||||
private static readonly Random _rng = new();
|
||||
private static readonly object _lock = new();
|
||||
|
||||
private static RoomInstance BuildInstance(string roomName, string dataBlob, SubRooms sub, long roomId, bool isPrivate)
|
||||
{
|
||||
long id = _rng.NextInt64(100_000_000_000L, long.MaxValue);
|
||||
var ri = new RoomInstance
|
||||
{
|
||||
Name = $"^{roomName}",
|
||||
dataBlob = dataBlob,
|
||||
isPrivate = isPrivate,
|
||||
location = sub.UnitySceneId,
|
||||
maxCapacity = sub.MaxPlayers,
|
||||
photonRegion = "us",
|
||||
photonRegionId = "us",
|
||||
photonRoomId = $"{roomName}-{id}-room",
|
||||
roomId = roomId,
|
||||
roomInstanceId = id,
|
||||
subRoomId = sub.SubRoomId
|
||||
};
|
||||
InstanceDB.Upsert(ri, isPrivate);
|
||||
return ri;
|
||||
}
|
||||
|
||||
private static SubRooms? ResolveSubRoom(Room roomData, long? subRoomId)
|
||||
{
|
||||
if (subRoomId.HasValue)
|
||||
return roomData.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId.Value);
|
||||
|
||||
var publicSubs = roomData.SubRooms?
|
||||
.Where(s => s.Accessibility == RoomAccessibility.Public)
|
||||
.ToList();
|
||||
|
||||
if (publicSubs?.Count > 0)
|
||||
return publicSubs[Random.Shared.Next(publicSubs.Count)];
|
||||
|
||||
return roomData.SubRooms?.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static void AutoFollowPlayers(long playerId, RoomInstance instance)
|
||||
{
|
||||
foreach (var other in PlayerDB.Players.FindAll()
|
||||
.Where(p => p.PlayerId != playerId &&
|
||||
p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == instance.roomInstanceId))
|
||||
PlayerDB.UpdatePlayerHeartbeat(other.PlayerId, instance);
|
||||
}
|
||||
|
||||
private static Heartbeat? ApplyPlayerId(long playerId, Heartbeat? hb)
|
||||
{
|
||||
if (hb != null) hb.playerId = playerId;
|
||||
return hb;
|
||||
}
|
||||
|
||||
private static HashSet<long> GetFriendIds(long playerId)
|
||||
=> FriendsDB.GetRelationships(playerId)
|
||||
.Where(r => r.RelationshipType == FriendsDBClasses.RelationshipType.Friend)
|
||||
.Select(r => r.OtherPlayerID)
|
||||
.ToHashSet();
|
||||
|
||||
private static void Prune(long roomId)
|
||||
{
|
||||
foreach (var entry in InstanceDB.GetAllForRoom(roomId))
|
||||
{
|
||||
if (InstanceDB.CountPlayers(entry.RoomInstanceId) == 0)
|
||||
InstanceDB.Delete(entry.RoomInstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
private static RoomInstance? FindBestPublicInstance(long playerId, long roomId, long? subRoomId, long? excludeInstanceId = null)
|
||||
{
|
||||
var friendIds = GetFriendIds(playerId);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll()
|
||||
.Where(p =>
|
||||
p.Player?.PlayerExtra?.Heartbeat?.isOnline == true &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance != null &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance.roomId == roomId &&
|
||||
!p.Player.PlayerExtra.Heartbeat.roomInstance.isPrivate &&
|
||||
!p.Player.PlayerExtra.Heartbeat.roomInstance.isFull &&
|
||||
(excludeInstanceId == null || p.Player.PlayerExtra.Heartbeat.roomInstance.roomInstanceId != excludeInstanceId))
|
||||
.ToList();
|
||||
|
||||
var candidates = allPlayers
|
||||
.Select(p => p.Player!.PlayerExtra!.Heartbeat!.roomInstance!)
|
||||
.Where(ri => !subRoomId.HasValue || ri.subRoomId == subRoomId.Value)
|
||||
.GroupBy(ri => ri.roomInstanceId)
|
||||
.Select(g => g.First())
|
||||
.Where(ri => InstanceDB.CountPlayers(ri.roomInstanceId) < ri.maxCapacity)
|
||||
.ToList();
|
||||
|
||||
var friendInstance = candidates
|
||||
.Where(ri => allPlayers.Any(p =>
|
||||
friendIds.Contains(p.PlayerId) &&
|
||||
p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == ri.roomInstanceId))
|
||||
.OrderByDescending(ri => InstanceDB.CountPlayers(ri.roomInstanceId))
|
||||
.FirstOrDefault();
|
||||
|
||||
return friendInstance ?? candidates
|
||||
.OrderByDescending(ri => InstanceDB.CountPlayers(ri.roomInstanceId))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
public static void MarkInstanceInactive(long roomInstanceId)
|
||||
{
|
||||
lock (_lock)
|
||||
InstanceDB.Delete(roomInstanceId);
|
||||
}
|
||||
|
||||
public static async Task MarkInstancePrivate(long roomInstanceId)
|
||||
{
|
||||
lock (_lock)
|
||||
InstanceDB.MarkPrivate(roomInstanceId);
|
||||
|
||||
foreach (var p in PlayerDB.Players.FindAll()
|
||||
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId))
|
||||
{
|
||||
var hb = p.Player!.PlayerExtra!.Heartbeat!;
|
||||
hb.playerId = p.PlayerId;
|
||||
hb.roomInstance!.isPrivate = true;
|
||||
await Controllers.NotificationsController.RefreshHeartbeat(p.PlayerId, hb);
|
||||
}
|
||||
}
|
||||
|
||||
public static void FakeInstanceFull(long roomInstanceId)
|
||||
{
|
||||
lock (_lock)
|
||||
InstanceDB.MarkFull(roomInstanceId, true);
|
||||
}
|
||||
|
||||
public static async Task<(int instancesRefreshed, int playersMoved)> RefreshPublicInstances(long roomId, long subRoomId)
|
||||
{
|
||||
var roomData = RoomDB.GetRoom(roomId);
|
||||
if (roomData == null) return (0, 0);
|
||||
|
||||
var sub = roomData.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId);
|
||||
if (sub == null) return (0, 0);
|
||||
|
||||
var moved = new List<(long playerId, Heartbeat hb)>();
|
||||
int refreshed = 0;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var occupants = PlayerDB.Players.FindAll()
|
||||
.Where(p =>
|
||||
p.Player?.PlayerExtra?.Heartbeat?.isOnline == true &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance != null &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance.roomId == roomId &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance.subRoomId == subRoomId &&
|
||||
!p.Player.PlayerExtra.Heartbeat.roomInstance.isPrivate)
|
||||
.ToList();
|
||||
|
||||
foreach (var group in occupants.GroupBy(p => p.Player!.PlayerExtra!.Heartbeat!.roomInstance!.roomInstanceId))
|
||||
{
|
||||
var fresh = BuildInstance(roomData.Name ?? "UnknownRoom", sub.DataBlob ?? "", sub, roomId, isPrivate: false);
|
||||
|
||||
foreach (var p in group)
|
||||
{
|
||||
var hb = PlayerDB.UpdatePlayerHeartbeat(p.PlayerId, fresh);
|
||||
if (hb == null) continue;
|
||||
hb.playerId = p.PlayerId;
|
||||
moved.Add((p.PlayerId, hb));
|
||||
}
|
||||
|
||||
InstanceDB.Delete(group.Key);
|
||||
refreshed++;
|
||||
}
|
||||
|
||||
Prune(roomId);
|
||||
}
|
||||
|
||||
foreach (var (playerId, hb) in moved)
|
||||
await Controllers.NotificationsController.RefreshHeartbeat(playerId, hb);
|
||||
|
||||
return (refreshed, moved.Count);
|
||||
}
|
||||
|
||||
public static async Task<int> SetInstancesPrivacy(long roomId, long subRoomId, bool isPrivate)
|
||||
{
|
||||
var updated = new List<(long playerId, Heartbeat hb)>();
|
||||
HashSet<long> instanceIds;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var occupants = PlayerDB.Players.FindAll()
|
||||
.Where(p =>
|
||||
p.Player?.PlayerExtra?.Heartbeat?.isOnline == true &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance != null &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance.roomId == roomId &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance.subRoomId == subRoomId &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance.isPrivate != isPrivate)
|
||||
.ToList();
|
||||
|
||||
instanceIds = occupants
|
||||
.Select(p => p.Player!.PlayerExtra!.Heartbeat!.roomInstance!.roomInstanceId)
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var instanceId in instanceIds)
|
||||
{
|
||||
if (isPrivate) InstanceDB.MarkPrivate(instanceId);
|
||||
else InstanceDB.MarkPublic(instanceId);
|
||||
}
|
||||
|
||||
foreach (var p in occupants)
|
||||
{
|
||||
var hb = p.Player!.PlayerExtra!.Heartbeat!;
|
||||
hb.playerId = p.PlayerId;
|
||||
hb.roomInstance!.isPrivate = isPrivate;
|
||||
updated.Add((p.PlayerId, hb));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (playerId, hb) in updated)
|
||||
await Controllers.NotificationsController.RefreshHeartbeat(playerId, hb);
|
||||
|
||||
return instanceIds.Count;
|
||||
}
|
||||
|
||||
public static bool IsInstanceActuallyFull(long roomInstanceId)
|
||||
=> InstanceDB.IsFull(roomInstanceId);
|
||||
|
||||
public static int CountPlayersInInstance(long roomInstanceId)
|
||||
=> InstanceDB.CountPlayers(roomInstanceId);
|
||||
|
||||
public static Heartbeat? CreateRoom(
|
||||
long playerId,
|
||||
long roomId,
|
||||
long? subRoomId = null,
|
||||
string? dataBlobOverride = null,
|
||||
int joinMode = 0,
|
||||
bool additionalPlayersAutoFollow = false,
|
||||
string roomNameOverride = "") // todo remove ^
|
||||
{
|
||||
var roomData = RoomDB.GetRoom(roomId);
|
||||
if (roomData == null) return null;
|
||||
|
||||
bool isPrivateInstance = joinMode == 2;
|
||||
bool isPrivateRoom = roomData.Accessibility == RoomAccessibility.Private;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
Prune(roomId);
|
||||
|
||||
if (isPrivateInstance)
|
||||
{
|
||||
var sub = ResolveSubRoom(roomData, subRoomId);
|
||||
if (sub == null) return null;
|
||||
var solo = BuildInstance(roomData.Name ?? "UnknownRoom", dataBlobOverride ?? sub.DataBlob ?? "", sub, roomId, isPrivate: true);
|
||||
return ApplyPlayerId(playerId, PlayerDB.UpdatePlayerHeartbeat(playerId, solo));
|
||||
}
|
||||
|
||||
if (isPrivateRoom)
|
||||
{
|
||||
var sub = ResolveSubRoom(roomData, subRoomId);
|
||||
if (sub == null) return null;
|
||||
|
||||
var existing = InstanceDB.GetActivePrivate(roomId, sub.SubRoomId);
|
||||
if (existing != null)
|
||||
{
|
||||
int count = InstanceDB.CountPlayers(existing.RoomInstanceId);
|
||||
if (!existing.IsFull && count < existing.MaxCapacity)
|
||||
{
|
||||
var ri = existing.ToRoomInstance();
|
||||
if (additionalPlayersAutoFollow) AutoFollowPlayers(playerId, ri);
|
||||
return ApplyPlayerId(playerId, PlayerDB.UpdatePlayerHeartbeat(playerId, ri));
|
||||
}
|
||||
}
|
||||
|
||||
var liveInstance = PlayerDB.Players.FindAll()
|
||||
.Where(p =>
|
||||
p.Player?.PlayerExtra?.Heartbeat?.isOnline == true &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance?.roomId == roomId &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance.subRoomId == sub.SubRoomId)
|
||||
.Select(p => p.Player!.PlayerExtra!.Heartbeat!.roomInstance!)
|
||||
.GroupBy(ri => ri.roomInstanceId)
|
||||
.Select(g => g.First())
|
||||
.Where(ri => !ri.isFull && InstanceDB.CountPlayers(ri.roomInstanceId) < ri.maxCapacity)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (liveInstance != null)
|
||||
{
|
||||
InstanceDB.Upsert(liveInstance, isPrivate: true);
|
||||
if (additionalPlayersAutoFollow) AutoFollowPlayers(playerId, liveInstance);
|
||||
return ApplyPlayerId(playerId, PlayerDB.UpdatePlayerHeartbeat(playerId, liveInstance));
|
||||
}
|
||||
|
||||
var newInstance = BuildInstance(roomData.Name ?? "UnknownRoom", dataBlobOverride ?? sub.DataBlob ?? "", sub, roomId, isPrivate: true);
|
||||
return ApplyPlayerId(playerId, PlayerDB.UpdatePlayerHeartbeat(playerId, newInstance));
|
||||
}
|
||||
|
||||
if (joinMode == 1)
|
||||
{
|
||||
var currentHb = PlayerDB.GetPlayerHeartbeat(playerId);
|
||||
long? currentInstanceId = currentHb?.roomInstance?.roomInstanceId;
|
||||
|
||||
var different = FindBestPublicInstance(playerId, roomId, subRoomId, excludeInstanceId: currentInstanceId);
|
||||
if (different != null)
|
||||
{
|
||||
if (additionalPlayersAutoFollow) AutoFollowPlayers(playerId, different);
|
||||
return ApplyPlayerId(playerId, PlayerDB.UpdatePlayerHeartbeat(playerId, different));
|
||||
}
|
||||
|
||||
var sub = ResolveSubRoom(roomData, subRoomId);
|
||||
if (sub == null) return null;
|
||||
var fresh = BuildInstance(roomData.Name ?? "UnknownRoom", dataBlobOverride ?? sub.DataBlob ?? "", sub, roomId, isPrivate: false);
|
||||
return ApplyPlayerId(playerId, PlayerDB.UpdatePlayerHeartbeat(playerId, fresh));
|
||||
}
|
||||
|
||||
var best = FindBestPublicInstance(playerId, roomId, subRoomId);
|
||||
if (best != null)
|
||||
{
|
||||
if (additionalPlayersAutoFollow) AutoFollowPlayers(playerId, best);
|
||||
return ApplyPlayerId(playerId, PlayerDB.UpdatePlayerHeartbeat(playerId, best));
|
||||
}
|
||||
|
||||
var resolvedSub = ResolveSubRoom(roomData, subRoomId);
|
||||
if (resolvedSub == null) return null;
|
||||
var created = BuildInstance(roomData.Name ?? "UnknownRoom", dataBlobOverride ?? resolvedSub.DataBlob ?? "", resolvedSub, roomId, isPrivate: false);
|
||||
return ApplyPlayerId(playerId, PlayerDB.UpdatePlayerHeartbeat(playerId, created));
|
||||
}
|
||||
}
|
||||
|
||||
public static Heartbeat CreateDorm(long playerId, string username, long dormRoomId)
|
||||
{
|
||||
var dorm = RoomDB.GetRoom(dormRoomId);
|
||||
var firstSub = dorm?.SubRooms?.FirstOrDefault();
|
||||
long subRoomId = firstSub?.SubRoomId ?? 0;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
Prune(dormRoomId);
|
||||
|
||||
var existing = InstanceDB.GetActivePrivate(dormRoomId, subRoomId);
|
||||
if (existing != null)
|
||||
{
|
||||
int count = InstanceDB.CountPlayers(existing.RoomInstanceId);
|
||||
if (!existing.IsFull && count < existing.MaxCapacity)
|
||||
{
|
||||
var existingRi = existing.ToRoomInstance();
|
||||
var existingHb = PlayerDB.UpdatePlayerHeartbeat(playerId, existingRi);
|
||||
existingHb.playerId = playerId;
|
||||
return existingHb;
|
||||
}
|
||||
}
|
||||
|
||||
long instanceId = _rng.NextInt64(100_000_000_000L, long.MaxValue);
|
||||
var session = new RoomInstance
|
||||
{
|
||||
isPrivate = true,
|
||||
location = "76d98498-60a1-430c-ab76-b54a29b7a163",
|
||||
maxCapacity = 4,
|
||||
Name = $"@{username}'s Dorm",
|
||||
photonRegion = "us",
|
||||
photonRegionId = "us",
|
||||
photonRoomId = $"DormRoom-{instanceId}-room",
|
||||
roomId = dormRoomId,
|
||||
roomInstanceId = instanceId,
|
||||
subRoomId = subRoomId,
|
||||
dataBlob = firstSub?.DataBlob ?? ""
|
||||
};
|
||||
|
||||
InstanceDB.Upsert(session, isPrivate: true);
|
||||
var hb = PlayerDB.UpdatePlayerHeartbeat(playerId, session);
|
||||
hb.playerId = playerId;
|
||||
return hb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Vanadium.Classes
|
||||
{
|
||||
public class Sub
|
||||
{
|
||||
public ulong creatorAccountId { get; set; }
|
||||
public ulong clubId { get; set; }
|
||||
public int membershipType { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user