diff --git a/.idea/.idea.ProjectLighthouse/.idea/git_toolbox_prj.xml b/.idea/.idea.ProjectLighthouse/.idea/git_toolbox_prj.xml new file mode 100644 index 00000000..b3820067 --- /dev/null +++ b/.idea/.idea.ProjectLighthouse/.idea/git_toolbox_prj.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/ProjectLighthouse.sln.DotSettings b/ProjectLighthouse.sln.DotSettings index 7a68182c..779d9d6e 100644 --- a/ProjectLighthouse.sln.DotSettings +++ b/ProjectLighthouse.sln.DotSettings @@ -72,10 +72,12 @@ UseExplicitType UseExplicitType UseExplicitType + DLC LBP MM NAT NP + PSP <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> @@ -107,15 +109,20 @@ True True True + True True True True True + True True True True True True + True + True + True True True True \ No newline at end of file diff --git a/ProjectLighthouse/Controllers/ListController.cs b/ProjectLighthouse/Controllers/ListController.cs index fffda451..d59cef44 100644 --- a/ProjectLighthouse/Controllers/ListController.cs +++ b/ProjectLighthouse/Controllers/ListController.cs @@ -30,7 +30,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers public async Task GetLevelQueue(string username, [FromQuery] int pageSize, [FromQuery] int pageStart) { Token? token = await this.database.TokenFromRequest(this.Request); - if (token == null) return this.BadRequest(); + if (token == null) return this.StatusCode(403, ""); GameVersion gameVersion = token.GameVersion; @@ -94,7 +94,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers public async Task GetFavouriteSlots(string username, [FromQuery] int pageSize, [FromQuery] int pageStart) { Token? token = await this.database.TokenFromRequest(this.Request); - if (token == null) return this.BadRequest(); + if (token == null) return this.StatusCode(403, ""); GameVersion gameVersion = token.GameVersion; diff --git a/ProjectLighthouse/Controllers/LoginController.cs b/ProjectLighthouse/Controllers/LoginController.cs index 420f8b17..d3e4df74 100644 --- a/ProjectLighthouse/Controllers/LoginController.cs +++ b/ProjectLighthouse/Controllers/LoginController.cs @@ -3,6 +3,7 @@ using System.IO; using System.Net; using System.Threading.Tasks; using Kettu; +using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types.Settings; @@ -48,11 +49,16 @@ namespace LBPUnion.ProjectLighthouse.Controllers Token? token = await this.database.AuthenticateUser(loginData, userLocation, titleId); if (token == null) return this.StatusCode(403, ""); - Logger.Log - ( - $"Successfully logged in user {(await this.database.UserFromToken(token))!.Username} as {token.GameVersion} client ({titleId})", - LoggerLevelLogin.Instance - ); + User? user = await this.database.UserFromToken(token); + if (user == null) return this.StatusCode(403, ""); + + Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client ({titleId})", LoggerLevelLogin.Instance); + + // Create a new room on LBP2+/Vita + if (token.GameVersion != GameVersion.LittleBigPlanet1) + { + RoomHelper.CreateRoom(user); + } return this.Ok ( diff --git a/ProjectLighthouse/Controllers/MatchController.cs b/ProjectLighthouse/Controllers/MatchController.cs index 4d1fa398..94f4fff5 100644 --- a/ProjectLighthouse/Controllers/MatchController.cs +++ b/ProjectLighthouse/Controllers/MatchController.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; @@ -71,33 +72,6 @@ namespace LBPUnion.ProjectLighthouse.Controllers #endregion - #region Process match data - - if (matchData is UpdateMyPlayerData) MatchHelper.SetUserLocation(user.UserId, token.UserLocation); - - if (matchData is FindBestRoom && MatchHelper.UserLocations.Count > 1) - { - foreach ((int id, string? location) in MatchHelper.UserLocations) - { - if (id == user.UserId) continue; - if (location == null) continue; - if (MatchHelper.DidUserRecentlyDiveInWith(user.UserId, id)) continue; - - User? otherUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id); - if (otherUser == null) continue; - - FindBestRoomResponse response = MatchHelper.FindBestRoomResponse(user.Username, otherUser.Username, token.UserLocation, location); - - string serialized = JsonSerializer.Serialize(response, typeof(FindBestRoomResponse)); - - MatchHelper.AddUserRecentlyDivedIn(user.UserId, id); - - return new ObjectResult($"[{{\"StatusCode\":200}},{serialized}]"); - } - } - - #endregion - #region Update LastMatch LastMatch? lastMatch = await this.database.LastMatches.Where(l => l.UserId == user.UserId).FirstOrDefaultAsync(); @@ -119,6 +93,54 @@ namespace LBPUnion.ProjectLighthouse.Controllers #endregion + #region Process match data + + if (matchData is UpdateMyPlayerData playerData) + { + MatchHelper.SetUserLocation(user.UserId, token.UserLocation); + Room? room = RoomHelper.FindRoomByUser(user, true); + + if (playerData.RoomState != null) + { + if (room != null && Equals(room.Host, user)) room.State = (RoomState)playerData.RoomState; + } + } + + if (matchData is FindBestRoom && MatchHelper.UserLocations.Count > 1) + { + FindBestRoomResponse? response = RoomHelper.FindBestRoom(user, token.UserLocation); + + if (response == null) return this.NotFound(); + + string serialized = JsonSerializer.Serialize(response, typeof(FindBestRoomResponse)); + foreach (Player player in response.Players) + { + MatchHelper.AddUserRecentlyDivedIn(user.UserId, player.User.UserId); + } + + return this.Ok($"[{{\"StatusCode\":200}},{serialized}]"); + } + + if (matchData is CreateRoom createRoom && MatchHelper.UserLocations.Count >= 1) + { + 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); + } + else return this.BadRequest(); + } + + // Create a new one as requested + RoomHelper.CreateRoom(users, createRoom.RoomSlot); + } + + #endregion + return this.Ok("[{\"StatusCode\":200}]"); } } diff --git a/ProjectLighthouse/Controllers/SlotsController.cs b/ProjectLighthouse/Controllers/SlotsController.cs index 5b536306..988f63ef 100644 --- a/ProjectLighthouse/Controllers/SlotsController.cs +++ b/ProjectLighthouse/Controllers/SlotsController.cs @@ -27,7 +27,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers public async Task SlotsBy([FromQuery] string u, [FromQuery] int pageStart, [FromQuery] int pageSize) { Token? token = await this.database.TokenFromRequest(this.Request); - if (token == null) return this.BadRequest(); + if (token == null) return this.StatusCode(403, ""); GameVersion gameVersion = token.GameVersion; @@ -71,7 +71,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers if (user == null) return this.StatusCode(403, ""); Token? token = await this.database.TokenFromRequest(this.Request); - if (token == null) return this.BadRequest(); + if (token == null) return this.StatusCode(403, ""); GameVersion gameVersion = token.GameVersion; @@ -95,7 +95,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers public async Task NewestSlots([FromQuery] int pageStart, [FromQuery] int pageSize) { Token? token = await this.database.TokenFromRequest(this.Request); - if (token == null) return this.BadRequest(); + if (token == null) return this.StatusCode(403, ""); GameVersion gameVersion = token.GameVersion; @@ -114,7 +114,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers public async Task TeamPickedSlots([FromQuery] int pageStart, [FromQuery] int pageSize) { Token? token = await this.database.TokenFromRequest(this.Request); - if (token == null) return this.BadRequest(); + if (token == null) return this.StatusCode(403, ""); GameVersion gameVersion = token.GameVersion; @@ -134,7 +134,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers public async Task LuckyDipSlots([FromQuery] int pageStart, [FromQuery] int pageSize, [FromQuery] int seed) { Token? token = await this.database.TokenFromRequest(this.Request); - if (token == null) return this.BadRequest(); + if (token == null) return this.StatusCode(403, ""); GameVersion gameVersion = token.GameVersion; diff --git a/ProjectLighthouse/Dockerfile b/ProjectLighthouse/Dockerfile index 3cbdb7a1..cd416564 100644 --- a/ProjectLighthouse/Dockerfile +++ b/ProjectLighthouse/Dockerfile @@ -1,9 +1,8 @@ -FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app -EXPOSE 80 -EXPOSE 443 +EXPOSE 10060 -FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["ProjectLighthouse/ProjectLighthouse.csproj", "ProjectLighthouse/"] RUN dotnet restore "ProjectLighthouse/ProjectLighthouse.csproj" diff --git a/ProjectLighthouse/Helpers/GameVersionHelper.cs b/ProjectLighthouse/Helpers/GameVersionHelper.cs index ba549839..c3e63be2 100644 --- a/ProjectLighthouse/Helpers/GameVersionHelper.cs +++ b/ProjectLighthouse/Helpers/GameVersionHelper.cs @@ -79,7 +79,12 @@ namespace LBPUnion.ProjectLighthouse.Helpers public static readonly string[] LittleBigPlanetVitaTitleIds = { - "PCSF00021", "PCSA00017", "PCSC00013", "PCSD00006", "PCSA00549", "PCSF00516" + "PCSF00021", "PCSA00017", "PCSC00013", "PCSD00006", "PCSA00549", "PCSF00516", + }; + + public static readonly string[] LittleBigPlanetPSPTitleIds = + { + "NPWR00500", "UCAS40262", "UCES01264", "UCUS98744", "UCJS10107", }; public static GameVersion FromTitleId(string titleId) @@ -88,8 +93,9 @@ namespace LBPUnion.ProjectLighthouse.Helpers if (LittleBigPlanet2TitleIds.Contains(titleId)) return GameVersion.LittleBigPlanet2; if (LittleBigPlanet3TitleIds.Contains(titleId)) return GameVersion.LittleBigPlanet3; if (LittleBigPlanetVitaTitleIds.Contains(titleId)) return GameVersion.LittleBigPlanetVita; + if (LittleBigPlanetPSPTitleIds.Contains(titleId)) return GameVersion.LittleBigPlanetPSP; return GameVersion.LittleBigPlanet1; } } -} +} \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/MatchHelper.cs b/ProjectLighthouse/Helpers/MatchHelper.cs index b3017e2a..30f07616 100644 --- a/ProjectLighthouse/Helpers/MatchHelper.cs +++ b/ProjectLighthouse/Helpers/MatchHelper.cs @@ -39,37 +39,6 @@ namespace LBPUnion.ProjectLighthouse.Helpers return recentlyDivedIn.Contains(otherUserId); } - public static FindBestRoomResponse FindBestRoomResponse(string username, string otherUsername, string location, string otherLocation) - => new() - { - Players = new List - { - new() - { - MatchingRes = 0, - PlayerId = otherUsername, - }, - new() - { - MatchingRes = 1, - PlayerId = username, - }, - }, - Locations = new List - { - location, - otherLocation, - }, - Slots = new List> - { - new() - { - 5, - 0, - }, - }, - }; - public static IMatchData? Deserialize(string data) { string matchType = ""; diff --git a/ProjectLighthouse/Helpers/RoomHelper.cs b/ProjectLighthouse/Helpers/RoomHelper.cs new file mode 100644 index 00000000..e654bed3 --- /dev/null +++ b/ProjectLighthouse/Helpers/RoomHelper.cs @@ -0,0 +1,162 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Kettu; +using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Types; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Match; + +namespace LBPUnion.ProjectLighthouse.Helpers +{ + public class RoomHelper + { + public static readonly List Rooms = new(); + + public static readonly RoomSlot PodSlot = new() + { + SlotType = SlotType.Pod, + SlotId = 0, + }; + + private static int roomIdIncrement = 0; + + internal static int RoomIdIncrement => roomIdIncrement++; + + public static FindBestRoomResponse? FindBestRoom(User user, string location) + { + bool anyRoomsLookingForPlayers = Rooms.Any(r => r.IsLookingForPlayers); + + List rooms = anyRoomsLookingForPlayers ? Rooms.Where(r => anyRoomsLookingForPlayers && r.IsLookingForPlayers).ToList() : Rooms; + foreach (Room room in rooms) + // Look for rooms looking for players before moving on to rooms that are idle. + { + if (MatchHelper.DidUserRecentlyDiveInWith(user.UserId, room.Host.UserId)) 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 + ( + p => + { + bool gotValue = MatchHelper.UserLocations.TryGetValue(p.UserId, out string? value); + + if (gotValue && value != null) + { + relevantUserLocations.Add(p.UserId, value); + } + return gotValue; + } + ); + + // If we don't have all locations then the game won't know how to communicate. Thus, it's not a valid room. + if (!allPlayersHaveLocations) continue; + + // If we got here then it should be a valid room. + + FindBestRoomResponse response = new(); + + response.Players = new List(); + response.Locations = new List(); + foreach (User player in room.Players) + { + response.Players.Add + ( + new Player + { + MatchingRes = 0, + User = player, + } + ); + + response.Locations.Add(relevantUserLocations.GetValueOrDefault(player.UserId)); // Already validated to exist + } + + response.Players.Add + ( + new Player + { + MatchingRes = 1, + User = user, + } + ); + + response.Locations.Add(location); + + response.Slots = new List> + { + new() + { + (int)room.Slot.SlotType, + room.Slot.SlotId, + }, + }; + + return response; + } + + return null; + } + + public static Room CreateRoom(User user, RoomSlot? slot = null) + => CreateRoom + ( + new List + { + user, + }, + slot + ); + public static Room CreateRoom(List users, RoomSlot? slot = null) + { + Room room = new() + { + RoomId = RoomIdIncrement, + Players = users, + State = RoomState.Idle, + Slot = slot ?? PodSlot, + }; + + CleanupRooms(room.Host, room); + Rooms.Add(room); + Logger.Log($"Created room (id: {room.RoomId}) for host {room.Host.Username} (id: {room.Host.UserId})", LoggerLevelMatch.Instance); + + return room; + } + + public static Room? FindRoomByUser(User user, bool createIfDoesNotExist = false) + { + foreach (Room room in Rooms) + { + foreach (User player in room.Players) + { + if (user == player) return room; + } + } + return createIfDoesNotExist ? CreateRoom(user) : null; + } + + [SuppressMessage("ReSharper", "InvertIf")] + public static void CleanupRooms(User? host = null, Room? newRoom = null) + { + // Delete old rooms based on host + if (host != null) + { + Rooms.RemoveAll(r => r.Host == host); + } + + // Remove players in this new room from other rooms + if (newRoom != null) + { + foreach (Room room in Rooms) + { + if (room == newRoom) continue; + + foreach (User newRoomPlayer in newRoom.Players) room.Players.RemoveAll(p => p == newRoomPlayer); + } + } + } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/GameVersion.cs b/ProjectLighthouse/Types/GameVersion.cs index fd3958fe..c0530d9c 100644 --- a/ProjectLighthouse/Types/GameVersion.cs +++ b/ProjectLighthouse/Types/GameVersion.cs @@ -6,6 +6,7 @@ namespace LBPUnion.ProjectLighthouse.Types LittleBigPlanet2 = 1, LittleBigPlanet3 = 2, LittleBigPlanetVita = 3, + LittleBigPlanetPSP = 4, Unknown = -1, } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Match/CreateRoom.cs b/ProjectLighthouse/Types/Match/CreateRoom.cs index 371acad9..1dc569c8 100644 --- a/ProjectLighthouse/Types/Match/CreateRoom.cs +++ b/ProjectLighthouse/Types/Match/CreateRoom.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; +using LBPUnion.ProjectLighthouse.Types.Levels; namespace LBPUnion.ProjectLighthouse.Types.Match { @@ -21,6 +22,13 @@ namespace LBPUnion.ProjectLighthouse.Types.Match [JsonIgnore] public IEnumerable FirstSlot => this.Slots[0]; + public RoomSlot RoomSlot + => new() + { + SlotType = (SlotType)Slots[0][0], + SlotId = Slots[0][1], + }; + public List NAT; public RoomState RoomState; public int HostMood; diff --git a/ProjectLighthouse/Types/Match/FindBestRoom.cs b/ProjectLighthouse/Types/Match/FindBestRoom.cs index 2b76de28..7e93f804 100644 --- a/ProjectLighthouse/Types/Match/FindBestRoom.cs +++ b/ProjectLighthouse/Types/Match/FindBestRoom.cs @@ -1,5 +1,28 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + namespace LBPUnion.ProjectLighthouse.Types.Match { - public class FindBestRoom : CreateRoom - {} + // Schema is the EXACT SAME as CreateRoom (but cant be a subclass here), so see comments there for details + [SuppressMessage("ReSharper", "CollectionNeverUpdated.Global")] + public class FindBestRoom : IMatchData + { + public List Players { get; set; } + + public List Reservations { get; set; } + public List> Slots { get; set; } + + [JsonIgnore] + public IEnumerable FirstSlot => this.Slots[0]; + + public List NAT; + public RoomState RoomState; + public int HostMood; + public int PassedNoJoinPoint; + public List Location; + public int Language; + public int BuildVersion; + public string Search; + } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Match/NatType.cs b/ProjectLighthouse/Types/Match/NatType.cs new file mode 100644 index 00000000..fd6250ac --- /dev/null +++ b/ProjectLighthouse/Types/Match/NatType.cs @@ -0,0 +1,9 @@ +namespace LBPUnion.ProjectLighthouse.Types.Match +{ + public enum NatType + { + Open = 1, + Moderate = 2, + Strict = 3, + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Match/Player.cs b/ProjectLighthouse/Types/Match/Player.cs index ad2140fa..95c18ec6 100644 --- a/ProjectLighthouse/Types/Match/Player.cs +++ b/ProjectLighthouse/Types/Match/Player.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace LBPUnion.ProjectLighthouse.Types.Match @@ -6,7 +7,11 @@ namespace LBPUnion.ProjectLighthouse.Types.Match [Serializable] public class Player { - public string PlayerId { get; set; } + [JsonIgnore] + public User User { get; set; } + + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public string PlayerId => User.Username; [JsonPropertyName("matching_res")] public int MatchingRes { get; set; } diff --git a/ProjectLighthouse/Types/Match/Room.cs b/ProjectLighthouse/Types/Match/Room.cs new file mode 100644 index 00000000..213e565f --- /dev/null +++ b/ProjectLighthouse/Types/Match/Room.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using LBPUnion.ProjectLighthouse.Types.Levels; + +namespace LBPUnion.ProjectLighthouse.Types.Match +{ + public class Room + { + public int RoomId; + + public List Players; + public RoomState State; + public RoomSlot Slot; + + public bool IsInPod => Slot.SlotType == SlotType.Pod; + public bool IsLookingForPlayers => this.State == RoomState.DivingIntoLevel || this.State == RoomState.DivingInWaiting; + + public User Host => this.Players[0]; + + #nullable enable + public static bool operator ==(Room? room1, Room? room2) + { + if (ReferenceEquals(room1, room2)) return true; + if ((object?)room1 == null || (object?)room2 == null) return false; + + return room1.RoomId == room2.RoomId; + } + public static bool operator !=(Room? room1, Room? room2) => !(room1 == room2); + + public override int GetHashCode() => this.RoomId; + #nullable disable + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Match/RoomSlot.cs b/ProjectLighthouse/Types/Match/RoomSlot.cs new file mode 100644 index 00000000..438fa763 --- /dev/null +++ b/ProjectLighthouse/Types/Match/RoomSlot.cs @@ -0,0 +1,10 @@ +using LBPUnion.ProjectLighthouse.Types.Levels; + +namespace LBPUnion.ProjectLighthouse.Types.Match +{ + public class RoomSlot + { + public SlotType SlotType; + public int SlotId; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Match/RoomState.cs b/ProjectLighthouse/Types/Match/RoomState.cs index 2cfc28e2..f3d31406 100644 --- a/ProjectLighthouse/Types/Match/RoomState.cs +++ b/ProjectLighthouse/Types/Match/RoomState.cs @@ -2,10 +2,29 @@ namespace LBPUnion.ProjectLighthouse.Types.Match { public enum RoomState { + /// + /// The room isn't doing anything in particular. + /// Idle = 0, - LookingForPlayersForLevel = 1, + + /// + /// The room is looking to join an existing room playing a specific slot. + /// + DivingIntoLevel = 1, + + /// + /// ??? + /// Unknown = 2, + + /// + /// The room is looking for other rooms to join. + /// DivingIn = 3, + + /// + /// The room is waiting for players to join their room. + /// DivingInWaiting = 4, } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Match/UpdateMyPlayerData.cs b/ProjectLighthouse/Types/Match/UpdateMyPlayerData.cs index ff93dd2c..fde5b8e7 100644 --- a/ProjectLighthouse/Types/Match/UpdateMyPlayerData.cs +++ b/ProjectLighthouse/Types/Match/UpdateMyPlayerData.cs @@ -1,7 +1,10 @@ +#nullable enable namespace LBPUnion.ProjectLighthouse.Types.Match { public class UpdateMyPlayerData : IMatchData { - public string Player; + public string Player { get; set; } + + public RoomState? RoomState { get; set; } } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/User.cs b/ProjectLighthouse/Types/User.cs index b4c51d47..14de418d 100644 --- a/ProjectLighthouse/Types/User.cs +++ b/ProjectLighthouse/Types/User.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics.CodeAnalysis; using System.Linq; using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Types.Profiles; @@ -187,5 +188,25 @@ namespace LBPUnion.ProjectLighthouse.Types #endregion Slots + #nullable enable + public override bool Equals(object? obj) + { + if (obj is User user) return user.UserId == UserId; + + return false; + } + + [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse")] + public static bool operator ==(User? user1, User? user2) + { + if (ReferenceEquals(user1, user2)) return true; + if ((object?)user1 == null || (object?)user2 == null) return false; + + return user1.UserId == user2.UserId; + } + public static bool operator !=(User? user1, User? user2) => !(user1 == user2); + + public override int GetHashCode() => this.UserId; + #nullable disable } } \ No newline at end of file