diff --git a/.run/Development Database.run.xml b/.run/Development Database.run.xml index deac13d8..63dcf575 100644 --- a/.run/Development Database.run.xml +++ b/.run/Development Database.run.xml @@ -1,5 +1,5 @@ - @@ -7,6 +7,7 @@ + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs index 2bc7f141..25e19a3d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs @@ -120,7 +120,7 @@ public class LoginController : ControllerBase await this.database.SaveChangesAsync(); // Create a new room on LBP2/3/Vita - if (token.GameVersion != GameVersion.LittleBigPlanet1) RoomHelper.CreateRoom(user, token.GameVersion, token.Platform); + if (token.GameVersion != GameVersion.LittleBigPlanet1) RoomHelper.CreateRoom(user.UserId, token.GameVersion, token.Platform); return this.Ok ( diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs index 15f932b5..693d2fb4 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs @@ -74,10 +74,10 @@ public class MatchController : ControllerBase if (matchData is UpdateMyPlayerData playerData) { MatchHelper.SetUserLocation(user.UserId, gameToken.UserLocation); - Room? room = RoomHelper.FindRoomByUser(user, gameToken.GameVersion, gameToken.Platform, true); + Room? room = RoomHelper.FindRoomByUser(user.UserId, gameToken.GameVersion, gameToken.Platform, true); if (playerData.RoomState != null) - if (room != null && Equals(room.Host, user)) + if (room != null && Equals(room.HostId, user.UserId)) room.State = (RoomState)playerData.RoomState; } @@ -101,12 +101,12 @@ public class MatchController : ControllerBase if (matchData is CreateRoom createRoom && MatchHelper.UserLocations.Count >= 1) { - List users = new(); + List users = new(); foreach (string playerUsername in createRoom.Players) { User? player = await this.database.Users.FirstOrDefaultAsync(u => u.Username == playerUsername); // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (player != null) users.Add(player); + if (player != null) users.Add(player.UserId); else return this.BadRequest(); } @@ -116,7 +116,7 @@ public class MatchController : ControllerBase if (matchData is UpdatePlayersInRoom updatePlayersInRoom) { - Room? room = RoomHelper.Rooms.FirstOrDefault(r => r.Host == user); + Room? room = RoomHelper.Rooms.FirstOrDefault(r => r.HostId == user.UserId); if (room != null) { @@ -129,7 +129,7 @@ public class MatchController : ControllerBase else return this.BadRequest(); } - room.Players = users; + room.PlayerIds = users.Select(u => u.UserId).ToList(); RoomHelper.CleanupRooms(null, room); } } diff --git a/ProjectLighthouse.Servers.Website/Controllers/Debug/RoomVisualizerController.cs b/ProjectLighthouse.Servers.Website/Controllers/Debug/RoomVisualizerController.cs index 2506d875..5336e43b 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/Debug/RoomVisualizerController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/Debug/RoomVisualizerController.cs @@ -1,3 +1,4 @@ +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; @@ -22,12 +23,12 @@ public class RoomVisualizerController : ControllerBase #if !DEBUG return this.NotFound(); #else - List users = await this.database.Users.OrderByDescending(_ => EF.Functions.Random()).Take(2).ToListAsync(); + List users = await this.database.Users.OrderByDescending(_ => EF.Functions.Random()).Take(2).Select(u => u.UserId).ToListAsync(); RoomHelper.CreateRoom(users, GameVersion.LittleBigPlanet2, Platform.PS3); - foreach (User user in users) + foreach (int user in users) { - MatchHelper.SetUserLocation(user.UserId, "127.0.0.1"); + MatchHelper.SetUserLocation(user, "127.0.0.1"); } return this.Redirect("/debug/roomVisualizer"); #endif @@ -39,7 +40,7 @@ public class RoomVisualizerController : ControllerBase #if !DEBUG return this.NotFound(); #else - RoomHelper.Rooms.RemoveAll(_ => true); + RoomHelper.Rooms.DeleteAll(); return this.Redirect("/debug/roomVisualizer"); #endif } diff --git a/ProjectLighthouse.Servers.Website/Pages/Debug/RoomVisualizerPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/Debug/RoomVisualizerPage.cshtml index 5e38349a..9f1495c6 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Debug/RoomVisualizerPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Debug/RoomVisualizerPage.cshtml @@ -1,4 +1,5 @@ @page "/debug/roomVisualizer" +@using LBPUnion.ProjectLighthouse.Extensions @using LBPUnion.ProjectLighthouse.Helpers @using LBPUnion.ProjectLighthouse.Types @using LBPUnion.ProjectLighthouse.Types.Match @@ -35,7 +36,7 @@ -

