Implement Redis for storing rooms

This commit is contained in:
jvyden 2022-05-15 12:04:10 -04:00
parent e998e59607
commit e12a798fd5
No known key found for this signature in database
GPG key ID: 18BCF2BE0262B278
14 changed files with 194 additions and 67 deletions

View file

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Development Database" type="docker-deploy" factoryName="docker-compose.yml"
<configuration default="false" name="Development Databases" type="docker-deploy" factoryName="docker-compose.yml"
server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
@ -7,6 +7,7 @@
<option name="sourceFilePath" value="docker-compose.yml"/>
</settings>
</deployment>
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isPublishEnabled="true"/>
<method v="2"/>
</configuration>
</component>

View file

@ -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
(

View file

@ -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<User> users = new();
List<int> 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);
}
}

View file

@ -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<User> users = await this.database.Users.OrderByDescending(_ => EF.Functions.Random()).Take(2).ToListAsync();
List<int> 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
}

View file

@ -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 @@
<meta http-equiv="refresh" content="@refreshSeconds">
</noscript>
<p>@RoomHelper.Rooms.Count rooms</p>
<p>@RoomHelper.Rooms.Count() rooms</p>
<a href="/debug/roomVisualizer/createFakeRoom">
<div class="ui blue button">Create Fake Room</div>
@ -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";
<div class="ui @color inverted segment">
<h3>Room @room.RoomId</h3>
@ -73,9 +74,9 @@
<b>You are currently in this room.</b>
</p>
}
<p>@room.Players.Count players, state is @room.State, version is @room.RoomVersion.ToPrettyString()on paltform @room.RoomPlatform</p>
<p>@room.PlayerIds.Count players, state is @room.State, version is @room.RoomVersion.ToPrettyString()on paltform @room.RoomPlatform</p>
<p>Slot type: @room.Slot.SlotType, slot id: @room.Slot.SlotId</p>
@foreach (User player in room.Players)
@foreach (User player in room.GetPlayers(Model.Database))
{
<div class="ui segment">@player.Username</div>
}

View file

@ -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<T>(this IRedisCollection<T> collection, Func<T, bool> predicate)
{
foreach (T item in collection)
{
if (!predicate.Invoke(item)) continue;
collection.DeleteSync(item);
}
}
public static void DeleteAll<T>(this IRedisCollection<T> collection)
{
foreach (T item in collection)
{
collection.DeleteSync(item);
}
}
}

View file

@ -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<User> GetPlayers(this Room room, Database database)
{
List<User> 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;
}
}

View file

@ -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<Room> Rooms = new();
public static readonly IRedisCollection<Room> Rooms = Redis.GetRooms();
public static readonly RoomSlot PodSlot = new()
{
@ -49,14 +52,9 @@ public class RoomHelper
return null;
}
bool anyRoomsLookingForPlayers;
List<Room> rooms;
IEnumerable<Room> 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<int, string> 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<Player>();
response.Locations = new List<string>();
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<User>
new List<int>
{
user,
userId,
},
roomVersion,
roomPlatform,
slot
);
public static Room CreateRoom(List<User> users, GameVersion roomVersion, Platform roomPlatform, RoomSlot? slot = null)
public static Room CreateRoom(List<int> 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)))
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<User> playersToRemove = room.Players.Where(player => player.Status.StatusType == StatusType.Offline).ToList();
foreach (User user in playersToRemove) room.Players.Remove(user);
List<User> players = room.GetPlayers(database ?? new Database());
List<int> 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)
{

View file

@ -18,4 +18,5 @@ public enum LogArea
Photos,
Resources,
Logger,
Redis,
}

View file

@ -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<Room> GetRooms() => provider.RedisCollection<Room>();
}

View file

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

View file

@ -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<User> 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<int> 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)

View file

@ -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; } = "";

View file

@ -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: