using DeluxeBackend.Controllers.Auth; using DeluxeBackend.Models; using DeluxeBackend.Services; using Discord; using Discord.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using static DeluxeBackend.Enums; namespace DeluxeBackend.Controllers.Matchmaking { [Token(Token = "0x2000BB7")] public enum PLBMBHKLCHD { [Token(Token = "0x4002DF5")] UnknownError = -1, [Token(Token = "0x4002DF6")] Success, [Token(Token = "0x4002DF7")] NoSuchGame, [Token(Token = "0x4002DF8")] PlayerNotOnline, [Token(Token = "0x4002DF9")] InsufficientSpace, [Token(Token = "0x4002DFA")] EventNotStarted, [Token(Token = "0x4002DFB")] EventAlreadyFinished, [Token(Token = "0x4002DFC")] BlockedFromRoom = 7, [Token(Token = "0x4002DFD")] JuniorNotAllowed = 11, [Token(Token = "0x4002DFE")] Banned, [Token(Token = "0x4002DFF")] AlreadyInBestInstance, [Token(Token = "0x4002E00")] InsufficientRelationship, [Token(Token = "0x4002E01")] UpdateRequired = 16, [Token(Token = "0x4002E02")] AlreadyInTargetInstance, [Token(Token = "0x4002E03")] UGCNotAllowed = 19, [Token(Token = "0x4002E04")] NoSuchRoom, [Token(Token = "0x4002E05")] RoomIsNotActive = 22, [Token(Token = "0x4002E06")] RoomBlockedByCreator, [Token(Token = "0x4002E07")] RoomIsPrivate = 25, [Token(Token = "0x4002E08")] RoomInstanceIsPrivate, [Token(Token = "0x4002E09")] DeviceClassNotSupported = 30, [Token(Token = "0x4002E0A")] DeviceClassNotSupportedByRoomOwner, [Token(Token = "0x4002E0B")] MovementModeNotSupportedByRoomOwner, [Token(Token = "0x4002E0C")] EventIsPrivate = 35, [Token(Token = "0x4002E0D")] EventIsFull, [Token(Token = "0x4002E0E")] RoomInviteExpired = 40, [Token(Token = "0x4002E0F")] NoAvailableRegion = 45, [Token(Token = "0x4002E10")] NotorietyTooPoor = 50, [Token(Token = "0x4002E11")] BannedFromRoom = 55, [Token(Token = "0x4002E12")] NoSuchClub = 70, [Token(Token = "0x4002E13")] ClubHasNoClubhouse, [Token(Token = "0x4002E14")] ClubIsNotActive = 73, [Token(Token = "0x4002E15")] NotAMemberOfClub, [Token(Token = "0x4002E16")] BannedFromClub, [Token(Token = "0x4002E17")] InstanceJoinNotPermitted, [Token(Token = "0x4002E18")] LevelTooLow, [Token(Token = "0x4002E19")] ChatPartyInviteNotFound, [Token(Token = "0x4002E1A")] ChatPartyInviteModerated, [Token(Token = "0x4002E1B")] ChatMessageNotAnInvite, [Token(Token = "0x4002E1C")] DeveloperOnly, [Token(Token = "0x4002E1D")] RRPlusRequired, [Token(Token = "0x4002E1F")] MetaJuniorAccountRestriction, [Token(Token = "0x4002E1F")] NotExclusivelyLoggedIn, [Token(Token = "0x4002E20")] AccountDoesNotExist } public enum JoinType { [Token(Token = "0x4002DF1")] PublicMatchmaking, [Token(Token = "0x4002DF2")] PublicNewInstance, [Token(Token = "0x4002DF3")] PrivateNewInstance } public class JoinRoomRequestDto { [Required] public bool BypassMovementModeRestriction { get; set; } [Required] public int MaxPersistenceVersion { get; set; } [Required] public JoinType JoinMode { get; set; } public string? ClientJoinData { get; set; } } [Route("Matchmaking/matchmake")] [ApiController] [Authorize(Roles = "gameClient")] public class MatchmakeController(IJwtService jwt, ILiteDbService db, INotificationService ws) : ControllerBase { private static readonly object _statsLock = new(); [HttpPost("none")] public async Task None() { var login = await jwt.GetLoginWithInfo(User); if (login == null) return Unauthorized(); var presence = GetOrCreatePresence(login.Account); presence.Instance = null; presence.AppVersion = login.AppVersion; await SaveAndNotifyPresenceAsync(presence); return Ok(presence.ToDictionary()); } [HttpPost("dorm")] public async Task Dorm([FromForm, Required] JoinRoomRequestDto request) { var login = await jwt.GetLoginWithInfo(User); if (login == null) return Unauthorized(); var account = login.Account; var presence = GetOrCreatePresence(account); var room = db.Rooms.Include(x => x.Creator).FindOne(x => x.IsDorm == true && x.Creator.Id == account.Id); if (room == null) { var baseDorm = db.Rooms.Include(x => x.Creator).FindOne(x => x.IsDorm == true && x.Creator.Id == 1); if (baseDorm == null) { return Ok(new { errorCode = PLBMBHKLCHD.RoomIsNotActive }); } room = InitializeNewDormRoom(account, baseDorm); db.Rooms.Insert(room); CloneSubRooms(room, baseDorm, account, login); } if (room.Accessibility == RoomAccessibility.Private && !room.HasRole(account.Id, RoomRoleType.CoOwner)) { return Ok(new { errorCode = PLBMBHKLCHD.RoomIsPrivate }); } var subRooms = db.SubRooms.Include(x => x.Room).Find(x => x.Room.Id == room.Id && x.CanMatchmakeInto).ToList(); if (subRooms.Count == 0) { return Ok(new { errorCode = PLBMBHKLCHD.DeveloperOnly }); } var subRoom = subRooms[Random.Shared.Next(subRooms.Count)]; var privateInstances = db.RoomInstances .Include(x => x.SubRoom) .Include(x => x.SubRoom!.Room) .Include(x => x.SubRoom!.Room!.Creator) .Find(x => x.SubRoom.Id == subRoom.Id && x.InstanceType == Enums.RoomInstanceType.Dormroom && x.EventId == -1) .ToList(); var roomInstance = privateInstances.Count == 0 ? CreateNewInstance(subRoom, Enums.RoomInstanceType.Dormroom, true) : privateInstances[0]; return await ProcessPresenceTransitionAsync(presence, roomInstance, account, room, login.AppVersion); } [HttpPost("room/{RoomId}")] public async Task GoToRoom([FromRoute] long RoomId, [FromForm, Required] JoinRoomRequestDto request) { var login = await jwt.GetLoginWithInfo(User); if (login == null) return Unauthorized(); var room = db.Rooms.Include(x => x.Creator).FindOne(x => x.Id == RoomId); if (RoomId == 3 && DateTime.UtcNow.Month == 6) { room = db.Rooms.Include(x => x.Creator).FindOne(x => x.Name == "PrideCenter"); } if (room == null) return Ok(new { errorCode = PLBMBHKLCHD.NoSuchRoom }); var account = login.Account; if (room.Accessibility == RoomAccessibility.Private && !room.HasRole(account.Id, RoomRoleType.CoOwner)) { return Ok(new { errorCode = PLBMBHKLCHD.RoomIsPrivate }); } var subRooms = db.SubRooms.Include(x => x.Room).Find(x => x.Room.Id == room.Id && x.CanMatchmakeInto).ToList(); if (subRooms.Count == 0) return Ok(new { errorCode = PLBMBHKLCHD.DeveloperOnly }); var subRoom = subRooms[Random.Shared.Next(subRooms.Count)]; var presence = GetOrCreatePresence(account); var roomInstance = ResolveRoomInstance(subRoom, room.Accessibility, request.JoinMode); if (roomInstance == null) return Ok(new { errorCode = PLBMBHKLCHD.NoAvailableRegion }); return await ProcessPresenceTransitionAsync(presence, roomInstance, account, room, login.AppVersion); } [HttpPost("room/{RoomId}/{SubRoomId}")] public async Task GoToSubRoom([FromRoute] long RoomId, [FromRoute] long SubRoomId, [FromForm, Required] JoinRoomRequestDto request) { var login = await jwt.GetLoginWithInfo(User); if (login == null) return Unauthorized(); var room = db.Rooms.Include(x => x.Creator).FindOne(x => x.Id == RoomId); if (RoomId == 3 && DateTime.UtcNow.Month == 6) { room = db.Rooms.Include(x => x.Creator).FindOne(x => x.Name == "PrideCenter"); } if (room == null) return Ok(new { errorCode = PLBMBHKLCHD.NoSuchRoom }); var account = login.Account; if (room.Accessibility == RoomAccessibility.Private && !room.HasRole(account.Id, RoomRoleType.CoOwner)) { return Ok(new { errorCode = PLBMBHKLCHD.RoomIsPrivate }); } var subRoom = db.SubRooms.Include(x => x.Room).FindOne(x => x.Room.Id == room.Id && x.Id == SubRoomId); if (subRoom == null) return Ok(new { errorCode = PLBMBHKLCHD.NoSuchGame }); var presence = GetOrCreatePresence(account); var roomInstance = ResolveRoomInstance(subRoom, room.Accessibility, request.JoinMode); if (roomInstance == null) return Ok(new { errorCode = PLBMBHKLCHD.NoAvailableRegion }); return await ProcessPresenceTransitionAsync(presence, roomInstance, account, room, login.AppVersion); } [HttpPost("event/{EventId}")] public async Task GoToEvent([FromRoute] long EventId, [FromForm, Required] JoinRoomRequestDto request) { var login = await jwt.GetLoginWithInfo(User); if (login == null) return Unauthorized(); var playerEvent = db.PlayerEvents.Include(x => x.Room).Include(x => x.SubRoom).Include(x => x.Creator).FindById(EventId); if (playerEvent == null) return NotFound(); var room = db.Rooms.Include(x => x.Creator).FindOne(x => x.Id == playerEvent.Room.Id); if (room == null) return Ok(new { errorCode = PLBMBHKLCHD.NoSuchRoom }); var account = login.Account; if (room.Accessibility == RoomAccessibility.Private && !room.HasRole(account.Id, RoomRoleType.CoOwner)) { return Ok(new { errorCode = PLBMBHKLCHD.RoomIsPrivate }); } var subRoom = db.SubRooms.Include(x => x.Room).FindOne(x => x.Room.Id == room.Id && x.Id == playerEvent.SubRoom.Id); if (subRoom == null) return Ok(new { errorCode = PLBMBHKLCHD.NoSuchGame }); RoomInstance? roomInstance = null; if (playerEvent.IsMultiInstance) { var roomInstances = db.RoomInstances.Include(x => x.SubRoom) .Find(x => x.SubRoom.Id == playerEvent.SubRoom.Id && x.Id != playerEvent.BroadcastingRoomInstanceId && x.EventId == playerEvent.Id && !x.Private).ToList(); roomInstance = roomInstances.Count == 0 ? CreateNewInstance(subRoom, Enums.RoomInstanceType.MultiInstanceEvent, false, EventId) : roomInstances[Random.Shared.Next(roomInstances.Count)]; } if (roomInstance == null) return Ok(new { errorCode = PLBMBHKLCHD.NoAvailableRegion }); var presence = GetOrCreatePresence(account); return await ProcessPresenceTransitionAsync(presence, roomInstance, account, room, login.AppVersion); } [HttpPost("invite/{InviteId}")] public async Task GoToInvite([FromRoute] long InviteId) { var login = await jwt.GetLoginWithInfo(User); if (login == null) return Unauthorized(); var account = login.Account; var playerInvite = db.PlayerInvites.FindById(InviteId); if (playerInvite == null || playerInvite.Account.Id != account.Id || !playerInvite.IsValid) { return Ok(new { errorCode = PLBMBHKLCHD.RoomInviteExpired }); } var roomInstance = db.RoomInstances.Include(x => x.SubRoom).Include(x => x.SubRoom!.Room).Include(x => x.SubRoom!.Room!.Creator).FindById(playerInvite.InstanceId); if (roomInstance == null) return Ok(new { errorCode = PLBMBHKLCHD.NoAvailableRegion }); var presence = GetOrCreatePresence(account); return await ProcessPresenceTransitionAsync(presence, roomInstance, account, roomInstance.SubRoom.Room, login.AppVersion); } [HttpPost("instance/{instanceId}")] public async Task GoToInstance([FromRoute] long instanceId) { var login = await jwt.GetLoginWithInfo(User); if (login == null) return Unauthorized(); var roomInstance = db.RoomInstances.Include(x => x.SubRoom).Include(x => x.SubRoom!.Room).Include(x => x.SubRoom!.Room!.Creator).FindById(instanceId); if (roomInstance == null) return NotFound(); if (roomInstance.Private) return Ok(new { errorCode = PLBMBHKLCHD.RoomInstanceIsPrivate }); var account = login.Account; var presence = GetOrCreatePresence(account); return await ProcessPresenceTransitionAsync(presence, roomInstance, account, roomInstance.SubRoom.Room, login.AppVersion); } #region Helpers private AccountPresence GetOrCreatePresence(Account account) { var presence = db.Presences .Include(x => x.Account) .Include(x => x.Instance) .Include(x => x.Instance!.SubRoom) .Include(x => x.Instance!.SubRoom!.Room) .Include(x => x.Instance!.SubRoom!.Room!.Creator) .FindOne(x => x.Account.Id == account.Id); return presence ?? new AccountPresence { Account = account }; } private async Task SaveAndNotifyPresenceAsync(AccountPresence presence) { presence.LastOnline = DateTime.UtcNow; presence.IsOnline = true; db.Presences.Upsert(presence); await ws.SendToAllPlayer("PresenceUpdate", presence.ToDictionary()); } private RoomInstance CreateNewInstance(SubRoom subRoom, RoomInstanceType type, bool isPrivate, long eventId = -1) { var instance = new RoomInstance { SubRoom = subRoom, InstanceType = type, Private = isPrivate, PhotonRegion = "eu", PhotonRoom = Guid.NewGuid().ToString(), EventId = eventId, CreatedAt = DateTime.UtcNow }; db.RoomInstances.Insert(instance); return instance; } private RoomInstance? ResolveRoomInstance(SubRoom subRoom, RoomAccessibility accessibility, JoinType joinMode) { if (accessibility == RoomAccessibility.Private) { var privateInstances = db.RoomInstances.Include(x => x.SubRoom).Include(x => x.SubRoom!.Room) .Find(x => x.SubRoom.Id == subRoom.Id && x.InstanceType == Enums.RoomInstanceType.Private && x.EventId == -1).ToList(); return privateInstances.Count == 0 ? CreateNewInstance(subRoom, Enums.RoomInstanceType.Private, true) : privateInstances[0]; } return joinMode switch { JoinType.PublicMatchmaking => GetOrCreatePublicInstance(subRoom), JoinType.PublicNewInstance => CreateNewInstance(subRoom, Enums.RoomInstanceType.Public, false), JoinType.PrivateNewInstance => CreateNewInstance(subRoom, Enums.RoomInstanceType.Private, true), _ => null }; } private RoomInstance GetOrCreatePublicInstance(SubRoom subRoom) { var roomInstances = db.RoomInstances.Include(x => x.SubRoom).Include(x => x.SubRoom!.Room) .Find(x => x.SubRoom.Id == subRoom.Id && x.InstanceType == Enums.RoomInstanceType.Public && x.EventId == -1).ToList(); return roomInstances.Count == 0 ? CreateNewInstance(subRoom, Enums.RoomInstanceType.Public, false) : roomInstances[Random.Shared.Next(roomInstances.Count)]; } private async Task ProcessPresenceTransitionAsync(AccountPresence presence, RoomInstance targetInstance, Account account, Room room, string appVersion) { if (presence.Instance?.Id == targetInstance.Id) { return Ok(new { errorCode = PLBMBHKLCHD.AlreadyInTargetInstance }); } UpdateRoomStats(account, room); presence.Instance = targetInstance; presence.AppVersion = appVersion; await SaveAndNotifyPresenceAsync(presence); return Ok(new { errorCode = PLBMBHKLCHD.Success, roomInstance = targetInstance.ToDictionary() }); } private static Room InitializeNewDormRoom(Account account, Room baseDorm) { return new Room { Name = Guid.NewGuid().ToString(), IsDorm = true, SupportedPlayerTypes = baseDorm.SupportedPlayerTypes, Accessibility = RoomAccessibility.Private, ImageName = baseDorm.ImageName, Description = baseDorm.Description, Creator = account, CustomWarning = baseDorm.CustomWarning, WarningMask = baseDorm.WarningMask, DataBlob = baseDorm.DataBlob, DisableMicAutoMute = baseDorm.DisableMicAutoMute, DisableRoomComments = baseDorm.DisableRoomComments, EncryptVoiceChat = baseDorm.EncryptVoiceChat, MinLevel = baseDorm.MinLevel, PersistenceVersion = baseDorm.PersistenceVersion }; } private void CloneSubRooms(Room targetRoom, Room sourceRoom, Account account, LoginWithInfoResult login) { var sourceSubRooms = db.SubRooms.Include(x => x.Room).Include(x => x.CurrentSave).Find(x => x.Room.Id == sourceRoom.Id); foreach (var subRoom in sourceSubRooms) { var newSubRoom = new SubRoom { Room = targetRoom, LocationId = subRoom.LocationId, Name = subRoom.Name, IsSandbox = subRoom.IsSandbox, MaxPlayers = subRoom.MaxPlayers, CanMatchmakeInto = subRoom.CanMatchmakeInto, SupportsJoinInProgress = subRoom.SupportsJoinInProgress, UseLevelBasedMatchmaking = subRoom.UseLevelBasedMatchmaking, UseAgeBasedMatchmaking = subRoom.UseAgeBasedMatchmaking, UseRecRoyaleMatchmaking = subRoom.UseRecRoyaleMatchmaking }; db.SubRooms.Insert(newSubRoom); if (subRoom.CurrentSave != null) { var newSave = new SubRoomSave { SubRoomId = newSubRoom.Id, DataBlob = subRoom.CurrentSave.DataBlob, SavedByAccountId = account.Id, SavedOnPlatform = login.Platform, SavedOnDeviceClass = login.DeviceClass, Description = $"Cloned From ^{targetRoom.Name}", UnityAssetId = subRoom.CurrentSave.UnityAssetId, PersistenceVersion = subRoom.CurrentSave.PersistenceVersion }; db.SubRoomSave.Insert(newSave); newSubRoom.CurrentSave = newSave; db.SubRooms.Update(newSubRoom); } } } private void UpdateRoomStats(Account account, Room room) { lock (_statsLock) { if (!room.Stats.VisitorIds.Contains(account.Id)) { room.Stats.VisitorIds.Add(account.Id); } room.Stats.VisitCount += 1; db.Rooms.Update(room); account.VisitedRooms[room.Id] = DateTime.UtcNow; db.Accounts.Update(account); } } #endregion } }