This commit is contained in:
skidfire
2026-09-06 19:59:27 -04:00
parent 55d8e11a5d
commit fe3468c65d
247 changed files with 338183 additions and 0 deletions
+315
View File
@@ -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}");
}
}
}
}
+87
View File
@@ -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
};
}
}
}
+60
View File
@@ -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;
}
}
}
+55
View File
@@ -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;
}
}
}
+305
View File
@@ -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);
}
}
}
+838
View File
@@ -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
};
}
}
+94
View File
@@ -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;
}
}
}
+82
View File
@@ -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
}
}
}
+209
View File
@@ -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
}
}
}
+150
View File
@@ -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; }
}
}
}
+53
View File
@@ -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
}
}
}
+59
View File
@@ -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;
}
}
}
+50
View File
@@ -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
};
}
}
}
+234
View File
@@ -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; }
}
}
}
+19
View File
@@ -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; } = "";
}
}
}
+673
View File
@@ -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
}
}
}
+45
View File
@@ -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,
}
}
}
+43
View File
@@ -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;
}
}
}
+287
View File
@@ -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;
}
}
}
+300
View File
@@ -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;
}
}
}
+209
View File
@@ -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);
}
}
}
}
+112
View File
@@ -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
};
}
}
}
+190
View File
@@ -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;
}
}
}
+309
View File
@@ -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;
}
}
+200
View File
@@ -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;
}
}
}
+639
View File
@@ -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];
}
}
}
+342
View File
@@ -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
}
};
}
}
}
+88
View File
@@ -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);
}
}
}
+1637
View File
@@ -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);
}
}
}
}
+43
View File
@@ -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);
}
}
}
+81
View File
@@ -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;
}
}
}
+194
View File
@@ -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;
}
}
}
+1392
View File
@@ -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;
}
}
}
+166
View File
@@ -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);
}
}
}
}
+235
View File
@@ -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;
}
}
}
+27
View File
@@ -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};";
}
}
}
+251
View File
@@ -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;
}
}
}
+11
View File
@@ -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; }
}
}
+15
View File
@@ -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; }
}
}
+136
View File
@@ -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;
}
}
}
+143
View File
@@ -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==
*/
+379
View File
@@ -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;
}
}
}
}
+9
View File
@@ -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; }
}
}