@RoomHelper.Rooms.Count rooms

+

@RoomHelper.Rooms.Count() rooms

Create Fake Room
@@ -63,7 +64,7 @@ @foreach (Room room in RoomHelper.Rooms) { - bool userInRoom = room.Players.Select(p => p.Username).Contains(Model.User?.Username); + bool userInRoom = room.PlayerIds.Contains(Model.User?.UserId ?? -1); string color = userInRoom ? "green" : "blue";

Room @room.RoomId

@@ -73,9 +74,9 @@ You are currently in this room.

} -

@room.Players.Count players, state is @room.State, version is @room.RoomVersion.ToPrettyString()on paltform @room.RoomPlatform

+

@room.PlayerIds.Count players, state is @room.State, version is @room.RoomVersion.ToPrettyString()on paltform @room.RoomPlatform

Slot type: @room.Slot.SlotType, slot id: @room.Slot.SlotId

- @foreach (User player in room.Players) + @foreach (User player in room.GetPlayers(Model.Database)) {
@player.Username
} diff --git a/ProjectLighthouse/Extensions/IRedisCollectionExtensions.cs b/ProjectLighthouse/Extensions/IRedisCollectionExtensions.cs new file mode 100644 index 00000000..b9384846 --- /dev/null +++ b/ProjectLighthouse/Extensions/IRedisCollectionExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Redis.OM.Searching; + +namespace LBPUnion.ProjectLighthouse.Extensions; + +[SuppressMessage("ReSharper", "LoopCanBePartlyConvertedToQuery")] +public static class IRedisCollectionExtensions +{ + public static void DeleteAll(this IRedisCollection collection, Func predicate) + { + foreach (T item in collection) + { + if (!predicate.Invoke(item)) continue; + + collection.DeleteSync(item); + } + } + + public static void DeleteAll(this IRedisCollection collection) + { + foreach (T item in collection) + { + collection.DeleteSync(item); + } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Extensions/RoomExtensions.cs b/ProjectLighthouse/Extensions/RoomExtensions.cs new file mode 100644 index 00000000..32976beb --- /dev/null +++ b/ProjectLighthouse/Extensions/RoomExtensions.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using LBPUnion.ProjectLighthouse.Types; +using LBPUnion.ProjectLighthouse.Types.Match; + +namespace LBPUnion.ProjectLighthouse.Extensions; + +public static class RoomExtensions +{ + public static List GetPlayers(this Room room, Database database) + { + List players = new(); + foreach (int playerId in room.PlayerIds) + { + User? player = database.Users.FirstOrDefault(p => p.UserId == playerId); + Debug.Assert(player != null); + + players.Add(player); + } + + return players; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/RoomHelper.cs b/ProjectLighthouse/Helpers/RoomHelper.cs index b925f55b..84e98c8f 100644 --- a/ProjectLighthouse/Helpers/RoomHelper.cs +++ b/ProjectLighthouse/Helpers/RoomHelper.cs @@ -3,17 +3,20 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Match; using LBPUnion.ProjectLighthouse.Types.Profiles; +using Redis.OM; +using Redis.OM.Searching; namespace LBPUnion.ProjectLighthouse.Helpers; public class RoomHelper { - public static readonly List Rooms = new(); + public static readonly IRedisCollection Rooms = Redis.GetRooms(); public static readonly RoomSlot PodSlot = new() { @@ -49,14 +52,9 @@ public class RoomHelper return null; } - bool anyRoomsLookingForPlayers; - List rooms; + IEnumerable rooms = Rooms; - lock(Rooms) - { - anyRoomsLookingForPlayers = Rooms.Any(r => r.IsLookingForPlayers); - rooms = anyRoomsLookingForPlayers ? Rooms.Where(r => anyRoomsLookingForPlayers && r.IsLookingForPlayers).ToList() : Rooms; - } + rooms = rooms.OrderBy(r => r.IsLookingForPlayers); rooms = rooms.Where(r => r.RoomVersion == roomVersion).ToList(); if (platform != null) rooms = rooms.Where(r => r.RoomPlatform == platform).ToList(); @@ -72,24 +70,24 @@ public class RoomHelper // Don't attempt to dive into the current room the player is in. if (user != null) { - rooms = rooms.Where(r => !r.Players.Contains(user)).ToList(); + rooms = rooms.Where(r => !r.PlayerIds.Contains(user.UserId)).ToList(); } foreach (Room room in rooms) // Look for rooms looking for players before moving on to rooms that are idle. { - if (user != null && MatchHelper.DidUserRecentlyDiveInWith(user.UserId, room.Host.UserId)) continue; + if (user != null && MatchHelper.DidUserRecentlyDiveInWith(user.UserId, room.HostId)) continue; Dictionary relevantUserLocations = new(); // Determine if all players in a room have UserLocations stored, also store the relevant userlocations while we're at it - bool allPlayersHaveLocations = room.Players.All + bool allPlayersHaveLocations = room.PlayerIds.All ( p => { - bool gotValue = MatchHelper.UserLocations.TryGetValue(p.UserId, out string? value); + bool gotValue = MatchHelper.UserLocations.TryGetValue(p, out string? value); - if (gotValue && value != null) relevantUserLocations.Add(p.UserId, value); + if (gotValue && value != null) relevantUserLocations.Add(p, value); return gotValue; } ); @@ -104,7 +102,7 @@ public class RoomHelper response.Players = new List(); response.Locations = new List(); - foreach (User player in room.Players) + foreach (User player in room.GetPlayers(new Database())) { response.Players.Add ( @@ -147,74 +145,82 @@ public class RoomHelper return null; } - public static Room CreateRoom(User user, GameVersion roomVersion, Platform roomPlatform, RoomSlot? slot = null) + public static Room CreateRoom(int userId, GameVersion roomVersion, Platform roomPlatform, RoomSlot? slot = null) => CreateRoom ( - new List + new List { - user, + userId, }, roomVersion, roomPlatform, slot ); - public static Room CreateRoom(List users, GameVersion roomVersion, Platform roomPlatform, RoomSlot? slot = null) + public static Room CreateRoom(List users, GameVersion roomVersion, Platform roomPlatform, RoomSlot? slot = null) { Room room = new() { RoomId = RoomIdIncrement, - Players = users, + PlayerIds = users, State = RoomState.Idle, Slot = slot ?? PodSlot, RoomVersion = roomVersion, RoomPlatform = roomPlatform, }; - CleanupRooms(room.Host, room); - lock(Rooms) Rooms.Add(room); - Logger.LogInfo($"Created room (id: {room.RoomId}) for host {room.Host.Username} (id: {room.Host.UserId})", LogArea.Match); + CleanupRooms(room.HostId, room); + lock(Rooms) Rooms.Insert(room); + Logger.LogInfo($"Created room (id: {room.RoomId}) for host {room.HostId}", LogArea.Match); return room; } - public static Room? FindRoomByUser(User user, GameVersion roomVersion, Platform roomPlatform, bool createIfDoesNotExist = false) + public static Room? FindRoomByUser(int userId, GameVersion roomVersion, Platform roomPlatform, bool createIfDoesNotExist = false) { lock(Rooms) - foreach (Room room in Rooms.Where(room => room.Players.Any(player => user == player))) + foreach (Room room in Rooms.Where(room => room.PlayerIds.Any(player => userId == player))) return room; - return createIfDoesNotExist ? CreateRoom(user, roomVersion, roomPlatform) : null; + return createIfDoesNotExist ? CreateRoom(userId, roomVersion, roomPlatform) : null; } public static Room? FindRoomByUserId(int userId) { lock(Rooms) - foreach (Room room in Rooms.Where(room => room.Players.Any(player => player.UserId == userId))) - return room; + foreach (Room room in Rooms) + { + if (room.PlayerIds.Any(p => p == userId)) + { + return room; + } + } return null; } [SuppressMessage("ReSharper", "InvertIf")] - public static void CleanupRooms(User? host = null, Room? newRoom = null) + public static void CleanupRooms(int? hostId = null, Room? newRoom = null, Database? database = null) { +// return; lock(Rooms) { - int roomCountBeforeCleanup = Rooms.Count; + int roomCountBeforeCleanup = Rooms.Count(); // Remove offline players from rooms foreach (Room room in Rooms) { - // do not shorten, this prevents collection modified errors - List playersToRemove = room.Players.Where(player => player.Status.StatusType == StatusType.Offline).ToList(); - foreach (User user in playersToRemove) room.Players.Remove(user); + List players = room.GetPlayers(database ?? new Database()); + + List playersToRemove = players.Where(player => player.Status.StatusType == StatusType.Offline).Select(player => player.UserId).ToList(); + + foreach (int player in playersToRemove) room.PlayerIds.Remove(player); } // Delete old rooms based on host - if (host != null) + if (hostId != null) try { - Rooms.RemoveAll(r => r.Host == host); + Rooms.DeleteAll(r => r.HostId == hostId); } catch { @@ -227,13 +233,13 @@ public class RoomHelper { if (room == newRoom) continue; - foreach (User newRoomPlayer in newRoom.Players) room.Players.RemoveAll(p => p == newRoomPlayer); + foreach (int newRoomPlayer in newRoom.PlayerIds) room.PlayerIds.RemoveAll(p => p == newRoomPlayer); } - Rooms.RemoveAll(r => r.Players.Count == 0); // Remove empty rooms - Rooms.RemoveAll(r => r.Players.Count > 4); // Remove obviously bogus rooms + Rooms.DeleteAll(r => r.PlayerIds.Count == 0); // Remove empty rooms + Rooms.DeleteAll(r => r.PlayerIds.Count > 4); // Remove obviously bogus rooms - int roomCountAfterCleanup = Rooms.Count; + int roomCountAfterCleanup = Rooms.Count(); if (roomCountBeforeCleanup != roomCountAfterCleanup) { diff --git a/ProjectLighthouse/Logging/LogArea.cs b/ProjectLighthouse/Logging/LogArea.cs index a43c59b8..ebb74275 100644 --- a/ProjectLighthouse/Logging/LogArea.cs +++ b/ProjectLighthouse/Logging/LogArea.cs @@ -18,4 +18,5 @@ public enum LogArea Photos, Resources, Logger, + Redis, } \ No newline at end of file diff --git a/ProjectLighthouse/Redis.cs b/ProjectLighthouse/Redis.cs new file mode 100644 index 00000000..c2b26683 --- /dev/null +++ b/ProjectLighthouse/Redis.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Types.Match; +using LBPUnion.ProjectLighthouse.Types.Settings; +using Redis.OM; +using Redis.OM.Contracts; +using Redis.OM.Searching; + +namespace LBPUnion.ProjectLighthouse; + +public static class Redis +{ + private static readonly RedisConnectionProvider provider; + + static Redis() + { + provider = new RedisConnectionProvider(ServerConfiguration.Instance.RedisConnectionString); + } + + private static bool initialized = false; + public static async Task Initialize() + { + if (initialized) throw new InvalidOperationException("Redis has already been initialized."); + + IRedisConnection connection = getConnection(); + + await connection.CreateIndexAsync(typeof(Room)); + + initialized = true; + Logger.LogSuccess("Initialized Redis.", LogArea.Redis); + } + + private static IRedisConnection getConnection() + { + Logger.LogDebug("Getting a redis connection", LogArea.Redis); + return provider.Connection; + } + + public static IRedisCollection GetRooms() => provider.RedisCollection(); +} \ No newline at end of file diff --git a/ProjectLighthouse/StartupTasks.cs b/ProjectLighthouse/StartupTasks.cs index 221a33f6..5ea135c8 100644 --- a/ProjectLighthouse/StartupTasks.cs +++ b/ProjectLighthouse/StartupTasks.cs @@ -76,6 +76,9 @@ public static class StartupTasks Logger.LogInfo("Starting room cleanup thread...", LogArea.Startup); RoomHelper.StartCleanupThread(); + Logger.LogInfo("Initializing Redis...", LogArea.Startup); + Redis.Initialize().Wait(); + stopwatch.Stop(); Logger.LogSuccess($"Ready! Startup took {stopwatch.ElapsedMilliseconds}ms. Passing off control to ASP.NET...", LogArea.Startup); } diff --git a/ProjectLighthouse/Types/Match/Room.cs b/ProjectLighthouse/Types/Match/Room.cs index 63128150..6772cc04 100644 --- a/ProjectLighthouse/Types/Match/Room.cs +++ b/ProjectLighthouse/Types/Match/Room.cs @@ -1,36 +1,47 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using LBPUnion.ProjectLighthouse.Types.Levels; +using Redis.OM.Modeling; namespace LBPUnion.ProjectLighthouse.Types.Match; +[Document(StorageType = StorageType.Json)] public class Room { - [JsonIgnore] - public List Players { get; set; } + private int roomId; - public int RoomId { get; set; } + public int RoomId { + get => this.roomId; + set { + this.RedisId = value.ToString(); + this.roomId = value; + } + } - [JsonIgnore] + [RedisIdField] + public string RedisId { get; set; } + + [Indexed] + public List PlayerIds { get; set; } + + [Indexed] public GameVersion RoomVersion { get; set; } - [JsonIgnore] + [Indexed] public Platform RoomPlatform { get; set; } + [Indexed] public RoomSlot Slot { get; set; } + + [Indexed] public RoomState State { get; set; } [JsonIgnore] - public bool IsInPod => this.Slot.SlotType == SlotType.Pod; - - [JsonIgnore] + [Indexed] public bool IsLookingForPlayers => this.State == RoomState.PlayingLevel || this.State == RoomState.DivingInWaiting; [JsonIgnore] - public User Host => this.Players[0]; - - public int PlayerCount => this.Players.Count; + public int HostId => this.PlayerIds[0]; #nullable enable public override bool Equals(object? obj) diff --git a/ProjectLighthouse/Types/Settings/ServerConfiguration.cs b/ProjectLighthouse/Types/Settings/ServerConfiguration.cs index a52031bc..31727ac4 100644 --- a/ProjectLighthouse/Types/Settings/ServerConfiguration.cs +++ b/ProjectLighthouse/Types/Settings/ServerConfiguration.cs @@ -22,7 +22,7 @@ public class ServerConfiguration // You can use an ObsoleteAttribute instead. Make sure you set it to error, though. // // Thanks for listening~ - public const int CurrentConfigVersion = 3; + public const int CurrentConfigVersion = 4; #region Meta @@ -170,6 +170,7 @@ public class ServerConfiguration public string ApiListenUrl { get; set; } = "http://localhost:10062"; public string DbConnectionString { get; set; } = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse"; + public string RedisConnectionString { get; set; } = ""; public string ExternalUrl { get; set; } = "http://localhost:10060"; public bool ConfigReloading { get; set; } public string EulaText { get; set; } = ""; diff --git a/docker-compose.yml b/docker-compose.yml index 0f507cfe..e35dd091 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,15 @@ services: - '3306' # Expose port to localhost:3306 volumes: - lighthouse-db:/var/lib/mysql + lighthouse-redis: + image: redis/redis-stack + ports: + - '6379:6379' + - '8001:8001' + expose: + - '6379' + - '8001' + volumes: lighthouse-db: \ No newline at end of file