This commit is contained in:
nito9999
2026-06-12 15:35:10 -04:00
parent f5716c7160
commit 6706b6f478
60 changed files with 48656 additions and 0 deletions
+340
View File
@@ -0,0 +1,340 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using FemRec2023.Classes;
using LiteDB;
namespace FemRec2023.Classes.DBs.DBClasses
{
public class PlayerDBClasses
{
public class FullPlayer
{
[BsonId]
public long PlayerId { get; set; }
public List<mPlatformID> PlatformIds { get; set; } = new();
public List<string>? DeviceIds { get; set; } = new();
public string? AuthToken { get; set; }
public string? Password { get; set; }
public List<PlayerRoles> PlayerRoles { get; set; } = new();
public Player? Player { get; set; }
}
public class Player
{
public string? Username { get; set; }
public string? DisplayName { get; set; }
public string? Bio { get; set; }
public int AvailableUsernameChanges { get; set; } = 3;
public bool? IsJunior { get; set; }
public int Level { get; set; } = 1;
public int XP { get; set; } = 0;
public string? ProfileImage { get; set; }
public string? BannerImage { get; set; }
public string? Email { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime LastLoginAt { get; set; }
public DateTime? Birthday { get; set; }
public CurrentAuthSession? CurrentAuthSession { get; set; } = new CurrentAuthSession();
public Reputation Reputation { get; set; } = new Reputation();
public List<long> VisitedRooms { get; set; } = new();
public List<long> CheeredRooms { get; set; } = new List<long>();
public List<long> FavoritedRooms { get; set; } = new List<long>();
public PlayerExtra PlayerExtra { get; set; } = new PlayerExtra();
}
public class PlayerDTOBase
{
public long accountId { get; set; }
public DateTime createdAt { get; set; }
public string? displayName { get; set; }
public bool? isJunior { get; set; }
public int platforms { get; set; }
public string? profileImage { get; set; }
public string? username { get; set; }
public int personalPronouns { get; set; }
public int identityFlags { get; set; }
}
public class PlayerDTO : PlayerDTOBase { }
public class PlayerMeDTO : PlayerDTOBase
{
public int availableUsernameChanges { get; set; } = 3;
public DateTime? birthday { get; set; }
public string? email { get; set; }
public string? phone { get; set; }
}
public class CurrentAuthSession { }
public class PlayerExtra
{
public Avatar Avatar { get; set; } = new Avatar();
public List<string> AvatarItems { get; set; } = new();
public List<SavedOutfit> SavedAvatars { get; set; } = new();
public ModerationBlockDetails? ModerationBlockDetails { get; set; } = new ModerationBlockDetails();
public List<Setting> Settings { get; set; } = new();
public Heartbeat Heartbeat { get; set; } = new Heartbeat();
public List<PlayerCurrency> Currencies { get; set; } = new();
}
public class PlayerCurrency
{
public int Balance { get; set; }
public CurrencyType CurrencyType { get; set; }
public BalanceType BalanceType { get; set; }
}
public class Avatar
{
public string OutfitSelections { get; set; } = "";
public string FaceFeatures { get; set; } = "";
public string SkinColor { get; set; } = "";
public string HairColor { get; set; } = "";
}
public class Heartbeat
{
public string appVersion { get; set; } = ServerConfig.GameVersion.ToString();
public DeviceClasses? deviceClass { get; set; } = DeviceClasses.Unknown;
public MatchmakingErrorCode? errorCode { get; set; } = null;
public bool isOnline { get; set; } = false;
public long playerId { get; set; } = 0;
public RoomInstance? roomInstance { get; set; } = null;
public StatusVisibility statusVisibility { get; set; } = StatusVisibility.Online;
public int vrMovementMode { get; set; } = 0;
}
public class RoomInstance
{
public bool encryptVoiceChat { get; set; }
public long clubId { get; set; } = 0;
public string? dataBlob { get; set; }
public long eventId { get; set; } = 0;
public bool isFull { get; set; }
public bool isInProgress { get; set; }
public bool isPrivate { get; set; }
public string location { get; set; }
public int maxCapacity { get; set; }
public string Name { get; set; }
public string photonRegion { get; set; }
public string photonRegionId { get; set; }
public string photonRoomId { get; set; }
public string roomCode { get; set; } = "";
public long roomId { get; set; }
public long roomInstanceId { get; set; }
public RoomInstanceType roomInstanceType { get; set; }
public long subRoomId { get; set; }
}
public class Reputation
{
public long AccountId { get; set; }
public bool IsCheerful { get; set; }
public double Noteriety { get; set; }
public CheerCategory SelectedCheer { get; set; }
public int CheerCredit { get; set; }
public int CheerGeneral { get; set; }
public int CheerHelpful { get; set; }
public int CheerCreative { get; set; }
public int CheerGreatHost { get; set; }
public int CheerSportsman { get; set; }
public int SubscriberCount { get; set; }
public int SubscribedCount { get; set; }
}
public class ModerationBlockDetails
{
public ReportCategory ReportCategory { get; set; } = ReportCategory.Moderator;
public int Duration { get; set; } = 0;
public long GameSessionId { get; set; } = 0;
public bool? IsBan { get; set; } = false;
public bool? IsHostKick { get; set; } = false;
public string? Message { get; set; } = "";
public ulong? PlayerIdReporter { get; set; } = null;
[JsonIgnore]
public long ModerationSetUnixTime { get; set; } = 0;
[JsonIgnore]
public ulong BannedByPlayerId { get; set; } = 0;
}
public class Setting
{
public required string Key { get; set; }
public required string Value { get; set; }
}
public class mPlatformID
{
public Platforms Platform { get; set; }
public ulong PlatformId { get; set; }
}
public class CachedLogins
{
public Platforms platform { get; set; }
public string? platformId { get; set; }
public long accountId { get; set; }
public DateTime? lastLoginTime { get; set; }
public bool requirePassword { get; set; }
}
public class PlayerProgressionDTO
{
public long PlayerId { get; set; }
public int Level { get; set; }
public int XP { get; set; }
}
public enum Platforms
{
All = -1,
Steam,
Oculus,
PlayStation,
Xbox,
HeadlessBot,
IOS,
GooglePlay
}
public enum PlayerRoles
{
Screenshare,
Moderator,
Developer
}
public enum ReportCategory
{
Moderator = -1,
Unknown,
DEPRECATED_MicrophoneAbuse,
Harassment,
Cheating,
DEPRECATED_ImmatureBehavior,
AFK,
Misc,
Underage,
VoteKick = 10,
MisleadingPurchases,
CoC_Underage = 100,
CoC_Sexual,
CoC_Discrimination,
CoC_Trolling,
CoC_NameOrProfile,
IssuingInaccurateReports = 1000
}
public enum CheerCategory
{
General,
Helpful = 10,
Sportmanship = 20,
GreatHost = 30,
Creative = 40,
RecRoomDeveloper = 9000
}
public enum MatchmakingErrorCode
{
UnknownError = -1,
Success,
NoSuchGame,
PlayerNotOnline,
InsufficientSpace,
EventNotStarted,
EventAlreadyFinished,
BlockedFromRoom = 7,
JuniorNotAllowed = 11,
Banned,
AlreadyInBestInstance,
InsufficientRelationship,
UpdateRequired = 16,
AlreadyInTargetInstance,
UGCNotAllowed = 19,
NoSuchRoom,
RoomIsNotActive = 22,
RoomBlockedByCreator,
RoomIsPrivate = 25,
RoomInstanceIsPrivate,
DeviceClassNotSupported = 30,
DeviceClassNotSupportedByRoomOwner,
MovementModeNotSupportedByRoomOwner,
EventIsPrivate = 35,
RoomInviteExpired = 40,
NoAvailableRegion = 45,
NotorietyTooPoor = 50,
BannedFromRoom = 55,
NoSuchRoomPlaylist = 60,
RoomPlaylistIsNotActive,
RoomPlaylistIsPrivate,
NoSuchClub = 70,
ClubHasNoClubhouse,
ClubIsNotActive = 73,
NotAMemberOfClub,
BannedFromClub,
InstanceJoinNotPermitted,
LevelTooLow
}
public enum DeviceClasses
{
Unknown,
VR,
Screen,
Mobile,
VRLow,
Quest2
}
public enum CurrencyType
{
Invalid,
LaserTagTickets,
RecCenterTokens,
LostSkullsGold = 100,
DraculaSilver,
RecRoyale_Season1 = 200,
RoomCurrency = 300
}
public enum BalanceType
{
NonPurchasedNotUsableInP2P = -2,
NonPurchasedDefault,
SteamPurchased,
OculusPurchased,
PlayStationPurchased,
MicrosoftPurchased,
IOSPurchased = 5,
GooglePlayPurchased,
PlayStationNonPurchasedP2P = 100,
NonPlayStationNonPurchasedP2P,
NonPurchasedEarnedByP2P = 1000
}
public enum StatusVisibility
{
Online,
Away,
Offline,
Unknown = 100
}
public enum RoomInstanceType
{
Public,
Private,
Dormroom,
Event,
Meetup,
Clubhouse
}
public class SavedOutfit : Avatar
{
public int Slot { get; set; }
public string? PreviewImageName { get; set; }
}
}
}
+172
View File
@@ -0,0 +1,172 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using FemRec2023.Classes;
using LiteDB;
namespace FemRec2023.Classes.DBs.DBClasses
{
public class RoomDBClasses
{
public class Room
{
[BsonId]
public long RoomId { get; set; }
public bool IsDorm { get; set; }
public int MaxPlayerCalculationMode { get; set; }
public int MaxPlayers { get; set; }
public bool CloningAllowed { get; set; }
public bool DisableMicAutoMute { get; set; }
public bool DisableRoomComments { get; set; }
public bool EncryptVoiceChat { get; set; }
public bool ToxmodEnabled { get; set; }
public bool LoadScreenLocked { get; set; }
public int PersistenceVersion { get; set; }
public bool AutoLocalizeRoom { get; set; }
public bool IsDeveloperOwned { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public string ImageName { get; set; }
public WarningMaskType WarningMask { get; set; }
public string? CustomWarning { get; set; }
public long CreatorAccountId { get; set; }
public RoomState? State { get; set; }
public RoomAccessibility Accessibility { get; set; }
public bool SupportsLevelVoting { get; set; }
public bool IsRRO { get; set; }
public bool SupportsScreens { get; set; }
public bool SupportsWalkVR { get; set; }
public bool SupportsTeleportVR { get; set; }
public bool SupportsVRLow { get; set; }
public bool SupportsQuest2 { get; set; }
public bool SupportsMobile { get; set; }
public bool SupportsJuniors { get; set; }
public int MinLevel { get; set; }
public DateTime CreatedAt { get; set; }
public Stats Stats { get; set; } = new Stats();
public string? RankedEntityId { get; set; }
public string? RankingContext { get; set; }
public List<SubRooms> SubRooms { get; set; } = new List<SubRooms>();
public List<Roles> Roles { get; set; } = new List<Roles>();
public string? DataBlob { get; set; }
public int UgcVersion { get; set; }
public List<Tags> Tags { get; set; } = new List<Tags>();
public List<string> PromoImages { get; set; } = new List<string>();
public List<PromoExternalContent> PromoExternalContent { get; set; } = new List<PromoExternalContent>();
public List<LoadScreens> LoadScreens { get; set; } = new List<LoadScreens>();
}
public class SubRooms
{
public long SubRoomId { get; set; }
public long RoomId { get; set; }
public string Name { get; set; }
public string? DataBlob { get; set; }
public bool IsSandbox { get; set; }
public int MaxPlayers { get; set; }
public RoomAccessibility Accessibility { get; set; }
public string UnitySceneId { get; set; }
public long SavedByAccountId { get; set; }
}
public class Tags
{
public required string Tag { get; set; }
public TagType Type { get; set; }
}
public class PromoExternalContent
{
public PromoExternalContentType Type { get; set; }
public required string Reference { get; set; }
}
public class LoadScreens
{
public string ImageName { get; set; }
public string Title { get; set; }
public string Subtitle { get; set; }
}
public class Stats
{
public int CheerCount { get; set; } = 0;
public int FavoriteCount { get; set; } = 0;
public int VisitorCount { get; set; } = 0;
public int VisitCount { get; set; } = 0;
}
public class Roles
{
public long AccountId { get; set; }
public Role Role { get; set; }
public Role InvitedRole { get; set; }
}
public class RoomEditPermission
{
public bool CanEditRoom { get; set; }
public string Error { get; set; } = string.Empty;
}
public enum Role : byte
{
None,
Banned,
Host = 10,
Moderator = 20,
CoOwner = 30,
TemporaryCoOwner,
Creator = 255
}
[Flags]
public enum WarningMaskType
{
None = 0,
Scary = 1,
Mature = 2,
FlashingLights = 4,
IntenseMotion = 8,
Violence = 16,
Custom = 32,
Reports = 64
}
public enum RoomAccessibility
{
Private,
Public,
Unlisted
}
public enum RoomState
{
Active,
PendingJunior = 11,
Moderation_PendingReview = 100,
Moderation_Closed,
MarkedForDelete = 1000
}
public enum TagType
{
General,
Auto,
AGOnly,
Banned
}
public enum PromoExternalContentType
{
YouTube
}
public enum JoinMode
{
PublicMatchmaking,
PublicNewInstance,
PrivateNewInstance
}
}
}
+252
View File
@@ -0,0 +1,252 @@
using System;
using LiteDB;
using FemRec2023.Classes;
using FemRec2023.Classes.DBs.DBClasses;
using static FemRec2023.Classes.DBs.DBClasses.PlayerDBClasses;
namespace FemRec2023.Classes.DBs
{
public class PlayerDB
{
public static LiteDatabase PlayerDBFile = new LiteDatabase(Path.Combine(Program.dataDir, "DBs", "Players.db"));
public static readonly ILiteCollection<FullPlayer> Players = PlayerDBFile.GetCollection<FullPlayer>("Players");
public static FullPlayer CreateAccount(Platforms platform, ulong platformId, bool isJunior)
{
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
{
Settings = new List<Setting>
{
new Setting { Key = "Recroom.AccountCreation.HasStarted", Value = "True" },
new Setting { Key = "Recroom.AccountCreation.HasChosenUsername", Value = "True" },
new Setting { Key = "Recroom.AccountCreation.HasCreatedPassword", Value = "True" },
new Setting { Key = "Recroom.AccountCreation.HasFinished", Value = "True" },
new Setting { Key = "TUTORIAL_COMPLETE_MASK", Value = "57" }
}
}
};
var newFullPlayer = new FullPlayer
{
PlatformIds = new List<mPlatformID>
{
new mPlatformID { Platform = platform, PlatformId = platformId }
},
Player = newPlayerData,
PlayerRoles = new List<PlayerRoles> { PlayerRoles.Developer },
AuthToken = Guid.NewGuid().ToString()
};
Players.Insert(newFullPlayer);
return newFullPlayer;
}
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()
}).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.displayName = p.DisplayName;
dto.profileImage = p.ProfileImage;
dto.isJunior = p.IsJunior;
dto.createdAt = p.CreatedAt;
dto.platforms = platformFlags;
dto.personalPronouns = 0;
dto.identityFlags = 0;
if (accountMe && dto is PlayerMeDTO meDto)
{
meDto.availableUsernameChanges = p.AvailableUsernameChanges;
meDto.birthday = p.Birthday;
meDto.email = p.Email;
meDto.phone = null;
}
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 PlayerMeDTO? GetAccountMe(long accountId)
{
var player = Players.FindById(accountId);
if (player == null)
return null;
return MapToDTO(player, true) as PlayerMeDTO;
}
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 SetPlayerSetting(string key, string value, long playerId)
{
if (string.IsNullOrWhiteSpace(key) || playerId <= 0)
return;
if (key is "SplitTestAssignedSegments" or "Growth.LastEmailPromptTime")
return;
var player = Players.FindById(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 });
}
Players.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 = 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
};
}).ToList();
}
public static Heartbeat GetPlayerHeartbeat(long playerId)
{
var player = Players.FindOne(x => x.PlayerId == playerId);
var hb = player?.Player.PlayerExtra.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)
{
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;
}
else
{
return null;
}
}
}
}
+159
View File
@@ -0,0 +1,159 @@
using System;
using System.Linq;
using System.Text.Json;
using LiteDB;
using FemRec2023.Classes;
using FemRec2023.Classes.DBs.DBClasses;
using static FemRec2023.Classes.DBs.DBClasses.RoomDBClasses;
namespace FemRec2023.Classes.DBs
{
public class RoomDB
{
public static LiteDatabase RoomDBFile = new LiteDatabase(Path.Combine(Program.dataDir, "DBs", "Rooms.db"));
public static readonly ILiteCollection<Room> Rooms = RoomDBFile.GetCollection<Room>("Rooms");
public static async Task ImportRooms(string path)
{
try
{
string jsonData = await File.ReadAllTextAsync(path);
var roomsList = System.Text.Json.JsonSerializer.Deserialize<List<Room>>(jsonData);
if (roomsList == null) return;
foreach (var room in roomsList)
{
await AddRoom(room, log: true);
}
}
catch (Exception ex)
{
Console.WriteLine($"[Import Error] Failed to import JSON: {ex.Message}");
}
}
public static async Task AddRoom(Room newRoom, bool log = false, bool shouldAssignNewIds = true)
{
if (newRoom == null) return;
await Task.Run(() =>
{
try
{
if (shouldAssignNewIds || newRoom.RoomId <= 0)
{
newRoom.RoomId = GetNextRoomId();
}
newRoom.RankedEntityId = newRoom.RoomId.ToString();
if (newRoom.SubRooms != null && newRoom.SubRooms.Any())
{
long currentMaxSubId = GetNextSubRoomId();
foreach (var sub in newRoom.SubRooms)
{
if (shouldAssignNewIds || sub.SubRoomId <= 0)
{
sub.SubRoomId = currentMaxSubId;
currentMaxSubId++;
}
sub.RoomId = newRoom.RoomId;
}
}
Rooms.Insert(newRoom);
if (log)
{
Console.WriteLine($"[DB] Added room: {newRoom.Name} (ID: {newRoom.RoomId})");
}
}
catch (Exception ex)
{
Console.WriteLine($"[DB Error] Failed to add room {newRoom.Name}: {ex.Message}");
}
});
}
public static long GetNextRoomId()
{
if (Rooms.Count() == 0)
return 1;
var maxId = Rooms.Max(x => x.RoomId);
return Convert.ToInt64(maxId) + 1;
}
public static long GetNextSubRoomId()
{
var allRooms = Rooms.FindAll().ToList();
if (allRooms.Count == 0)
return 1;
long maxSubId = 0;
foreach (var room in allRooms)
{
if (room.SubRooms != null && room.SubRooms.Any())
{
long currentMax = room.SubRooms.Max(s => Convert.ToInt64(s.SubRoomId));
if (currentMax > maxSubId) maxSubId = currentMax;
}
}
return maxSubId + 1;
}
public static Room GetRoom(long roomId)
{
return Rooms.FindById(roomId);
}
public static Room GetRoomByName(string name)
{
return Rooms.FindOne(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
public static List<Room> GetRoomsByNames(List<string> names)
{
if (names == null || names.Count == 0)
return new List<Room>();
return Rooms.Find(room =>
names.Contains(room.Name, StringComparer.OrdinalIgnoreCase)
).ToList();
}
public static (List<Room> Results, int Total) GetHotRooms(string tag, int skip, int take)
{
var query = Rooms.Query().Where(r => !r.IsDorm);
string t = tag?.ToLower();
bool isRRO = (t == "rro" || t == "recroomoriginal");
if (isRRO)
{
query = query.Where("Tags[*].Tag ANY IN ['rro', 'recroomoriginal']");
}
else if (t != "new")
{
query = query.Where("Tags[*].Tag ALL NOT IN ['base', 'rro', 'recroomoriginal']");
}
if (!isRRO)
{
query = query.Where(r => r.Accessibility == RoomAccessibility.Public);
}
var finalQuery = (t == "new")
? query.OrderByDescending(r => r.CreatedAt)
: query.OrderByDescending(r => r.Stats.VisitCount);
var results = finalQuery.Skip(skip).Limit(take).ToList();
int total = finalQuery.Count();
return (results, total);
}
}
}