Files
skidfire fe3468c65d yikes,
2026-09-06 19:59:27 -04:00

1637 lines
65 KiB
C#

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);
}
}
}
}