Files
Mocha-2023-RecRoom-Server/Classes/DBs/PlayerDB.cs
T
mochaisvibecoded 42f7f57b72 :3
2026-08-24 17:00:26 -04:00

3733 lines
126 KiB
C#

using System;
using LiteDB;
using Mocha2023.Classes;
using Mocha2023.Classes.DBs.DBClasses;
using Mocha2023.Controllers;
using static Mocha2023.Classes.DBs.DBClasses.PlayerDBClasses;
using System.Globalization;
namespace Mocha2023.Classes.DBs
{
public class PlayerDB
{
private static readonly object SocialLock = new();
private static readonly object ProgressionLock = new();
private static readonly object PartyLock = new();
private static readonly object GiftLock = new();
private static long PartySequence = DateTime.UtcNow.Ticks & long.MaxValue;
private static long GiftSequence =
DateTimeOffset.UtcNow.ToUnixTimeSeconds();
private static long CheerSequence = DateTime.UtcNow.Ticks & long.MaxValue;
private const int RoomLevelSeconds = 15 * 60;
private const int MaximumLevel = 50;
private const int MaximumCreditableHeartbeatGapSeconds = 120;
private const int PresenceFreshnessSeconds = 120;
private const int MaximumPendingGiftPackages = 200;
public const int CheerCreditLimit = 15;
private const long CheerCreditRefreshSeconds = 5 * 60 * 60;
public static LiteDatabase PlayerDBFile =
new LiteDatabase(Path.Combine(Program.dataDir, "DBs", "Players.db"));
public static readonly ILiteCollection<FullPlayer> Players =
PlayerDBFile.GetCollection<FullPlayer>("Players");
private const string RRPlusSettingKey = "RecRoomPlus.IsActive";
private const string InitialTokensSettingKey =
"Mocha.InitialTokensGranted";
private const string AlpacaShirtOwnershipResetSettingKey =
"Mocha.AlpacaShirtOwnershipResetV1";
public const long AlpacaShirtAvatarItemId = 1521;
public const string AlpacaShirtAvatarItemDescriptor =
"d0a9262f-5504-46a7-bb10-7507503db58e,941c046e-4e95-49f8-a7d7-19071fcc3c94,0440f08f-ef1d-49d8-942b-523056e8bb45,703ff56b-560d-4ff4-8c63-d195e879a328";
public const int InitialTokenBalance = 1_000;
private const string PlaceholderDisplayName =
"Displaynamecouldbeverylong";
private static readonly Random LevelBoxRng = new();
private static int GetLevelBoxRarity(int level) => level switch
{
<= 19 => 10,
<= 29 => 20,
<= 39 => 30,
<= 49 => 40,
_ => 50
};
static PlayerDB()
{
LiteDbMaintenance.StartPeriodicCheckpoint("Players.db", PlayerDBFile);
try
{
int repaired = RepairPlaceholderDisplayNames();
if (repaired > 0)
{
Console.WriteLine(
$"[ACCOUNT DISPLAYNAME MIGRATION] repaired={repaired}");
}
}
catch (Exception exception)
{
Console.WriteLine(
$"[ACCOUNT DISPLAYNAME MIGRATION] failed={exception.Message}");
}
}
private static int RepairPlaceholderDisplayNames()
{
int repaired = 0;
foreach (FullPlayer account in Players.FindAll().ToList())
{
Player? player = account?.Player;
if (player == null)
continue;
string currentDisplayName =
player.DisplayName?.Trim() ?? string.Empty;
if (!string.IsNullOrWhiteSpace(currentDisplayName) &&
!IsPlaceholderDisplayName(currentDisplayName))
{
continue;
}
string username =
player.Username?.Trim().TrimStart('@') ?? string.Empty;
string replacement = string.IsNullOrWhiteSpace(username)
? $"Player{account.PlayerId}"
: username;
if (string.Equals(
player.DisplayName,
replacement,
StringComparison.Ordinal))
{
continue;
}
player.DisplayName = replacement;
if (Players.Update(account))
repaired++;
}
return repaired;
}
private static bool IsPlaceholderDisplayName(string? value)
{
return string.Equals(
value?.Trim(),
PlaceholderDisplayName,
StringComparison.OrdinalIgnoreCase);
}
public static FullPlayer CreateAccount(
Platforms platform,
ulong platformId,
bool isJunior,
long? accountId = null,
bool completeAccountCreation = true)
{
string username = NameGen.GetRandomName();
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,
PlayerExtra = new PlayerExtra
{
Currencies = new List<PlayerCurrency>
{
new PlayerCurrency
{
CurrencyType = CurrencyType.RecCenterTokens,
BalanceType = BalanceType.NonPurchasedDefault,
Balance = InitialTokenBalance
}
},
Settings = new List<Setting>
{
new Setting
{
Key = InitialTokensSettingKey,
Value = "True"
},
new Setting
{
Key = AlpacaShirtOwnershipResetSettingKey,
Value = "True"
},
new Setting
{
Key = "Recroom.AccountCreation.HasStarted",
Value = "True"
},
new Setting
{
Key = "Recroom.AccountCreation.HasChosenUsername",
Value = completeAccountCreation ? "True" : "False"
},
new Setting
{
Key = "Recroom.AccountCreation.HasCreatedPassword",
Value = completeAccountCreation ? "True" : "False"
},
new Setting
{
Key = "Recroom.AccountCreation.HasFinished",
Value = completeAccountCreation ? "True" : "False"
},
new Setting
{
Key = "TUTORIAL_COMPLETE_MASK",
Value = completeAccountCreation ? "57" : "0"
},
new Setting
{
Key = "HAS_COMPLETED_ORIENTATION",
Value = "False"
},
new Setting
{
Key = RRPlusSettingKey,
Value = "False"
}
}
}
};
var newFullPlayer = new FullPlayer
{
PlayerId = accountId ?? 0,
PlatformIds = new List<mPlatformID>
{
new mPlatformID
{
Platform = platform,
PlatformId = platformId
}
},
Player = newPlayerData,
PlayerRoles = new List<PlayerRoles>(),
AuthToken = Guid.NewGuid().ToString()
};
Players.Insert(newFullPlayer);
EquipmentInventoryStore.EnsureInitialized(newFullPlayer.PlayerId);
PlayerInventoryStore.EnsureInitialized(
newFullPlayer.PlayerId,
newFullPlayer.Player.PlayerExtra?.AvatarItems);
RoomDB.GetOrCreatePlayerDorm(newFullPlayer.PlayerId);
return newFullPlayer;
}
public static bool HasRRPlus(long playerId)
{
if (playerId <= 0)
return false;
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
var setting = player.Player.PlayerExtra?.Settings?
.FirstOrDefault(x => string.Equals(
x.Key,
RRPlusSettingKey,
StringComparison.OrdinalIgnoreCase));
return setting != null &&
string.Equals(setting.Value, "True", StringComparison.OrdinalIgnoreCase);
}
public static bool SetRRPlus(long playerId, bool enabled)
{
if (playerId <= 0)
return false;
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Settings ??= new List<Setting>();
var setting = player.Player.PlayerExtra.Settings
.FirstOrDefault(x => string.Equals(
x.Key,
RRPlusSettingKey,
StringComparison.OrdinalIgnoreCase));
if (setting == null)
{
player.Player.PlayerExtra.Settings.Add(new Setting
{
Key = RRPlusSettingKey,
Value = enabled ? "True" : "False"
});
}
else
{
setting.Key = RRPlusSettingKey;
setting.Value = enabled ? "True" : "False";
}
return Players.Update(player);
}
public static int ForceRRPlusForAllPlayers()
{
Console.WriteLine(
"[RR+ MEMBERSHIP] ForceRRPlusForAllPlayers called but is disabled - no players were updated.");
return 0;
}
public static bool ChangeAccountId(
long currentAccountId,
long newAccountId)
{
if (currentAccountId <= 0 ||
newAccountId <= 0 ||
currentAccountId == newAccountId)
{
return false;
}
if (Players.FindById(newAccountId) != null)
return false;
var player = Players.FindById(currentAccountId);
if (player == null)
return false;
Players.Delete(currentAccountId);
player.PlayerId = newAccountId;
Players.Insert(player);
return true;
}
public static bool GetLogins(
Platforms platform,
ulong platformId,
out List<CachedLogins> accounts)
{
var results = Players
.Find(x => x.PlatformIds
.Select(p => p.PlatformId)
.Any(id => id == platformId))
.Where(p => p.PlatformIds.Any(
pid => pid.Platform == platform))
.OrderByDescending(x => x.Player.LastLoginAt)
.ToList();
accounts = results
.Select(p => new CachedLogins
{
accountId = p.PlayerId,
lastLoginTime = p.Player.LastLoginAt,
platform = platform,
platformId = platformId.ToString(),
requirePassword = false
})
.ToList();
return accounts.Count > 0;
}
private static PlayerDTOBase MapToDTO(
FullPlayer player,
bool accountMe)
{
int platformFlags = player.PlatformIds?.Aggregate(
0,
(acc, pid) => acc | (int)pid.Platform) ?? 0;
var p = player.Player ?? new Player();
PlayerDTOBase dto = accountMe
? new PlayerMeDTO()
: new PlayerDTO();
dto.accountId = player.PlayerId;
dto.username = p.Username;
dto.displayEmoji = p.DisplayEmoji ?? "";
dto.displayName = GetClientDisplayName(p, player.PlayerId);
dto.profileImage = p.ProfileImage;
dto.bannerImage = p.BannerImage;
dto.isJunior = p.IsJunior;
dto.createdAt = p.CreatedAt;
dto.platforms = platformFlags;
dto.personalPronouns = p.PersonalPronouns;
dto.identityFlags = p.IdentityFlags;
if (accountMe && dto is PlayerMeDTO meDto)
{
meDto.availableUsernameChanges =
p.AvailableUsernameChanges;
meDto.birthday = p.Birthday;
meDto.email = p.Email;
meDto.phone = null;
}
return dto;
}
private static string GetClientDisplayName(Player player, long playerId)
{
string displayName = player.DisplayName?.Trim() ?? string.Empty;
if (!string.IsNullOrWhiteSpace(displayName) &&
!IsPlaceholderDisplayName(displayName))
{
return displayName;
}
string username = player.Username?.Trim().TrimStart('@') ??
string.Empty;
return string.IsNullOrWhiteSpace(username)
? $"Player{playerId}"
: username;
}
public static List<PlayerDTOBase> GetAccountsBulk(
List<long> playerIds,
long? callerId = null)
{
var players = Players
.Find(x => playerIds.Contains(x.PlayerId))
.ToList();
var results = players
.Select(p => MapToDTO(
p,
callerId.HasValue &&
p.PlayerId == callerId.Value))
.ToList();
if (playerIds.Contains(1) &&
!results.Any(account => account.accountId == 1))
{
results.Add(BuildCoachSystemAccount());
}
return results
.OrderBy(a => a.accountId)
.ToList();
}
public static PlayerDTOBase BuildCoachSystemAccount() => new PlayerDTO
{
accountId = 1,
username = "Coach",
displayName = "Coach",
profileImage = "DefaultPFP.png",
bannerImage = null,
displayEmoji = string.Empty,
isJunior = false,
platforms = 0,
personalPronouns = 0,
identityFlags = 0,
createdAt = new DateTime(2016, 1, 1, 0, 0, 0, DateTimeKind.Utc)
};
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 UpdateUsername(
long playerId,
string username,
bool initialAccountSetup = false)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
username = username.Trim().TrimStart('@');
if (username.Length is < 3 or > 20 ||
username.Any(ch => !char.IsLetterOrDigit(ch) && ch != '_'))
{
return false;
}
if (Players.FindAll().Any(candidate =>
candidate.PlayerId != playerId &&
string.Equals(
candidate.Player?.Username,
username,
StringComparison.OrdinalIgnoreCase)))
{
return false;
}
if (!initialAccountSetup && player.Player.AvailableUsernameChanges <= 0)
return false;
player.Player.Username = username;
if (initialAccountSetup)
{
player.Player.DisplayName = username;
SetSettingValue(
player,
"Recroom.AccountCreation.HasChosenUsername",
"True");
}
else
{
player.Player.AvailableUsernameChanges--;
}
return Players.Update(player);
}
public static bool SetPassword(
long playerId,
string newPassword,
string? oldPassword = null)
{
var player = Players.FindById(playerId);
if (player?.Player == null || string.IsNullOrEmpty(newPassword) ||
newPassword.Length < PasswordSecurity.MinPasswordLength ||
newPassword.Length > PasswordSecurity.MaxPasswordLength)
return false;
if (!string.IsNullOrEmpty(player.Password) &&
!PasswordSecurity.Verify(oldPassword, player.Password, out _))
{
return false;
}
player.Password = PasswordSecurity.Hash(newPassword);
SetSettingValue(
player,
"Recroom.AccountCreation.HasCreatedPassword",
"True");
return Players.Update(player);
}
public static bool HasPassword(long playerId)
{
var player = Players.FindById(playerId);
return !string.IsNullOrEmpty(player?.Password);
}
public static bool UpdateBirthday(long playerId, DateTime birthday)
{
var player = Players.FindById(playerId);
if (player?.Player == null || birthday.Date > DateTime.UtcNow.Date)
return false;
player.Player.Birthday = birthday.Date;
var today = DateTime.UtcNow.Date;
int age = today.Year - birthday.Year;
if (birthday.Date > today.AddYears(-age))
age--;
player.Player.IsJunior = age < 13;
return Players.Update(player);
}
private static void SetSettingValue(
FullPlayer player,
string key,
string value)
{
if (player.Player == null)
return;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Settings ??= new List<Setting>();
var setting = player.Player.PlayerExtra.Settings.FirstOrDefault(item =>
string.Equals(item.Key, key, StringComparison.OrdinalIgnoreCase));
if (setting == null)
{
player.Player.PlayerExtra.Settings.Add(new Setting
{
Key = key,
Value = value
});
}
else
{
setting.Value = value;
}
}
public static bool UpdateDisplayName(
long playerId,
string displayName)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
displayName = displayName?.Trim() ?? string.Empty;
player.Player.DisplayName = string.IsNullOrWhiteSpace(displayName) ||
IsPlaceholderDisplayName(displayName)
? GetClientDisplayName(player.Player, playerId)
: displayName;
return Players.Update(player);
}
public static bool UpdatePersonalPronouns(long playerId, int personalPronouns)
{
if (personalPronouns < 0 || (personalPronouns & ~0x3F) != 0)
return false;
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
player.Player.PersonalPronouns = personalPronouns;
return Players.Update(player);
}
public static bool UpdateIdentityFlags(
long playerId,
int identityFlags)
{
if (identityFlags < 0)
return false;
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
player.Player.IdentityFlags = identityFlags;
return Players.Update(player);
}
public static IReadOnlyList<int> GetWishlist(long accountId)
{
var player = Players.FindById(accountId);
string? stored = player?.Player?.PlayerExtra?.Settings?
.FirstOrDefault(setting => string.Equals(
setting.Key,
"Store.Wishlist",
StringComparison.OrdinalIgnoreCase))?.Value;
if (string.IsNullOrWhiteSpace(stored))
return Array.Empty<int>();
return stored.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(value => int.TryParse(value, out int id) ? id : 0)
.Where(id => id > 0)
.Distinct()
.Take(500)
.ToList();
}
public static bool SetWishlistItem(long accountId, int purchasableItemId, bool wished)
{
if (purchasableItemId <= 0)
return false;
var player = Players.FindById(accountId);
if (player?.Player == null)
return false;
var ids = GetWishlist(accountId).ToHashSet();
if (wished)
{
if (ids.Count >= 500 && !ids.Contains(purchasableItemId))
return false;
ids.Add(purchasableItemId);
}
else
{
ids.Remove(purchasableItemId);
}
SetSettingValue(
player,
"Store.Wishlist",
string.Join(',', ids.OrderBy(value => value)));
return Players.Update(player);
}
public static bool SetAvatar(
long accountId,
Avatar avatar)
{
var player = Players.FindById(accountId);
if (player?.Player == null)
return false;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Avatar = avatar;
return Players.Update(player);
}
public static void SetPlayerSetting(
string key,
string value,
long playerId)
{
key = key?.Trim() ?? string.Empty;
value ??= string.Empty;
if (string.IsNullOrWhiteSpace(key) || key.Length > 128 ||
value.Length > 4_096 || key.Any(char.IsControl) ||
value.Any(ch => ch == '\0') || playerId <= 0)
return;
if (key is
"SplitTestAssignedSegments" or
"Growth.LastEmailPromptTime")
{
return;
}
var player = Players.FindById(playerId);
if (player?.Player == null)
return;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Settings ??= new List<Setting>();
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
});
}
if (string.Equals(
key,
"HAS_COMPLETED_ORIENTATION",
StringComparison.OrdinalIgnoreCase) &&
bool.TryParse(value, out bool completedOrientation) &&
completedOrientation)
{
SetSettingValue(
player,
"Recroom.AccountCreation.HasFinished",
"True");
SetSettingValue(player, "TUTORIAL_COMPLETE_MASK", "57");
}
Players.Update(player);
}
public static bool NeedsOrientation(FullPlayer? player)
{
var settings = player?.Player?.PlayerExtra?.Settings;
if (settings == null)
return false;
var orientation = settings.FirstOrDefault(setting =>
string.Equals(
setting.Key,
"HAS_COMPLETED_ORIENTATION",
StringComparison.OrdinalIgnoreCase));
if (orientation != null)
{
return !bool.TryParse(orientation.Value, out bool completed) ||
!completed;
}
var accountCreation = settings.FirstOrDefault(setting =>
string.Equals(
setting.Key,
"Recroom.AccountCreation.HasFinished",
StringComparison.OrdinalIgnoreCase));
return accountCreation != null &&
!string.Equals(
accountCreation.Value,
"True",
StringComparison.OrdinalIgnoreCase);
}
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 PlayerProgressionDTO? SetProgression(
long playerId,
int level,
int xp)
{
if (playerId <= 0)
return null;
int oldLevel;
PlayerProgressionDTO result;
lock (ProgressionLock)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return null;
oldLevel = player.Player.Level;
player.Player.Level = Math.Clamp(level, 1, 50);
player.Player.XP = Math.Max(0, xp);
if (!Players.Update(player))
return null;
result = new PlayerProgressionDTO
{
PlayerId = playerId,
Level = player.Player.Level,
XP = player.Player.XP
};
}
_ = NotiController.NotifyProgressionAsync(
playerId,
oldLevel,
result.Level,
result.XP);
if (result.Level > oldLevel)
GrantLevelUpBox(playerId, oldLevel, result.Level);
return result;
}
public static PlayerProgressionDTO? AddExperience(
long playerId,
int xpToAdd,
int? resultingLevel = null)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return null;
int level = resultingLevel ?? player.Player.Level;
int xp = Math.Max(0, player.Player.XP + xpToAdd);
return SetProgression(playerId, level, xp);
}
public static List<GiftPackage> GetPendingGiftPackages(long playerId)
{
if (playerId <= 0)
return new List<GiftPackage>();
lock (GiftLock)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return new List<GiftPackage>();
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.PendingGiftPackages ??=
new List<GiftPackage>();
bool repaired = RepairPendingGiftIdsNoLock(
player.Player.PlayerExtra.PendingGiftPackages);
if (repaired)
Players.Update(player);
return player.Player.PlayerExtra.PendingGiftPackages
.OrderBy(gift => gift.GiftPackageId)
.ToList();
}
}
public static ClearOutgoingGiftsResult ClearPendingGiftsFromSender(
long fromPlayerId)
{
var result = new ClearOutgoingGiftsResult();
if (fromPlayerId <= 0)
return result;
lock (GiftLock)
{
foreach (var account in Players.FindAll())
{
if (account?.Player?.PlayerExtra?.PendingGiftPackages == null ||
account.Player.PlayerExtra.PendingGiftPackages.Count == 0)
continue;
int removed = account.Player.PlayerExtra.PendingGiftPackages
.RemoveAll(gift => gift.FromPlayerId == fromPlayerId);
if (removed <= 0)
continue;
if (Players.Update(account))
{
result.RemovedBoxes += removed;
result.AffectedPlayers++;
}
}
}
return result;
}
public class ClearOutgoingGiftsResult
{
public int RemovedBoxes { get; set; }
public int AffectedPlayers { get; set; }
}
public static bool RemoveGiftPackage(long playerId, long giftPackageId)
{
if (playerId <= 0 || giftPackageId <= 0)
return false;
lock (GiftLock)
{
var player = Players.FindById(playerId);
var pending = player?.Player?.PlayerExtra?.PendingGiftPackages;
if (pending == null)
return false;
int removed = pending.RemoveAll(gift => gift.GiftPackageId == giftPackageId);
return removed > 0 && Players.Update(player!);
}
}
public static int ClearPendingGiftPackages(long playerId)
{
if (playerId <= 0)
return 0;
lock (GiftLock)
{
var player = Players.FindById(playerId);
var pending = player?.Player?.PlayerExtra?.PendingGiftPackages;
if (pending == null || pending.Count == 0)
return 0;
int removed = pending.Count;
pending.Clear();
return Players.Update(player!) ? removed : 0;
}
}
public static GiftPackage? QueueGiftPackage(
long playerId,
GiftPackage gift)
{
if (playerId <= 0 || gift == null)
return null;
lock (GiftLock)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return null;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.PendingGiftPackages ??=
new List<GiftPackage>();
if (player.Player.PlayerExtra.PendingGiftPackages.Count >=
MaximumPendingGiftPackages)
{
Console.WriteLine(
$"[GIFT QUEUE] recipient={playerId} rejected=queue_full " +
$"limit={MaximumPendingGiftPackages}");
return null;
}
if (gift.GiftPackageId <= 0 ||
gift.GiftPackageId > int.MaxValue ||
player.Player.PlayerExtra.PendingGiftPackages.Any(value =>
value.GiftPackageId == gift.GiftPackageId))
{
gift.GiftPackageId = NextGiftPackageIdNoLock(
player.Player.PlayerExtra.PendingGiftPackages);
}
gift.PlayerId = checked((int)playerId);
player.Player.PlayerExtra.PendingGiftPackages.Add(gift);
return Players.Update(player) ? gift : null;
}
}
public enum FriendotronGiftStatus
{
Success,
InvalidRequest,
SenderNotFound,
RecipientNotFound,
DailyLimitReached,
RecipientQueueFull,
PersistenceFailed
}
public static FriendotronGiftStatus QueueFriendotronGift(
long senderPlayerId,
long recipientPlayerId,
GiftPackage gift,
out GiftPackage? queuedGift,
out DateTime nextAvailableAtUtc)
{
queuedGift = null;
nextAvailableAtUtc = DateTime.UtcNow;
if (senderPlayerId is <= 0 or > int.MaxValue ||
recipientPlayerId is <= 0 or > int.MaxValue ||
gift == null)
{
return FriendotronGiftStatus.InvalidRequest;
}
lock (GiftLock)
{
FullPlayer? sender = Players.FindById(senderPlayerId);
if (sender?.Player == null)
return FriendotronGiftStatus.SenderNotFound;
FullPlayer? recipient = senderPlayerId == recipientPlayerId
? sender
: Players.FindById(recipientPlayerId);
if (recipient?.Player == null)
return FriendotronGiftStatus.RecipientNotFound;
sender.Player.PlayerExtra ??= new PlayerExtra();
recipient.Player.PlayerExtra ??= new PlayerExtra();
recipient.Player.PlayerExtra.PendingGiftPackages ??=
new List<GiftPackage>();
long utcDay = DateTimeOffset.UtcNow.ToUnixTimeSeconds() /
(24L * 60L * 60L);
nextAvailableAtUtc = DateTimeOffset.FromUnixTimeSeconds(
checked((utcDay + 1) * 24L * 60L * 60L))
.UtcDateTime;
if (sender.Player.PlayerExtra.FriendotronLastSentUtcDay >=
utcDay)
{
return FriendotronGiftStatus.DailyLimitReached;
}
if (recipient.Player.PlayerExtra.PendingGiftPackages.Count >=
MaximumPendingGiftPackages)
{
return FriendotronGiftStatus.RecipientQueueFull;
}
if (gift.GiftPackageId <= 0 ||
gift.GiftPackageId > int.MaxValue ||
recipient.Player.PlayerExtra.PendingGiftPackages.Any(value =>
value.GiftPackageId == gift.GiftPackageId))
{
gift.GiftPackageId = NextGiftPackageIdNoLock(
recipient.Player.PlayerExtra.PendingGiftPackages);
}
gift.FromPlayerId = checked((int)senderPlayerId);
gift.PlayerId = checked((int)recipientPlayerId);
recipient.Player.PlayerExtra.PendingGiftPackages.Add(gift);
long previousSentUtcDay =
sender.Player.PlayerExtra.FriendotronLastSentUtcDay;
sender.Player.PlayerExtra.FriendotronLastSentUtcDay = utcDay;
int expectedUpdates = senderPlayerId == recipientPlayerId
? 1
: 2;
int updated = expectedUpdates == 1
? (Players.Update(sender) ? 1 : 0)
: Players.Update(new[] { sender, recipient });
if (updated != expectedUpdates)
{
recipient.Player.PlayerExtra.PendingGiftPackages.Remove(gift);
sender.Player.PlayerExtra.FriendotronLastSentUtcDay =
previousSentUtcDay;
return FriendotronGiftStatus.PersistenceFailed;
}
queuedGift = gift;
return FriendotronGiftStatus.Success;
}
}
private static bool RepairPendingGiftIdsNoLock(
List<GiftPackage> gifts)
{
bool changed = false;
var used = new HashSet<long>();
foreach (GiftPackage gift in gifts)
{
if (gift.GiftPackageId > 0 &&
gift.GiftPackageId <= int.MaxValue &&
used.Add(gift.GiftPackageId))
{
GiftSequence = Math.Max(GiftSequence, gift.GiftPackageId);
continue;
}
gift.GiftPackageId = NextGiftPackageIdNoLock(gifts, used);
used.Add(gift.GiftPackageId);
changed = true;
}
return changed;
}
private static long NextGiftPackageIdNoLock(
IEnumerable<GiftPackage> gifts,
HashSet<long>? reserved = null)
{
var used = reserved ?? gifts
.Where(value => value.GiftPackageId > 0 &&
value.GiftPackageId <= int.MaxValue)
.Select(value => value.GiftPackageId)
.ToHashSet();
for (int attempt = 0; attempt < 100000; attempt++)
{
GiftSequence++;
if (GiftSequence <= 0 || GiftSequence > int.MaxValue)
GiftSequence = 100000;
if (!used.Contains(GiftSequence))
return GiftSequence;
}
throw new InvalidOperationException(
"Unable to allocate a client-safe gift package ID.");
}
public static GiftPackage? ConsumeGiftPackage(
long playerId,
long giftPackageId)
{
if (playerId <= 0 || giftPackageId <= 0)
return null;
lock (GiftLock)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return null;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.PendingGiftPackages ??=
new List<GiftPackage>();
player.Player.PlayerExtra.AvatarItems ??= new List<string>();
GiftPackage? gift =
player.Player.PlayerExtra.PendingGiftPackages.FirstOrDefault(
value => value.GiftPackageId == giftPackageId);
if (gift == null)
return null;
if (!string.IsNullOrWhiteSpace(gift.AvatarItemDesc) &&
!player.Player.PlayerExtra.AvatarItems.Contains(
gift.AvatarItemDesc,
StringComparer.OrdinalIgnoreCase))
{
player.Player.PlayerExtra.AvatarItems.Add(
gift.AvatarItemDesc);
PlayerInventoryStore.SetAvatarItemOwned(
playerId,
gift.AvatarItemDesc,
avatarItemId: 0,
friendlyName: null,
owned: true,
legacyAvatarDescriptors: player.Player.PlayerExtra.AvatarItems);
}
if (!string.IsNullOrWhiteSpace(gift.ConsumableItemDesc))
{
PlayerInventoryStore.AddConsumable(
playerId,
gift.ConsumableItemDesc,
consumableItemId: 0,
friendlyName: null,
amount: Math.Clamp(gift.ConsumableQuantity, 1, 100000));
}
if (!string.IsNullOrWhiteSpace(gift.EquipmentPrefabName) &&
!string.IsNullOrWhiteSpace(gift.EquipmentModificationGuid))
{
EquipmentInventoryStore.AddOwnedItem(
playerId,
gift.EquipmentPrefabName,
gift.EquipmentModificationGuid,
friendlyName: gift.FriendlyName,
tooltip: gift.Tooltip,
rarity: gift.Rarity > 0 ? gift.Rarity : null,
thumbnailImage: gift.ThumbnailImage);
}
if (gift.Currency != 0)
{
player.Player.PlayerExtra.Currencies ??=
new List<PlayerCurrency>();
CurrencyType currencyType =
Enum.IsDefined(typeof(CurrencyType), gift.CurrencyType)
? (CurrencyType)gift.CurrencyType
: CurrencyType.RecCenterTokens;
PlayerCurrency? currency =
player.Player.PlayerExtra.Currencies.FirstOrDefault(
value => value.CurrencyType == currencyType);
if (currency == null)
{
currency = new PlayerCurrency
{
CurrencyType = currencyType,
BalanceType = BalanceType.NonPurchasedDefault
};
player.Player.PlayerExtra.Currencies.Add(currency);
}
long adjustedBalance =
(long)currency.Balance + gift.Currency;
currency.Balance = (int)Math.Clamp(
adjustedBalance,
(long)int.MinValue,
(long)int.MaxValue);
}
if (gift.XP > 0)
player.Player.XP = Math.Max(0, player.Player.XP + gift.XP);
player.Player.PlayerExtra.PendingGiftPackages.Remove(gift);
return Players.Update(player) ? gift : null;
}
}
public static PartySnapshot? GetPartySnapshot(long playerId)
{
lock (PartyLock)
return GetPartySnapshotNoLock(playerId);
}
public static PartySnapshot? InviteToParty(
long inviterPlayerId,
long targetPlayerId)
{
if (inviterPlayerId <= 0 ||
targetPlayerId <= 0 ||
inviterPlayerId == targetPlayerId)
{
return null;
}
PartySnapshot? party;
lock (PartyLock)
{
var inviter = Players.FindById(inviterPlayerId);
var target = Players.FindById(targetPlayerId);
if (inviter?.Player == null || target?.Player == null)
return null;
party = EnsurePartyNoLock(inviterPlayerId);
if (party == null || party.MemberPlayerIds.Contains(targetPlayerId))
return party;
target.Player.PlayerExtra ??= new PlayerExtra();
target.Player.PlayerExtra.PendingPartyInvites ??= new List<PartyInviteRecord>();
DateTime expiresBefore = DateTime.UtcNow.AddMinutes(-10);
target.Player.PlayerExtra.PendingPartyInvites.RemoveAll(invite =>
invite.CreatedAt < expiresBefore ||
invite.PartyId == party.PartyId ||
invite.InviterPlayerId == inviterPlayerId);
target.Player.PlayerExtra.PendingPartyInvites.Add(new PartyInviteRecord
{
PartyId = party.PartyId,
InviterPlayerId = inviterPlayerId,
CreatedAt = DateTime.UtcNow
});
if (!Players.Update(target))
return null;
}
return party;
}
public static PartySnapshot? AcceptPartyInvite(
long inviteePlayerId,
long inviterPlayerId)
{
if (inviteePlayerId <= 0 || inviterPlayerId <= 0)
return null;
PartySnapshot? party;
List<long> notifyPlayerIds;
lock (PartyLock)
{
var invitee = Players.FindById(inviteePlayerId);
var inviter = Players.FindById(inviterPlayerId);
if (invitee?.Player == null || inviter?.Player == null)
return null;
invitee.Player.PlayerExtra ??= new PlayerExtra();
invitee.Player.PlayerExtra.PendingPartyInvites ??= new List<PartyInviteRecord>();
PartyInviteRecord? invite = invitee.Player.PlayerExtra.PendingPartyInvites
.Where(value => value.CreatedAt >= DateTime.UtcNow.AddMinutes(-10))
.OrderByDescending(value => value.CreatedAt)
.FirstOrDefault(value => value.InviterPlayerId == inviterPlayerId);
party = EnsurePartyNoLock(inviterPlayerId);
if (party == null ||
invite == null ||
invite.PartyId != party.PartyId ||
party.MemberPlayerIds.Count >= 8)
{
return null;
}
RemovePlayerFromPartyNoLock(inviteePlayerId);
party.MemberPlayerIds = party.MemberPlayerIds
.Append(inviteePlayerId)
.Where(value => value > 0)
.Distinct()
.ToList();
invitee.Player.PlayerExtra.PendingPartyInvites.RemoveAll(value =>
value.PartyId == party.PartyId ||
value.InviterPlayerId == inviterPlayerId ||
value.CreatedAt < DateTime.UtcNow.AddMinutes(-10));
Players.Update(invitee);
PersistPartyNoLock(party);
notifyPlayerIds = party.MemberPlayerIds.ToList();
}
_ = NotiController.NotifyPartyUpdatedAsync(
notifyPlayerIds,
CreatePartyNotificationPayload(party));
return party;
}
public static PartySnapshot? LeaveParty(long playerId)
{
if (playerId <= 0)
return null;
PartySnapshot? remainingParty;
List<long> notifyPlayerIds;
lock (PartyLock)
{
PartySnapshot? existing = GetPartySnapshotNoLock(playerId);
if (existing == null)
{
ClearPartyStateNoLock(playerId);
return null;
}
notifyPlayerIds = existing.MemberPlayerIds.ToList();
remainingParty = RemovePlayerFromPartyNoLock(playerId);
}
var payload = remainingParty ?? new PartySnapshot
{
PartyId = 0,
LeaderPlayerId = 0,
MemberPlayerIds = new List<long>()
};
_ = NotiController.NotifyPartyUpdatedAsync(
notifyPlayerIds.Append(playerId),
CreatePartyNotificationPayload(payload));
return remainingParty;
}
public static bool DeclinePartyInvite(
long inviteePlayerId,
long inviterPlayerId)
{
lock (PartyLock)
{
var invitee = Players.FindById(inviteePlayerId);
if (invitee?.Player == null)
return false;
invitee.Player.PlayerExtra ??= new PlayerExtra();
invitee.Player.PlayerExtra.PendingPartyInvites ??= new List<PartyInviteRecord>();
int removed = invitee.Player.PlayerExtra.PendingPartyInvites.RemoveAll(value =>
value.InviterPlayerId == inviterPlayerId ||
value.CreatedAt < DateTime.UtcNow.AddMinutes(-10));
return removed > 0 && Players.Update(invitee);
}
}
public static List<PartyInviteRecord> GetPendingPartyInvites(long playerId)
{
lock (PartyLock)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return new List<PartyInviteRecord>();
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.PendingPartyInvites ??= new List<PartyInviteRecord>();
player.Player.PlayerExtra.PendingPartyInvites.RemoveAll(value =>
value.CreatedAt < DateTime.UtcNow.AddMinutes(-10));
Players.Update(player);
return player.Player.PlayerExtra.PendingPartyInvites
.OrderByDescending(value => value.CreatedAt)
.ToList();
}
}
private static PartySnapshot? EnsurePartyNoLock(long playerId)
{
PartySnapshot? existing = GetPartySnapshotNoLock(playerId);
if (existing != null)
return existing;
var player = Players.FindById(playerId);
if (player?.Player == null)
return null;
long partyId = Interlocked.Increment(ref PartySequence);
if (partyId <= 0)
{
PartySequence = DateTime.UtcNow.Ticks & long.MaxValue;
partyId = Interlocked.Increment(ref PartySequence);
}
var party = new PartySnapshot
{
PartyId = partyId,
LeaderPlayerId = playerId,
MemberPlayerIds = new List<long> { playerId }
};
PersistPartyNoLock(party);
return party;
}
private static PartySnapshot? GetPartySnapshotNoLock(long playerId)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return null;
player.Player.PlayerExtra ??= new PlayerExtra();
PlayerExtra extra = player.Player.PlayerExtra;
extra.PartyMemberPlayerIds ??= new List<long>();
if (extra.PartyId <= 0)
return null;
List<long> members = extra.PartyMemberPlayerIds
.Append(playerId)
.Where(value => value > 0 && Players.FindById(value)?.Player != null)
.Distinct()
.ToList();
if (members.Count == 0)
return null;
long leader = members.Contains(extra.PartyLeaderPlayerId)
? extra.PartyLeaderPlayerId
: members[0];
return new PartySnapshot
{
PartyId = extra.PartyId,
LeaderPlayerId = leader,
MemberPlayerIds = members
};
}
private static PartySnapshot? RemovePlayerFromPartyNoLock(long playerId)
{
PartySnapshot? party = GetPartySnapshotNoLock(playerId);
if (party == null)
{
ClearPartyStateNoLock(playerId);
return null;
}
party.MemberPlayerIds.RemoveAll(value => value == playerId);
ClearPartyStateNoLock(playerId);
if (party.MemberPlayerIds.Count <= 1)
{
foreach (long remainingPlayerId in party.MemberPlayerIds)
ClearPartyStateNoLock(remainingPlayerId);
return null;
}
if (party.LeaderPlayerId == playerId ||
!party.MemberPlayerIds.Contains(party.LeaderPlayerId))
{
party.LeaderPlayerId = party.MemberPlayerIds[0];
}
PersistPartyNoLock(party);
return party;
}
private static void PersistPartyNoLock(PartySnapshot party)
{
party.MemberPlayerIds = party.MemberPlayerIds
.Where(value => value > 0)
.Distinct()
.ToList();
if (!party.MemberPlayerIds.Contains(party.LeaderPlayerId))
party.LeaderPlayerId = party.MemberPlayerIds.FirstOrDefault();
foreach (long memberPlayerId in party.MemberPlayerIds)
{
var member = Players.FindById(memberPlayerId);
if (member?.Player == null)
continue;
member.Player.PlayerExtra ??= new PlayerExtra();
member.Player.PlayerExtra.PartyId = party.PartyId;
member.Player.PlayerExtra.PartyLeaderPlayerId = party.LeaderPlayerId;
member.Player.PlayerExtra.PartyMemberPlayerIds = party.MemberPlayerIds.ToList();
Players.Update(member);
}
}
private static object CreatePartyNotificationPayload(PartySnapshot party) => new
{
partyId = party.PartyId,
leaderPlayerId = party.LeaderPlayerId,
memberPlayerIds = party.MemberPlayerIds.ToArray()
};
private static void ClearPartyStateNoLock(long playerId)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.PartyId = 0;
player.Player.PlayerExtra.PartyLeaderPlayerId = 0;
player.Player.PlayerExtra.PartyMemberPlayerIds = new List<long>();
Players.Update(player);
}
public static List<Reputation> GetReputationBulk(
List<long> playerIds)
{
lock (SocialLock)
{
var allPlayers = Players.FindAll().ToList();
var players = allPlayers
.Where(x => playerIds.Contains(x.PlayerId))
.ToList();
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var result = new List<Reputation>(players.Count);
foreach (FullPlayer p in players)
{
var rep = p.Player?.Reputation ?? new Reputation();
if (p.Player != null)
{
p.Player.Reputation = rep;
if (RefreshCheerCreditsNoLock(rep, p.PlayerId, now))
Players.Update(p);
}
bool isDeveloper = HasRole(
p,
PlayerRoles.Developer);
bool isCommunityTeam = HasRole(
p,
PlayerRoles.CommunityTeam);
bool hasStaffBadgeAccess =
isDeveloper || isCommunityTeam;
result.Add(new Reputation
{
AccountId = p.PlayerId,
IsCheerful =
hasStaffBadgeAccess || rep.IsCheerful,
Noteriety = rep.Noteriety,
SelectedCheer = NormalizeSelectedStaffBadge(
rep.SelectedCheer,
isDeveloper,
isCommunityTeam),
CheerCredit = Math.Clamp(
rep.CheerCredit,
0,
CheerCreditLimit),
CheerGeneral = hasStaffBadgeAccess
? Math.Max(10, rep.CheerGeneral)
: rep.CheerGeneral,
CheerHelpful = hasStaffBadgeAccess
? Math.Max(10, rep.CheerHelpful)
: rep.CheerHelpful,
CheerCreative = hasStaffBadgeAccess
? Math.Max(10, rep.CheerCreative)
: rep.CheerCreative,
CheerGreatHost = hasStaffBadgeAccess
? Math.Max(10, rep.CheerGreatHost)
: rep.CheerGreatHost,
CheerSportsman = hasStaffBadgeAccess
? Math.Max(10, rep.CheerSportsman)
: rep.CheerSportsman,
SubscriberCount =
RelationshipDB.GetSubscriberCount(p.PlayerId),
SubscribedCount =
RelationshipDB.GetSubscribedCount(p.PlayerId)
});
}
return result;
}
}
public static bool GrantDeveloperCheerAccess(
FullPlayer account,
bool selectDeveloperBadge)
{
return GrantStaffCheerAccess(
account,
CheerCategory.RecRoomDeveloper,
selectDeveloperBadge);
}
public static bool GrantCommunityTeamCheerAccess(
FullPlayer account,
bool selectCommunityTeamBadge)
{
return GrantStaffCheerAccess(
account,
CheerCategory.RecRoomCommunityTeam,
selectCommunityTeamBadge);
}
private static bool GrantStaffCheerAccess(
FullPlayer account,
CheerCategory staffBadge,
bool selectStaffBadge)
{
if (account.Player == null ||
staffBadge is not (
CheerCategory.RecRoomDeveloper or
CheerCategory.RecRoomCommunityTeam))
{
return false;
}
account.Player.Reputation ??= new Reputation();
Reputation reputation = account.Player.Reputation;
bool changed = false;
if (!reputation.IsCheerful)
{
reputation.IsCheerful = true;
changed = true;
}
if (reputation.CheerGeneral < 10)
{
reputation.CheerGeneral = 10;
changed = true;
}
if (reputation.CheerHelpful < 10)
{
reputation.CheerHelpful = 10;
changed = true;
}
if (reputation.CheerCreative < 10)
{
reputation.CheerCreative = 10;
changed = true;
}
if (reputation.CheerGreatHost < 10)
{
reputation.CheerGreatHost = 10;
changed = true;
}
if (reputation.CheerSportsman < 10)
{
reputation.CheerSportsman = 10;
changed = true;
}
if (selectStaffBadge &&
reputation.SelectedCheer != staffBadge)
{
reputation.SelectedCheer = staffBadge;
changed = true;
}
return changed;
}
private static bool HasRole(
FullPlayer account,
PlayerRoles role)
{
return account.PlayerRoles?.Contains(role) == true;
}
private static CheerCategory NormalizeSelectedStaffBadge(
CheerCategory selectedCheer,
bool isDeveloper,
bool isCommunityTeam)
{
if (selectedCheer == CheerCategory.RecRoomDeveloper &&
!isDeveloper)
{
return CheerCategory.General;
}
if (selectedCheer ==
CheerCategory.RecRoomCommunityTeam &&
!isCommunityTeam)
{
return CheerCategory.General;
}
return selectedCheer;
}
public static int EnsureSocialDefaultsForAllPlayers()
{
lock (SocialLock)
{
var allPlayers = Players.FindAll().ToList();
int updated = 0;
updated += RelationshipDB.MigrateLegacyData(allPlayers);
foreach (var account in allPlayers)
{
if (account.Player == null)
continue;
bool changed = false;
account.Player.Relationships ??= new List<PlayerRelationship>();
account.Player.ReceivedCheers ??= new List<PlayerCheerRecord>();
account.Player.SubscribedAccountIds ??= new List<long>();
account.Player.Reputation ??= new Reputation();
if (account.Player.Reputation.AccountId != account.PlayerId)
{
account.Player.Reputation.AccountId = account.PlayerId;
changed = true;
}
changed |= RefreshCheerCreditsNoLock(
account.Player.Reputation,
account.PlayerId,
DateTimeOffset.UtcNow.ToUnixTimeSeconds());
bool isDeveloper = HasRole(
account,
PlayerRoles.Developer);
bool isCommunityTeam = HasRole(
account,
PlayerRoles.CommunityTeam);
Reputation reputation =
account.Player.Reputation;
bool hasNoEarnedCheer =
reputation.CheerGeneral == 0 &&
reputation.CheerHelpful == 0 &&
reputation.CheerCreative == 0 &&
reputation.CheerGreatHost == 0 &&
reputation.CheerSportsman == 0;
if (isDeveloper)
{
changed |= GrantDeveloperCheerAccess(
account,
selectDeveloperBadge:
hasNoEarnedCheer &&
reputation.SelectedCheer ==
CheerCategory.General);
}
else if (isCommunityTeam)
{
changed |= GrantCommunityTeamCheerAccess(
account,
selectCommunityTeamBadge:
hasNoEarnedCheer &&
reputation.SelectedCheer ==
CheerCategory.General);
}
CheerCategory normalizedSelectedCheer =
NormalizeSelectedStaffBadge(
reputation.SelectedCheer,
isDeveloper,
isCommunityTeam);
if (normalizedSelectedCheer !=
reputation.SelectedCheer)
{
reputation.SelectedCheer =
normalizedSelectedCheer;
changed = true;
}
List<PlayerRelationship> authoritativeRelationships =
RelationshipDB.GetRelationships(account.PlayerId);
if (!RelationshipListsMatch(
account.Player.Relationships,
authoritativeRelationships))
{
account.Player.Relationships = authoritativeRelationships;
changed = true;
}
if (changed && Players.Update(account))
updated++;
}
return updated;
}
}
private static bool RelationshipListsMatch(
IReadOnlyCollection<PlayerRelationship> first,
IReadOnlyCollection<PlayerRelationship> second)
{
if (first.Count != second.Count)
return false;
var firstByPlayer = first
.GroupBy(value => value.PlayerID)
.ToDictionary(group => group.Key, group => group.First());
if (firstByPlayer.Count != first.Count)
return false;
foreach (PlayerRelationship expected in second)
{
if (!firstByPlayer.TryGetValue(expected.PlayerID, out PlayerRelationship? actual) ||
actual.Id != expected.Id ||
actual.RelationshipType != expected.RelationshipType ||
actual.Favorited != expected.Favorited ||
actual.Muted != expected.Muted ||
actual.Ignored != expected.Ignored)
{
return false;
}
}
return true;
}
private static PlayerDBClasses.RoomInstance CloneRoomInstance(
PlayerDBClasses.RoomInstance source)
{
return new PlayerDBClasses.RoomInstance
{
encryptVoiceChat = source.encryptVoiceChat,
clubId = source.clubId,
dataBlob = source.dataBlob,
eventId = source.eventId,
isFull = source.isFull,
isInProgress = source.isInProgress,
isPrivate = source.isPrivate,
location = source.location,
maxCapacity = source.maxCapacity,
Name = source.Name,
photonRegion = source.photonRegion,
photonRegionId = source.photonRegionId,
photonRoomId = source.photonRoomId,
roomCode = source.roomCode,
roomId = source.roomId,
roomInstanceId = source.roomInstanceId,
roomInstanceType = source.roomInstanceType,
subRoomId = source.subRoomId,
subRoomDataSaveId = source.subRoomDataSaveId,
createdAt = source.createdAt
};
}
public static int EnsureInitialTokensForAllPlayers()
{
lock (SocialLock)
{
int updated = 0;
foreach (var account in Players.FindAll().ToList())
{
if (account.Player == null)
continue;
account.Player.PlayerExtra ??= new PlayerExtra();
account.Player.PlayerExtra.Settings ??= new List<Setting>();
account.Player.PlayerExtra.Currencies ??=
new List<PlayerCurrency>();
bool alreadyGranted = account.Player.PlayerExtra.Settings
.Any(setting =>
string.Equals(
setting.Key,
InitialTokensSettingKey,
StringComparison.OrdinalIgnoreCase) &&
string.Equals(
setting.Value,
"True",
StringComparison.OrdinalIgnoreCase));
if (alreadyGranted)
continue;
var tokenCurrency = account.Player.PlayerExtra.Currencies
.FirstOrDefault(currency =>
currency.CurrencyType ==
CurrencyType.RecCenterTokens);
if (tokenCurrency == null)
{
tokenCurrency = new PlayerCurrency
{
CurrencyType = CurrencyType.RecCenterTokens,
BalanceType = BalanceType.NonPurchasedDefault,
Balance = InitialTokenBalance
};
account.Player.PlayerExtra.Currencies.Add(tokenCurrency);
}
else if (tokenCurrency.Balance <= 0)
{
tokenCurrency.Balance = InitialTokenBalance;
tokenCurrency.BalanceType =
BalanceType.NonPurchasedDefault;
}
account.Player.PlayerExtra.Settings.Add(new Setting
{
Key = InitialTokensSettingKey,
Value = "True"
});
if (Players.Update(account))
updated++;
}
return updated;
}
}
public static int ResetAlpacaShirtOwnershipForExistingPlayers()
{
lock (SocialLock)
{
int ownershipsRemoved = 0;
foreach (var account in Players.FindAll().ToList())
{
if (account.Player == null)
continue;
account.Player.PlayerExtra ??= new PlayerExtra();
account.Player.PlayerExtra.Settings ??= new List<Setting>();
account.Player.PlayerExtra.AvatarItems ??= new List<string>();
bool resetAlreadyApplied = account.Player.PlayerExtra.Settings
.Any(setting =>
string.Equals(
setting.Key,
AlpacaShirtOwnershipResetSettingKey,
StringComparison.OrdinalIgnoreCase) &&
string.Equals(
setting.Value,
"True",
StringComparison.OrdinalIgnoreCase));
if (resetAlreadyApplied)
continue;
int removedForAccount = account.Player.PlayerExtra.AvatarItems
.RemoveAll(descriptor => string.Equals(
descriptor,
AlpacaShirtAvatarItemDescriptor,
StringComparison.OrdinalIgnoreCase));
account.Player.PlayerExtra.Settings.Add(new Setting
{
Key = AlpacaShirtOwnershipResetSettingKey,
Value = "True"
});
if (Players.Update(account))
ownershipsRemoved += removedForAccount;
}
return ownershipsRemoved;
}
}
public static bool OwnsAvatarItem(
long playerId,
string avatarItemDescriptor)
{
if (string.IsNullOrWhiteSpace(avatarItemDescriptor))
return false;
lock (SocialLock)
{
var legacyItems = Players.FindById(playerId)?
.Player?.PlayerExtra?.AvatarItems;
return PlayerInventoryStore.OwnsAvatarItem(
playerId,
avatarItemDescriptor,
legacyAvatarDescriptors: legacyItems);
}
}
public static bool RemoveAvatarItem(
long playerId,
string avatarItemDescriptor,
long avatarItemId = 0)
{
if (playerId <= 0 ||
(string.IsNullOrWhiteSpace(avatarItemDescriptor) && avatarItemId <= 0))
{
return false;
}
lock (SocialLock)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.AvatarItems ??= new List<string>();
if (!string.IsNullOrWhiteSpace(avatarItemDescriptor))
{
player.Player.PlayerExtra.AvatarItems.RemoveAll(value =>
string.Equals(
value,
avatarItemDescriptor,
StringComparison.OrdinalIgnoreCase));
}
bool saved = PlayerInventoryStore.SetAvatarItemOwned(
playerId,
avatarItemDescriptor,
avatarItemId,
friendlyName: null,
owned: false,
legacyAvatarDescriptors: player.Player.PlayerExtra.AvatarItems);
return saved && Players.Update(player);
}
}
public static List<PlayerRelationship> GetRelationships(long playerId)
{
return Players.FindById(playerId)?.Player == null
? new List<PlayerRelationship>()
: RelationshipDB.GetRelationships(playerId);
}
public static PlayerRelationship? SetRelationshipFlags(
long sourcePlayerId,
long targetPlayerId,
bool? ignored = null,
bool? muted = null,
bool? favorited = null)
{
if (!ValidateRelationshipPlayers(sourcePlayerId, targetPlayerId))
return null;
PlayerRelationship relationship;
lock (SocialLock)
{
relationship = RelationshipDB.SetFlags(
sourcePlayerId,
targetPlayerId,
ignored: ignored,
muted: muted,
favorited: favorited);
if (!SynchronizeLegacyRelationshipNoLock(sourcePlayerId, targetPlayerId))
return null;
}
return relationship;
}
private static bool ApplyBanStateToHeartbeat(
FullPlayer player,
long playerId,
out Heartbeat heartbeat)
{
player.Player ??= new Player();
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Heartbeat ??= new Heartbeat();
heartbeat = player.Player.PlayerExtra.Heartbeat;
heartbeat.playerId = playerId;
var moderation =
player.Player.PlayerExtra.ModerationBlockDetails;
if (moderation?.IsBan != true)
{
if (heartbeat.errorCode ==
MatchmakingErrorCode.Banned)
{
heartbeat.errorCode =
MatchmakingErrorCode.Success;
}
return false;
}
long now = DateTimeOffset.UtcNow
.ToUnixTimeSeconds();
bool permanent = moderation.Duration <= 0;
bool active =
permanent ||
moderation.ModerationSetUnixTime +
moderation.Duration > now;
if (!active)
{
moderation.IsBan = false;
moderation.Duration = 0;
moderation.Message = "";
moderation.ModerationSetUnixTime = 0;
moderation.BannedByPlayerId = 0;
if (heartbeat.errorCode ==
MatchmakingErrorCode.Banned)
{
heartbeat.errorCode =
MatchmakingErrorCode.Success;
}
return false;
}
heartbeat.isOnline = false;
heartbeat.roomInstance = null;
heartbeat.errorCode =
MatchmakingErrorCode.Banned;
heartbeat.lastHeartbeatUnixTime =
now;
return true;
}
public static PlayerRelationship? SendFriendRequest(
long sourcePlayerId,
long targetPlayerId)
{
if (!ValidateRelationshipPlayers(sourcePlayerId, targetPlayerId))
return null;
PlayerRelationship? result;
lock (SocialLock)
{
PlayerRelationship? existing = RelationshipDB.GetRelationship(
sourcePlayerId,
targetPlayerId);
if (existing?.RelationshipType is RelationshipType.OutgoingFriendRequest
or RelationshipType.Friend)
{
result = existing;
}
else
{
result = RelationshipDB.SendFriendRequest(
sourcePlayerId,
targetPlayerId,
out _);
}
if (!SynchronizeLegacyRelationshipNoLock(sourcePlayerId, targetPlayerId))
return null;
}
return result;
}
public static PlayerRelationship? AcceptFriendRequest(
long receiverPlayerId,
long requesterPlayerId)
{
if (!ValidateRelationshipPlayers(receiverPlayerId, requesterPlayerId))
return null;
PlayerRelationship? result;
lock (SocialLock)
{
result = RelationshipDB.AcceptFriendRequest(
receiverPlayerId,
requesterPlayerId);
if (result == null)
return null;
if (!SynchronizeLegacyRelationshipNoLock(receiverPlayerId, requesterPlayerId))
return null;
}
return result;
}
public static PlayerRelationship? AddFriendRequest(
long sourcePlayerId,
long targetPlayerId) =>
SendFriendRequest(sourcePlayerId, targetPlayerId);
private static bool ValidateRelationshipPlayers(
long firstPlayerId,
long secondPlayerId)
{
if (firstPlayerId <= 0 ||
secondPlayerId <= 0 ||
firstPlayerId == secondPlayerId)
{
return false;
}
return Players.FindById(firstPlayerId)?.Player != null &&
Players.FindById(secondPlayerId)?.Player != null;
}
private static bool SynchronizeLegacyRelationshipNoLock(
long firstPlayerId,
long secondPlayerId)
{
FullPlayer? first = Players.FindById(firstPlayerId);
FullPlayer? second = Players.FindById(secondPlayerId);
if (first?.Player == null || second?.Player == null)
return false;
first.Player.Relationships ??= new List<PlayerRelationship>();
second.Player.Relationships ??= new List<PlayerRelationship>();
SynchronizeOneLegacyRelationship(
first.Player.Relationships,
RelationshipDB.GetRelationship(firstPlayerId, secondPlayerId),
secondPlayerId);
SynchronizeOneLegacyRelationship(
second.Player.Relationships,
RelationshipDB.GetRelationship(secondPlayerId, firstPlayerId),
firstPlayerId);
return Players.Update(first) && Players.Update(second);
}
private static void SynchronizeOneLegacyRelationship(
List<PlayerRelationship> relationships,
PlayerRelationship? authoritative,
long otherPlayerId)
{
relationships.RemoveAll(value => value.PlayerID == otherPlayerId);
if (authoritative != null && authoritative.RelationshipType != RelationshipType.None)
relationships.Add(authoritative);
}
private static long CreateRelationshipId(
long firstPlayerId,
long secondPlayerId) =>
RelationshipDB.GetStableNumericId(firstPlayerId, secondPlayerId);
public static bool SetSubscription(
long subscriberPlayerId,
long targetPlayerId,
bool subscribe)
{
if (subscriberPlayerId == targetPlayerId)
return false;
lock (SocialLock)
{
var subscriber = Players.FindById(subscriberPlayerId);
var target = Players.FindById(targetPlayerId);
if (subscriber?.Player == null || target?.Player == null)
return false;
if (!RelationshipDB.SetSubscription(
subscriberPlayerId,
targetPlayerId,
subscribe))
return false;
subscriber.Player.SubscribedAccountIds ??= new List<long>();
subscriber.Player.Reputation ??= new Reputation();
target.Player.Reputation ??= new Reputation();
if (subscribe)
{
if (!subscriber.Player.SubscribedAccountIds.Contains(targetPlayerId))
subscriber.Player.SubscribedAccountIds.Add(targetPlayerId);
}
else
{
subscriber.Player.SubscribedAccountIds.RemoveAll(value =>
value == targetPlayerId);
}
subscriber.Player.Reputation.SubscribedCount =
RelationshipDB.GetSubscribedCount(subscriberPlayerId);
Players.Update(subscriber);
target.Player.Reputation.SubscriberCount =
RelationshipDB.GetSubscriberCount(targetPlayerId);
Players.Update(target);
return true;
}
}
public static int GetSubscriberCount(long targetPlayerId)
{
return RelationshipDB.GetSubscriberCount(targetPlayerId);
}
public static int GetCurrencyBalance(
long playerId,
CurrencyType currencyType)
{
var player = Players.FindById(playerId);
return player?.Player?.PlayerExtra?.Currencies?
.FirstOrDefault(currency =>
currency.CurrencyType == currencyType)?.Balance ?? 0;
}
public static List<(CurrencyType CurrencyType, int Balance)> GetAllCurrencyBalances(
long playerId)
{
var player = Players.FindById(playerId);
var currencies = player?.Player?.PlayerExtra?.Currencies ?? new List<PlayerCurrency>();
var storedBalances = currencies
.GroupBy(currency => currency.CurrencyType)
.ToDictionary(group => group.Key, group => group.First().Balance);
return Enum.GetValues<CurrencyType>()
.Where(currency => currency != CurrencyType.Invalid)
.Select(currency => (
currency,
storedBalances.TryGetValue(currency, out int balance) ? balance : 0))
.ToList();
}
public static int? SetCurrencyBalance(
long playerId,
CurrencyType currencyType,
int amount,
bool add)
{
lock (SocialLock)
{
var player = 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(value => value.CurrencyType == currencyType);
if (currency == null)
{
currency = new PlayerCurrency
{
CurrencyType = currencyType,
BalanceType = BalanceType.NonPurchasedDefault
};
player.Player.PlayerExtra.Currencies.Add(currency);
}
long requestedBalance = add
? (long)currency.Balance + amount
: amount;
currency.Balance = (int)Math.Clamp(
requestedBalance,
0L,
int.MaxValue);
return Players.Update(player) ? currency.Balance : null;
}
}
public static bool TrySpendCurrency(
long playerId,
CurrencyType currencyType,
int amount,
out int newBalance)
{
newBalance = 0;
if (amount < 0)
return false;
lock (SocialLock)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Currencies ??= new List<PlayerCurrency>();
var currency = player.Player.PlayerExtra.Currencies
.FirstOrDefault(value => value.CurrencyType == currencyType);
newBalance = currency?.Balance ?? 0;
if (amount == 0)
return true;
if (currency == null || currency.Balance < amount)
return false;
int originalBalance = currency.Balance;
currency.Balance -= amount;
newBalance = currency.Balance;
if (Players.Update(player))
return true;
currency.Balance = originalBalance;
newBalance = originalBalance;
return false;
}
}
public static bool TryPurchaseAvatarItem(
long playerId,
string avatarItemDescriptor,
int price,
out int newBalance,
out bool alreadyOwned)
{
newBalance = 0;
alreadyOwned = false;
if (price < 0 || string.IsNullOrWhiteSpace(avatarItemDescriptor))
return false;
lock (SocialLock)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Currencies ??= new List<PlayerCurrency>();
player.Player.PlayerExtra.AvatarItems ??= new List<string>();
var currency = player.Player.PlayerExtra.Currencies
.FirstOrDefault(value =>
value.CurrencyType == CurrencyType.RecCenterTokens);
newBalance = currency?.Balance ?? 0;
alreadyOwned = PlayerInventoryStore.OwnsAvatarItem(
playerId,
avatarItemDescriptor,
legacyAvatarDescriptors: player.Player.PlayerExtra.AvatarItems);
if (alreadyOwned)
return true;
if (currency == null || currency.Balance < price)
return false;
currency.Balance -= price;
player.Player.PlayerExtra.AvatarItems.Add(avatarItemDescriptor);
PlayerInventoryStore.SetAvatarItemOwned(
playerId,
avatarItemDescriptor,
avatarItemId: 0,
friendlyName: null,
owned: true,
legacyAvatarDescriptors: player.Player.PlayerExtra.AvatarItems);
newBalance = currency.Balance;
return Players.Update(player);
}
}
public static bool GrantAvatarItem(
long playerId,
string avatarItemDescriptor)
{
if (playerId <= 0 || string.IsNullOrWhiteSpace(avatarItemDescriptor))
return false;
lock (SocialLock)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.AvatarItems ??= new List<string>();
if (player.Player.PlayerExtra.AvatarItems.Contains(
avatarItemDescriptor,
StringComparer.OrdinalIgnoreCase))
{
PlayerInventoryStore.SetAvatarItemOwned(
playerId,
avatarItemDescriptor,
avatarItemId: 0,
friendlyName: null,
owned: true,
legacyAvatarDescriptors: player.Player.PlayerExtra.AvatarItems);
return true;
}
player.Player.PlayerExtra.AvatarItems.Add(avatarItemDescriptor);
PlayerInventoryStore.SetAvatarItemOwned(
playerId,
avatarItemDescriptor,
avatarItemId: 0,
friendlyName: null,
owned: true,
legacyAvatarDescriptors: player.Player.PlayerExtra.AvatarItems);
return Players.Update(player);
}
}
public static bool IsSubscribed(long subscriberPlayerId, long targetPlayerId)
{
return RelationshipDB.IsSubscribed(subscriberPlayerId, targetPlayerId);
}
public static bool RemoveFriend(long sourcePlayerId, long targetPlayerId)
{
bool removed;
lock (SocialLock)
{
var source = Players.FindById(sourcePlayerId);
var target = Players.FindById(targetPlayerId);
if (source?.Player == null || target?.Player == null)
return false;
source.Player.Relationships ??= new List<PlayerRelationship>();
target.Player.Relationships ??= new List<PlayerRelationship>();
source.Player.Relationships.RemoveAll(value =>
value.PlayerID == targetPlayerId);
target.Player.Relationships.RemoveAll(value =>
value.PlayerID == sourcePlayerId);
Players.Update(source);
Players.Update(target);
removed = RelationshipDB.RemoveFriend(sourcePlayerId, targetPlayerId);
}
return removed;
}
public static PlayerCheerRecord? GivePlayerCheer(
long giverPlayerId,
long receiverPlayerId,
CheerCategory category,
out NotificationDB.ClientNotification? notification,
bool anonymous = false)
{
notification = null;
if (giverPlayerId == receiverPlayerId ||
!IsPublicCheerCategory(category))
{
return null;
}
PlayerCheerRecord? cheer;
lock (SocialLock)
{
var giver = Players.FindById(giverPlayerId);
var receiver = Players.FindById(receiverPlayerId);
if (giver?.Player == null || receiver?.Player == null)
return null;
giver.Player.Reputation ??= new Reputation();
receiver.Player.ReceivedCheers ??= new List<PlayerCheerRecord>();
receiver.Player.Reputation ??= new Reputation();
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
RefreshCheerCreditsNoLock(
giver.Player.Reputation,
giverPlayerId,
now);
if (giver.Player.Reputation.CheerCredit <= 0)
return null;
cheer = new PlayerCheerRecord
{
CheerId = Interlocked.Increment(ref CheerSequence),
GiverPlayerId = giverPlayerId,
ReceiverPlayerId = receiverPlayerId,
CheerCategory = category,
Anonymous = anonymous,
Status = CheerStatus.Pending,
CreatedAt = DateTime.UtcNow
};
notification = NotificationDB.CreatePlayerCheer(
giverPlayerId,
receiverPlayerId,
(int)category,
anonymous);
cheer.NotificationId = notification.Id;
receiver.Player.ReceivedCheers.Add(cheer);
var reputation = receiver.Player.Reputation;
reputation.AccountId = receiverPlayerId;
reputation.IsCheerful = true;
giver.Player.Reputation.AccountId = giverPlayerId;
giver.Player.Reputation.CheerCredit--;
switch (category)
{
case CheerCategory.General:
reputation.CheerGeneral++;
break;
case CheerCategory.Helpful:
reputation.CheerHelpful++;
break;
case CheerCategory.Sportmanship:
reputation.CheerSportsman++;
break;
case CheerCategory.GreatHost:
reputation.CheerGreatHost++;
break;
case CheerCategory.Creative:
reputation.CheerCreative++;
break;
}
if (Players.Update(new[] { giver, receiver }) != 2)
{
NotificationDB.DeleteMessages(
receiverPlayerId,
new[] { notification.Id });
return null;
}
}
return cheer;
}
private static bool RefreshCheerCreditsNoLock(
Reputation reputation,
long accountId,
long nowUnixTime)
{
bool changed = false;
if (reputation.AccountId != accountId)
{
reputation.AccountId = accountId;
changed = true;
}
if (reputation.CheerCreditRefreshAtUnixTime <= 0)
{
reputation.CheerCredit = CheerCreditLimit;
reputation.CheerCreditRefreshAtUnixTime =
nowUnixTime + CheerCreditRefreshSeconds;
return true;
}
if (nowUnixTime >= reputation.CheerCreditRefreshAtUnixTime)
{
long elapsed =
nowUnixTime - reputation.CheerCreditRefreshAtUnixTime;
long completedIntervals =
(elapsed / CheerCreditRefreshSeconds) + 1;
reputation.CheerCredit = CheerCreditLimit;
reputation.CheerCreditRefreshAtUnixTime +=
completedIntervals * CheerCreditRefreshSeconds;
changed = true;
}
int clamped = Math.Clamp(
reputation.CheerCredit,
0,
CheerCreditLimit);
if (clamped != reputation.CheerCredit)
{
reputation.CheerCredit = clamped;
changed = true;
}
return changed;
}
public static int ResolvePlayerCheers(
long receiverPlayerId,
IEnumerable<long> cheerOrNotificationIds,
CheerStatus status)
{
if (receiverPlayerId <= 0 ||
status == CheerStatus.Pending)
{
return 0;
}
HashSet<long> ids = cheerOrNotificationIds
.Where(value => value > 0)
.ToHashSet();
if (ids.Count == 0)
return 0;
lock (SocialLock)
{
var receiver = Players.FindById(receiverPlayerId);
if (receiver?.Player == null)
return 0;
receiver.Player.ReceivedCheers ??=
new List<PlayerCheerRecord>();
List<PlayerCheerRecord> matching =
receiver.Player.ReceivedCheers
.Where(value =>
value.Status == CheerStatus.Pending &&
(ids.Contains(value.CheerId) ||
ids.Contains(value.NotificationId)))
.ToList();
if (matching.Count == 0)
return 0;
DateTime handledAt = DateTime.UtcNow;
foreach (PlayerCheerRecord cheer in matching)
{
cheer.Status = status;
cheer.HandledAt = handledAt;
}
return Players.Update(receiver)
? matching.Count
: 0;
}
}
public static List<PlayerCheerRecord> GetPendingPlayerCheers(
long receiverPlayerId)
{
if (receiverPlayerId <= 0)
return new List<PlayerCheerRecord>();
lock (SocialLock)
{
var receiver = Players.FindById(receiverPlayerId);
return receiver?.Player?.ReceivedCheers?
.Where(value => value.Status == CheerStatus.Pending)
.OrderByDescending(value => value.CreatedAt)
.ToList() ?? new List<PlayerCheerRecord>();
}
}
private static bool IsPublicCheerCategory(
CheerCategory category) =>
category is CheerCategory.General or
CheerCategory.Helpful or
CheerCategory.Sportmanship or
CheerCategory.GreatHost or
CheerCategory.Creative;
public static Heartbeat GetPlayerHeartbeat(
long playerId,
long? viewingPlayerId = null)
{
var player = Players.FindOne(
x => x.PlayerId == playerId);
return CreateClientHeartbeat(
player,
playerId,
DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
viewingPlayerId);
}
public static List<Heartbeat> GetPlayerHeartbeatsBulk(
List<long> playerIds,
long? viewingPlayerId = null)
{
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var players = Players
.Find(x => playerIds.Contains(x.PlayerId))
.ToList();
return players
.Select(p => CreateClientHeartbeat(
p,
p.PlayerId,
now,
viewingPlayerId))
.ToList();
}
public static long[] GetActiveSameInstancePlayerIds(long subjectPlayerId)
{
if (subjectPlayerId <= 0)
return Array.Empty<long>();
Heartbeat? subjectHeartbeat = Players.FindById(subjectPlayerId)?
.Player?.PlayerExtra?.Heartbeat;
long roomInstanceId =
subjectHeartbeat?.roomInstance?.roomInstanceId ?? 0;
if (roomInstanceId <= 0)
return Array.Empty<long>();
long activeAfter = DateTimeOffset.UtcNow.ToUnixTimeSeconds() -
PresenceFreshnessSeconds;
return Players.FindAll()
.Where(account =>
{
if (account.PlayerId == subjectPlayerId)
return false;
Heartbeat? heartbeat =
account.Player?.PlayerExtra?.Heartbeat;
return heartbeat?.isOnline == true &&
heartbeat.lastHeartbeatUnixTime >= activeAfter &&
heartbeat.roomInstance?.roomInstanceId == roomInstanceId;
})
.Select(account => account.PlayerId)
.Distinct()
.ToArray();
}
private static Heartbeat CreateClientHeartbeat(
FullPlayer? player,
long playerId,
long now,
long? viewingPlayerId)
{
Heartbeat stored =
player?.Player?.PlayerExtra?.Heartbeat ??
new Heartbeat();
bool isFresh =
stored.lastHeartbeatUnixTime > 0 &&
stored.lastHeartbeatUnixTime >=
now - PresenceFreshnessSeconds;
bool isOnline = stored.isOnline && isFresh;
RoomInstance? roomInstance = isOnline &&
stored.roomInstance != null
? CloneRoomInstance(stored.roomInstance)
: null;
bool viewerSharesRestrictedInstance = false;
if (roomInstance != null &&
viewingPlayerId.HasValue &&
viewingPlayerId.Value != playerId)
{
Heartbeat? viewerHeartbeat = Players
.FindById(viewingPlayerId.Value)?
.Player?.PlayerExtra?.Heartbeat;
viewerSharesRestrictedInstance =
viewerHeartbeat?.isOnline == true &&
viewerHeartbeat.roomInstance?.roomInstanceId ==
roomInstance.roomInstanceId;
}
if (roomInstance != null &&
viewingPlayerId.HasValue &&
viewingPlayerId.Value != playerId &&
Sessions.IsRestrictedInstance(roomInstance) &&
!viewerSharesRestrictedInstance &&
!NotificationDB.HasActiveRoomInvite(
viewingPlayerId.Value,
playerId,
roomInstance.roomInstanceId))
{
roomInstance = null;
}
if (roomInstance != null &&
roomInstance.roomInstanceType != RoomInstanceType.Dormroom &&
roomInstance.roomId > 0)
{
var room = RoomDB.GetRoom(roomInstance.roomId);
if (room != null && !string.IsNullOrWhiteSpace(room.Name))
roomInstance.Name = $"^{room.Name.Trim().TrimStart('^')}";
}
return new Heartbeat
{
appVersion = stored.appVersion,
deviceClass = stored.deviceClass,
errorCode = stored.errorCode,
isOnline = isOnline,
playerId = playerId,
roomInstance = roomInstance,
statusVisibility = isOnline
? stored.statusVisibility
: StatusVisibility.Offline,
vrMovementMode = stored.vrMovementMode,
lastHeartbeatUnixTime = stored.lastHeartbeatUnixTime
};
}
public static void RecordRoomVisit(
long playerId,
long roomId)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return;
player.Player.VisitedRooms ??= new List<long>();
if (!player.Player.VisitedRooms.Contains(roomId))
player.Player.VisitedRooms.Add(roomId);
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.RoomVisits ??=
new List<RoomVisit>();
var existingVisit =
player.Player.PlayerExtra.RoomVisits
.FirstOrDefault(v => v.RoomId == roomId);
bool isNewVisitor = existingVisit == null;
if (existingVisit != null)
{
existingVisit.VisitedAt = DateTime.UtcNow;
}
else
{
player.Player.PlayerExtra.RoomVisits.Add(
new RoomVisit
{
RoomId = roomId,
VisitedAt = DateTime.UtcNow
});
}
Players.Update(player);
var room = RoomDB.GetRoom(roomId);
if (room != null)
{
room.Stats ??= new RoomDBClasses.Stats();
room.Stats.VisitCount++;
if (isNewVisitor)
room.Stats.VisitorCount++;
RoomDB.Rooms.Update(room);
}
}
public static bool ToggleCheer(
long playerId,
long roomId,
out bool isNowCheered)
{
var player = Players.FindById(playerId);
isNowCheered = false;
if (player?.Player == null)
return false;
player.Player.CheeredRooms ??= new List<long>();
bool wasCheered =
player.Player.CheeredRooms.Contains(roomId);
if (wasCheered)
{
player.Player.CheeredRooms.Remove(roomId);
}
else
{
player.Player.CheeredRooms.Add(roomId);
}
isNowCheered = !wasCheered;
Players.Update(player);
var room = RoomDB.GetRoom(roomId);
if (room != null)
{
room.Stats ??= new RoomDBClasses.Stats();
room.Stats.CheerCount = Math.Max(
0,
room.Stats.CheerCount +
(wasCheered ? -1 : 1));
RoomDB.Rooms.Update(room);
}
return true;
}
public static bool ToggleFavorite(
long playerId,
long roomId,
out bool isNowFavorited)
{
var player = Players.FindById(playerId);
isNowFavorited = false;
if (player?.Player == null)
return false;
player.Player.FavoritedRooms ??= new List<long>();
bool wasFavorited =
player.Player.FavoritedRooms.Contains(roomId);
if (wasFavorited)
{
player.Player.FavoritedRooms.Remove(roomId);
}
else
{
player.Player.FavoritedRooms.Add(roomId);
}
isNowFavorited = !wasFavorited;
Players.Update(player);
var room = RoomDB.GetRoom(roomId);
if (room != null)
{
room.Stats ??= new RoomDBClasses.Stats();
room.Stats.FavoriteCount = Math.Max(
0,
room.Stats.FavoriteCount +
(wasFavorited ? -1 : 1));
RoomDB.Rooms.Update(room);
}
return true;
}
public static (
List<PlayerDTOBase> Results,
int Total)
SearchAccounts(
string query,
int skip,
int take)
{
if (string.IsNullOrWhiteSpace(query))
{
return (
new List<PlayerDTOBase>(),
0
);
}
var q = query
.Trim()
.ToLowerInvariant();
var matches = Players
.FindAll()
.Where(p =>
(p.Player?.Username?
.ToLowerInvariant()
.Contains(q) ?? false)
||
(p.Player?.DisplayName?
.ToLowerInvariant()
.Contains(q) ?? false))
.OrderBy(p => p.Player?.Username)
.ToList();
int total = matches.Count;
var page = matches
.Skip(skip)
.Take(take)
.Select(p => MapToDTO(p, false))
.ToList();
return (page, total);
}
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 ??= new Player();
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Heartbeat ??=
new Heartbeat();
var hb = player.Player.PlayerExtra.Heartbeat;
long? oldRoomInstanceId = hb.roomInstance?.roomInstanceId;
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
hb.playerId = playerId;
hb.roomInstance = online ? roomInstance : null;
hb.errorCode = MatchmakingErrorCode.Success;
hb.isOnline = online;
hb.lastHeartbeatUnixTime = now;
long newRoomInstanceId =
online ? roomInstance?.roomInstanceId ?? 0 : 0;
if (player.Player.PlayerExtra.LevelingRoomInstanceId !=
newRoomInstanceId)
{
ResetRoomLevelTimer(
player.Player.PlayerExtra,
newRoomInstanceId,
newRoomInstanceId > 0 ? now : 0);
}
Players.Update(player);
if (oldRoomInstanceId != hb.roomInstance?.roomInstanceId)
{
Console.WriteLine(
$"[ROOM MEMBERSHIP] player={playerId} " +
$"oldInstance={oldRoomInstanceId?.ToString() ?? "none"} " +
$"newInstance={hb.roomInstance?.roomInstanceId.ToString() ?? "none"} " +
"broadcast=false");
}
return hb;
}
public static Heartbeat? LeaveCurrentRoom(long playerId)
{
var player = Players.FindById(playerId);
if (player == null)
return null;
player.Player ??= new Player();
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Heartbeat ??= new Heartbeat();
var heartbeat = player.Player.PlayerExtra.Heartbeat;
long? oldRoomInstanceId = heartbeat.roomInstance?.roomInstanceId;
heartbeat.playerId = playerId;
heartbeat.isOnline = true;
heartbeat.roomInstance = null;
heartbeat.lastHeartbeatUnixTime =
DateTimeOffset.UtcNow.ToUnixTimeSeconds();
ResetRoomLevelTimer(player.Player.PlayerExtra, 0, 0);
Players.Update(player);
Console.WriteLine(
$"[ROOM LEAVE] player={playerId} " +
$"instance={oldRoomInstanceId?.ToString() ?? "none"} " +
"isolated=true presencePush=controller-awaited");
return heartbeat;
}
public static Heartbeat? TouchPlayerHeartbeat(long playerId)
{
int? oldProgressionLevel = null;
int? newProgressionLevel = null;
int progressionXp = 0;
Heartbeat? result;
lock (ProgressionLock)
{
var player = Players.FindById(playerId);
if (player == null)
return null;
bool oldOnline =
player.Player?.PlayerExtra?.Heartbeat?.isOnline ?? false;
MatchmakingErrorCode? oldErrorCode =
player.Player?.PlayerExtra?.Heartbeat?.errorCode;
if (ApplyBanStateToHeartbeat(
player,
playerId,
out var heartbeat))
{
if (player.Player?.PlayerExtra != null)
ResetRoomLevelTimer(
player.Player.PlayerExtra,
0,
0);
Players.Update(player);
if (oldOnline != heartbeat.isOnline ||
oldErrorCode != heartbeat.errorCode)
{
_ = NotiController.NotifyPlayerPresenceUpdatedAsync(
playerId);
}
return heartbeat;
}
heartbeat.playerId = playerId;
heartbeat.isOnline = true;
heartbeat.errorCode =
MatchmakingErrorCode.Success;
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
heartbeat.lastHeartbeatUnixTime = now;
if (player.Player != null &&
ApplyRoomTimeLeveling(
player.Player,
now,
out int awardedFromLevel,
out int awardedToLevel))
{
oldProgressionLevel = awardedFromLevel;
newProgressionLevel = awardedToLevel;
progressionXp = player.Player.XP;
}
Players.Update(player);
if (oldOnline != heartbeat.isOnline ||
oldErrorCode != heartbeat.errorCode)
{
_ = NotiController.NotifyPlayerPresenceUpdatedAsync(
playerId);
}
result = heartbeat;
}
if (oldProgressionLevel.HasValue &&
newProgressionLevel.HasValue)
{
Console.WriteLine(
$"[ROOM LEVEL] player={playerId} " +
$"level={oldProgressionLevel.Value}->{newProgressionLevel.Value} " +
$"interval={RoomLevelSeconds}s");
GrantLevelUpBox(
playerId,
oldProgressionLevel.Value,
newProgressionLevel.Value);
_ = NotiController.NotifyProgressionAsync(
playerId,
oldProgressionLevel.Value,
newProgressionLevel.Value,
progressionXp);
}
return result;
}
public static Heartbeat? ResumePlayerHeartbeat(long playerId)
{
lock (ProgressionLock)
{
var player = Players.FindById(playerId);
if (player == null)
return null;
player.Player ??= new Player();
player.Player.PlayerExtra ??= new PlayerExtra();
Heartbeat heartbeat = player.Player.PlayerExtra.Heartbeat ??=
new Heartbeat();
if (ApplyBanStateToHeartbeat(
player,
playerId,
out heartbeat))
{
ResetRoomLevelTimer(
player.Player.PlayerExtra,
0,
0);
Players.Update(player);
return heartbeat;
}
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
bool existingPresenceIsFresh =
heartbeat.isOnline &&
heartbeat.lastHeartbeatUnixTime > 0 &&
heartbeat.lastHeartbeatUnixTime >=
now - PresenceFreshnessSeconds;
if (!existingPresenceIsFresh)
{
heartbeat.roomInstance = null;
ResetRoomLevelTimer(
player.Player.PlayerExtra,
0,
0);
}
heartbeat.playerId = playerId;
heartbeat.isOnline = true;
heartbeat.errorCode = MatchmakingErrorCode.Success;
heartbeat.lastHeartbeatUnixTime = now;
Players.Update(player);
return heartbeat;
}
}
public static void SetPlayerOffline(long playerId)
{
var player = Players.FindById(playerId);
var heartbeat = player?.Player?.PlayerExtra?.Heartbeat;
if (player == null || heartbeat == null)
return;
long? preservedRoomInstanceId = heartbeat.roomInstance?.roomInstanceId;
heartbeat.isOnline = false;
heartbeat.lastHeartbeatUnixTime =
DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (player.Player?.PlayerExtra != null)
ResetRoomLevelTimer(player.Player.PlayerExtra, 0, 0);
Players.Update(player);
Console.WriteLine(
$"[PLAYER OFFLINE] player={playerId} " +
$"preservedInstance={preservedRoomInstanceId?.ToString() ?? "none"} " +
"broadcast=controller-awaited");
}
private static bool ApplyRoomTimeLeveling(
Player player,
long now,
out int oldLevel,
out int newLevel)
{
oldLevel = player.Level;
newLevel = player.Level;
player.PlayerExtra ??= new PlayerExtra();
Heartbeat heartbeat = player.PlayerExtra.Heartbeat ??=
new Heartbeat();
long roomInstanceId =
heartbeat.isOnline
? heartbeat.roomInstance?.roomInstanceId ?? 0
: 0;
if (roomInstanceId <= 0 || player.Level >= MaximumLevel)
{
ResetRoomLevelTimer(
player.PlayerExtra,
roomInstanceId,
roomInstanceId > 0 ? now : 0);
return false;
}
if (player.PlayerExtra.LevelingRoomInstanceId != roomInstanceId)
{
ResetRoomLevelTimer(
player.PlayerExtra,
roomInstanceId,
now);
return false;
}
long previous =
player.PlayerExtra.LevelingLastHeartbeatUnixTime;
player.PlayerExtra.LevelingLastHeartbeatUnixTime = now;
if (previous <= 0 || now <= previous)
return false;
long elapsed = now - previous;
if (elapsed > MaximumCreditableHeartbeatGapSeconds)
{
return false;
}
player.PlayerExtra.LevelingRoomActiveSeconds = Math.Min(
RoomLevelSeconds,
player.PlayerExtra.LevelingRoomActiveSeconds +
checked((int)elapsed));
if (player.PlayerExtra.LevelingRoomActiveSeconds <
RoomLevelSeconds)
{
return false;
}
oldLevel = player.Level;
player.Level = Math.Min(MaximumLevel, player.Level + 1);
newLevel = player.Level;
player.PlayerExtra.LevelingRoomActiveSeconds = 0;
return newLevel > oldLevel;
}
private static void GrantLevelUpBox(
long playerId,
int oldLevel,
int newLevel)
{
if (playerId <= 0 || newLevel <= oldLevel)
return;
var player = Players.FindById(playerId);
if (player?.Player == null)
return;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Currencies ??= new List<PlayerCurrency>();
for (int level = oldLevel + 1; level <= newLevel; level++)
{
int rarity = GetLevelBoxRarity(level);
APIController.StorefrontAdminItem? boxItem =
APIController.GetWebsiteStorefrontItems()
.Where(item =>
item.Rarity == rarity &&
!string.IsNullOrWhiteSpace(item.ConsumableItemDesc))
.OrderBy(_ => LevelBoxRng.Next())
.FirstOrDefault();
if (boxItem != null)
{
PlayerInventoryStore.AddConsumable(
playerId,
boxItem.ConsumableItemDesc,
consumableItemId: 0,
friendlyName: boxItem.FriendlyName,
amount: 1);
}
APIController.StorefrontAdminItem? avatarBoxItem =
APIController.GetWebsiteStorefrontItems()
.Where(item =>
item.Rarity == rarity &&
!string.IsNullOrWhiteSpace(item.AvatarItemDesc))
.OrderBy(_ => LevelBoxRng.Next())
.FirstOrDefault() ??
APIController.GetWebsiteStorefrontItems()
.Where(item => !string.IsNullOrWhiteSpace(item.AvatarItemDesc))
.OrderBy(_ => LevelBoxRng.Next())
.FirstOrDefault();
if (avatarBoxItem != null)
{
PlayerInventoryStore.SetAvatarItemOwned(
playerId,
avatarBoxItem.AvatarItemDesc,
avatarBoxItem.AvatarItemId,
avatarBoxItem.FriendlyName,
owned: true);
}
PlayerCurrency? currency =
player.Player.PlayerExtra.Currencies.FirstOrDefault(
value => value.CurrencyType == CurrencyType.RecCenterTokens);
if (currency == null)
{
currency = new PlayerCurrency
{
CurrencyType = CurrencyType.RecCenterTokens,
BalanceType = BalanceType.NonPurchasedDefault
};
player.Player.PlayerExtra.Currencies.Add(currency);
}
long adjustedBalance = (long)currency.Balance + 1000;
currency.Balance = (int)Math.Clamp(
adjustedBalance,
(long)int.MinValue,
(long)int.MaxValue);
Console.WriteLine(
$"[LEVEL BOX] player={playerId} level={level} " +
$"rarity={rarity} box={boxItem?.FriendlyName ?? "none"} " +
$"avatarItem={avatarBoxItem?.FriendlyName ?? "none"} " +
"tokens=1000");
}
Players.Update(player);
}
private static void ResetRoomLevelTimer(
PlayerExtra playerExtra,
long roomInstanceId,
long lastHeartbeatUnixTime)
{
playerExtra.LevelingRoomInstanceId = roomInstanceId;
playerExtra.LevelingRoomActiveSeconds = 0;
playerExtra.LevelingLastHeartbeatUnixTime =
lastHeartbeatUnixTime;
}
public static void BanPlayer(
long playerId,
int durationSeconds,
string reason,
ulong bannedByPlayerId = 0)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return;
player.Player.PlayerExtra ??= new PlayerExtra();
string appealCode = GenerateAppealCode();
string reasonWithAppeal =
$"{reason}\n\nAppeal this ban: /recnet/banappeal?code={appealCode}";
player.Player.PlayerExtra.ModerationBlockDetails =
new ModerationBlockDetails
{
ReportCategory =
ReportCategory.CoC_Discrimination,
Duration = durationSeconds,
IsBan = true,
Message = reasonWithAppeal,
ModerationSetUnixTime =
DateTimeOffset.UtcNow
.ToUnixTimeSeconds(),
BannedByPlayerId = bannedByPlayerId,
AppealCode = appealCode
};
ApplyBanStateToHeartbeat(
player,
playerId,
out _);
Players.Update(player);
NotiController.ForceDisconnectPlayer(playerId);
}
private const string AppealCodeChars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
private static string GenerateAppealCode()
{
Span<char> buffer = stackalloc char[10];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = AppealCodeChars[System.Random.Shared.Next(AppealCodeChars.Length)];
return new string(buffer);
}
public static bool UnbanPlayer(long playerId)
{
var player = Players.FindById(playerId);
if (player?
.Player?
.PlayerExtra?
.ModerationBlockDetails == null)
{
return false;
}
player.Player.PlayerExtra.ModerationBlockDetails =
new ModerationBlockDetails();
Players.Update(player);
return true;
}
public static bool UpdateEquipment(
long playerId,
List<EquipmentItem> equipment)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Equipment = equipment;
return Players.Update(player);
}
public static string? GetEmail(long playerId)
{
var player = Players.FindById(playerId);
return player?.Player?.Email;
}
public static bool UpdateDisplayEmoji(
long playerId,
string displayEmoji)
{
if (playerId <= 0)
return false;
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
player.Player.DisplayEmoji = displayEmoji ?? "";
return Players.Update(player);
}
public static bool UpdateEmail(
long playerId,
string email)
{
var player = Players.FindById(playerId);
if (player?.Player == null)
return false;
player.Player.Email = email;
return Players.Update(player);
}
public static bool IsPlayerBanned(
long playerId,
out ModerationBlockDetails? details)
{
details = null;
var player = Players.FindById(playerId);
var mod = player?
.Player?
.PlayerExtra?
.ModerationBlockDetails;
if (mod == null || mod.IsBan != true)
return false;
long now = DateTimeOffset.UtcNow
.ToUnixTimeSeconds();
bool permanentBan = mod.Duration <= 0;
bool stillBanned =
permanentBan ||
(mod.ModerationSetUnixTime + mod.Duration) > now;
details = stillBanned ? mod : null;
return stillBanned;
}
}
}