Files
2026-07-23 18:21:43 -07:00

458 lines
20 KiB
C#

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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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
}
}