diff --git a/GameDatabase/Entities/UserDatum.cs b/GameDatabase/Entities/UserDatum.cs index 490b80c..1f343cb 100644 --- a/GameDatabase/Entities/UserDatum.cs +++ b/GameDatabase/Entities/UserDatum.cs @@ -35,7 +35,7 @@ namespace GameDatabase.Entities public Difficulty AchievementDisplayDifficulty { get; set; } public int AiWinCount { get; set; } public List Tokens { get; set; } = new(); - public uint[] UnlockedSongIdList { get; set; } = Array.Empty(); + public List UnlockedSongIdList { get; set; } = []; public bool IsAdmin { get; set; } } } \ No newline at end of file diff --git a/GameDatabase/GameDatabase.csproj b/GameDatabase/GameDatabase.csproj index 15f82f7..9cb07e7 100644 --- a/GameDatabase/GameDatabase.csproj +++ b/GameDatabase/GameDatabase.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 11 + 12 diff --git a/TaikoLocalServer/Controllers/Game/SongPurchaseController.cs b/TaikoLocalServer/Controllers/Game/SongPurchaseController.cs index 605c81c..b555f89 100644 --- a/TaikoLocalServer/Controllers/Game/SongPurchaseController.cs +++ b/TaikoLocalServer/Controllers/Game/SongPurchaseController.cs @@ -1,57 +1,30 @@ -using System.Text.Json; -using Throw; +using TaikoLocalServer.Mappers; namespace TaikoLocalServer.Controllers.Game; -[Route("/v12r08_ww/chassis/songpurchase_wm2fh5bl.php")] [ApiController] public class SongPurchaseController : BaseController { - private readonly IUserDatumService userDatumService; - - public SongPurchaseController(IUserDatumService userDatumService) - { - this.userDatumService = userDatumService; - } - - [HttpPost] + [HttpPost("/v12r08_ww/chassis/songpurchase_wm2fh5bl.php")] [Produces("application/protobuf")] public async Task SongPurchase([FromBody] SongPurchaseRequest request) { Logger.LogInformation("SongPurchase request : {Request}", request.Stringify()); - var user = await userDatumService.GetFirstUserDatumOrNull(request.Baid); - user.ThrowIfNull($"User with baid {request.Baid} does not exist!"); - - Logger.LogInformation("Original UnlockedSongIdList: {UnlockedSongIdList}", user.UnlockedSongIdList); - - var unlockedSongIdList = user.UnlockedSongIdList.ToList(); - - var token = user.Tokens.FirstOrDefault(t => t.Id == request.TokenId); - if (token is not null && token.Count >= request.Price) - { - token.Count -= (int)request.Price; - } - else - { - Logger.LogError("User with baid {Baid} does not have enough tokens to purchase song with id {SongNo}!", request.Baid, request.SongNo); - return Ok(new SongPurchaseResponse { Result = 0 }); - } - - if (!unlockedSongIdList.Contains(request.SongNo)) unlockedSongIdList.Add(request.SongNo); - - user.UnlockedSongIdList = unlockedSongIdList.ToArray(); - - Logger.LogInformation("Updated UnlockedSongIdList: {UnlockedSongIdList}", user.UnlockedSongIdList); - - await userDatumService.UpdateUserDatum(user); - - var response = new SongPurchaseResponse - { - Result = 1, - TokenCount = token.Count - }; + var commonResponse = await Mediator.Send(SongPurchaseMappers.MapToCommand(request)); + var response = SongPurchaseMappers.MapTo3906(commonResponse); + + return Ok(response); + } + + [HttpPost("/v12r00_cn/chassis/songpurchase.php")] + [Produces("application/protobuf")] + public async Task SongPurchase3209([FromBody] Models.v3209.SongPurchaseRequest request) + { + Logger.LogInformation("SongPurchase request : {Request}", request.Stringify()); + var commonResponse = await Mediator.Send(SongPurchaseMappers.MapToCommand(request)); + var response = SongPurchaseMappers.MapTo3209(commonResponse); return Ok(response); } } \ No newline at end of file diff --git a/TaikoLocalServer/Controllers/Game/UserDataController.cs b/TaikoLocalServer/Controllers/Game/UserDataController.cs index 0970199..cf3c06b 100644 --- a/TaikoLocalServer/Controllers/Game/UserDataController.cs +++ b/TaikoLocalServer/Controllers/Game/UserDataController.cs @@ -1,178 +1,36 @@ using Microsoft.Extensions.Options; using System.Buffers.Binary; using System.Text.Json; +using TaikoLocalServer.Handlers; +using TaikoLocalServer.Mappers; using TaikoLocalServer.Settings; using Throw; namespace TaikoLocalServer.Controllers.Game; -[Route("/v12r08_ww/chassis/userdata_gc6x17o8.php")] [ApiController] public class UserDataController : BaseController { - private readonly IUserDatumService userDatumService; - - private readonly ISongPlayDatumService songPlayDatumService; - - private readonly IGameDataService gameDataService; - - private readonly ServerSettings settings; - - public UserDataController(IUserDatumService userDatumService, ISongPlayDatumService songPlayDatumService, - IGameDataService gameDataService, IOptions settings) - { - this.userDatumService = userDatumService; - this.songPlayDatumService = songPlayDatumService; - this.gameDataService = gameDataService; - this.settings = settings.Value; - } - - [HttpPost] + [HttpPost("/v12r08_ww/chassis/userdata_gc6x17o8.php")] [Produces("application/protobuf")] public async Task GetUserData([FromBody] UserDataRequest request) { Logger.LogInformation("UserData request : {Request}", request.Stringify()); - var songIdMax = settings.EnableMoreSongs ? Constants.MUSIC_ID_MAX_EXPANDED : Constants.MUSIC_ID_MAX; + var commonResponse = await Mediator.Send(new UserDataQuery(request.Baid)); + var response = UserDataMappers.MapTo3906(commonResponse); - var userData = await userDatumService.GetFirstUserDatumOrDefault(request.Baid); + return Ok(response); + } + + [HttpPost("/v12r00_cn/chassis/userdata.php")] + [Produces("application/protobuf")] + public async Task GetUserData3209([FromBody] Models.v3209.UserDataRequest request) + { + Logger.LogInformation("UserData request : {Request}", request.Stringify()); - var unlockedSongIdList = userData.UnlockedSongIdList;/*new List(); - try - { - unlockedSongIdList = !string.IsNullOrEmpty(userData.UnlockedSongIdList) - ? JsonSerializer.Deserialize>(userData.UnlockedSongIdList) - : new List(); - } - catch (JsonException e) - { - Logger.LogError(e, "Parsing UnlockedSongIdList data for user with baid {Request} failed!", request.Baid); - } - - unlockedSongIdList.ThrowIfNull("UnlockedSongIdList should never be null");*/ - - var musicList = gameDataService.GetMusicList(); - var lockedSongsList = gameDataService.GetLockedSongsList(); - lockedSongsList = lockedSongsList.Except(unlockedSongIdList).ToList(); - var enabledMusicList = musicList.Except(lockedSongsList); - var releaseSongArray = - FlagCalculator.GetBitArrayFromIds(enabledMusicList, songIdMax, Logger); - - var defaultSongWithUraList = gameDataService.GetMusicWithUraList(); - var enabledUraMusicList = defaultSongWithUraList.Except(lockedSongsList); - var uraSongArray = - FlagCalculator.GetBitArrayFromIds(enabledUraMusicList, songIdMax, Logger); - - var toneFlg = userData.ToneFlgArray;/*Array.Empty(); - try - { - toneFlg = JsonSerializer.Deserialize(userData.ToneFlgArray); - } - catch (JsonException e) - { - Logger.LogError(e, "Parsing tone flg json data failed"); - } - - // The only way to get a null is provide string "null" as input, - // which means database content need to be fixed, so better throw - toneFlg.ThrowIfNull("Tone flg should never be null!");*/ - - // If toneFlg is empty, add 0 to it - if (toneFlg.Length == 0) - { - toneFlg = new uint[] { 0 }; - userData.ToneFlgArray = toneFlg;//JsonSerializer.Serialize(toneFlg); - await userDatumService.UpdateUserDatum(userData); - } - - var toneArray = FlagCalculator.GetBitArrayFromIds(toneFlg, gameDataService.GetToneFlagArraySize(), Logger); - - var titleFlg = userData.TitleFlgArray;/*Array.Empty(); - try - { - titleFlg = JsonSerializer.Deserialize(userData.TitleFlgArray); - } - catch (JsonException e) - { - Logger.LogError(e, "Parsing title flg json data failed"); - } - - // The only way to get a null is provide string "null" as input, - // which means database content need to be fixed, so better throw - titleFlg.ThrowIfNull("Title flg should never be null!");*/ - - var titleArray = FlagCalculator.GetBitArrayFromIds(titleFlg, gameDataService.GetTitleFlagArraySize(), Logger); - - var recentSongs = (await songPlayDatumService.GetSongPlayDatumByBaid(request.Baid)) - .AsEnumerable() - .OrderByDescending(datum => datum.PlayTime) - .ThenByDescending(datum => datum.SongNumber) - .Select(datum => datum.SongId) - .ToArray(); - - // Use custom implementation as distinctby cannot guarantee preserved element - var recentSet = new OrderedSet(); - foreach (var id in recentSongs) - { - recentSet.Add(id); - if (recentSet.Count == 10) - { - break; - } - } - - recentSongs = recentSet.ToArray(); - - var favoriteSongs = userData.FavoriteSongsArray;/*Array.Empty(); - try - { - favoriteSongs = JsonSerializer.Deserialize(userData.FavoriteSongsArray); - } - catch (JsonException e) - { - Logger.LogError(e, "Parsing favorite songs json data failed"); - } - - // The only way to get a null is provide string "null" as input, - // which means database content need to be fixed, so better throw - favoriteSongs.ThrowIfNull("Favorite song should never be null!");*/ - - var defaultOptions = new byte[2]; - BinaryPrimitives.WriteInt16LittleEndian(defaultOptions, userData.OptionSetting); - - var difficultySettingArray = JsonHelper.GetUIntArrayFromJson(userData.DifficultySettingArray, 3, Logger, nameof(userData.DifficultySettingArray)); - for (int i = 0; i < 3; i++) - { - if (difficultySettingArray[i] >= 2) - { - difficultySettingArray[i] -= 1; - } - } - - var difficultyPlayedArray = JsonHelper.GetUIntArrayFromJson(userData.DifficultyPlayedArray, 3, Logger, nameof(userData.DifficultyPlayedArray)); - - var response = new UserDataResponse - { - Result = 1, - ToneFlg = toneArray, - TitleFlg = titleArray, - ReleaseSongFlg = releaseSongArray, - UraReleaseSongFlg = uraSongArray, - AryFavoriteSongNoes = favoriteSongs, - AryRecentSongNoes = recentSongs, - DefaultOptionSetting = defaultOptions, - NotesPosition = userData.NotesPosition, - IsVoiceOn = userData.IsVoiceOn, - IsSkipOn = userData.IsSkipOn, - DifficultySettingCourse = difficultySettingArray[0], - DifficultySettingStar = difficultySettingArray[1], - DifficultySettingSort = difficultySettingArray[2], - DifficultyPlayedCourse = difficultyPlayedArray[0], - DifficultyPlayedStar = difficultyPlayedArray[1], - DifficultyPlayedSort = difficultyPlayedArray[2], - IsChallengecompe = false, - SongRecentCnt = (uint)recentSongs.Length - }; + var commonResponse = await Mediator.Send(new UserDataQuery((uint)request.Baid)); + var response = UserDataMappers.MapTo3209(commonResponse); return Ok(response); } diff --git a/TaikoLocalServer/Controllers/Game/VerifyQrCodeController.cs b/TaikoLocalServer/Controllers/Game/VerifyQrCodeController.cs index 1dc40d5..827d055 100644 --- a/TaikoLocalServer/Controllers/Game/VerifyQrCodeController.cs +++ b/TaikoLocalServer/Controllers/Game/VerifyQrCodeController.cs @@ -30,6 +30,27 @@ public class VerifyQrCodeController : BaseController return Ok(response); } + + [HttpPost("/v12r00_cn/chassis/verifyqrcode.php")] + [Produces("application/protobuf")] + public IActionResult VerifyQrCode3209([FromBody] Models.v3209.VerifyQrcodeRequest request) + { + Logger.LogInformation("VerifyQrCode request : {Request}", request.Stringify()); + + var qrCodeId = VerifyQr(request.QrcodeSerial); + var response = new Models.v3209.VerifyQrcodeResponse + { + Result = 1, + QrcodeId = (uint)qrCodeId + }; + + if (qrCodeId == -1) + { + response.Result = 51; + } + + return Ok(response); + } private int VerifyQr(string serial) { diff --git a/TaikoLocalServer/Handlers/PurchaseSongCommand.cs b/TaikoLocalServer/Handlers/PurchaseSongCommand.cs new file mode 100644 index 0000000..31e0981 --- /dev/null +++ b/TaikoLocalServer/Handlers/PurchaseSongCommand.cs @@ -0,0 +1,41 @@ +using GameDatabase.Context; +using TaikoLocalServer.Models.Application; +using Throw; + +namespace TaikoLocalServer.Handlers; + +public record PurchaseSongCommand(uint Baid, uint SongNo, uint TokenId, uint Price) : IRequest; + +public class PurchaseSongCommandHandler(TaikoDbContext context, ILogger logger) + : IRequestHandler +{ + + public async Task Handle(PurchaseSongCommand request, CancellationToken cancellationToken) + { + var user = await context.UserData + .Include(u => u.Tokens) + .FirstOrDefaultAsync(u => u.Baid == request.Baid, cancellationToken); + user.ThrowIfNull($"User with baid {request.Baid} does not exist!"); + if (user.UnlockedSongIdList.Contains(request.SongNo)) + { + logger.LogWarning("User with baid {Baid} already has song with id {SongNo} unlocked!", request.Baid, request.SongNo); + return new CommonSongPurchaseResponse { Result = 0 }; + } + + var token = user.Tokens.FirstOrDefault(t => t.Id == request.TokenId); + if (token is not null && token.Count >= request.Price) + { + token.Count -= (int)request.Price; + } + else + { + logger.LogError("User with baid {Baid} does not have enough tokens to purchase song with id {SongNo}!", request.Baid, request.SongNo); + return new CommonSongPurchaseResponse { Result = 0 }; + } + + user.UnlockedSongIdList.Add(request.SongNo); + context.UserData.Update(user); + await context.SaveChangesAsync(cancellationToken); + return new CommonSongPurchaseResponse { Result = 1, TokenCount = token.Count }; + } +} \ No newline at end of file diff --git a/TaikoLocalServer/Handlers/UserDataQuery.cs b/TaikoLocalServer/Handlers/UserDataQuery.cs new file mode 100644 index 0000000..101d60a --- /dev/null +++ b/TaikoLocalServer/Handlers/UserDataQuery.cs @@ -0,0 +1,104 @@ +using System.Buffers.Binary; +using GameDatabase.Context; +using TaikoLocalServer.Models.Application; +using Throw; + +namespace TaikoLocalServer.Handlers; + +public record UserDataQuery(uint Baid) : IRequest; + +public class UserDataQueryHandler(TaikoDbContext context, IGameDataService gameDataService, ILogger logger) + : IRequestHandler +{ + + public async Task Handle(UserDataQuery request, CancellationToken cancellationToken) + { + var userData = await context.UserData.FindAsync(request.Baid, cancellationToken); + userData.ThrowIfNull($"User not found for Baid {request.Baid}!"); + + var unlockedSongIdList = userData.UnlockedSongIdList; + + var musicList = gameDataService.GetMusicList(); + var lockedSongsList = gameDataService.GetLockedSongsList(); + lockedSongsList = lockedSongsList.Except(unlockedSongIdList).ToList(); + var enabledMusicList = musicList.Except(lockedSongsList); + var releaseSongArray = + FlagCalculator.GetBitArrayFromIds(enabledMusicList, Constants.MUSIC_ID_MAX, logger); + + var defaultSongWithUraList = gameDataService.GetMusicWithUraList(); + var enabledUraMusicList = defaultSongWithUraList.Except(lockedSongsList); + var uraSongArray = + FlagCalculator.GetBitArrayFromIds(enabledUraMusicList, Constants.MUSIC_ID_MAX, logger); + + if (userData.ToneFlgArray.Length == 0) + { + userData.ToneFlgArray = [0]; + context.UserData.Update(userData); + await context.SaveChangesAsync(cancellationToken); + } + + var toneArray = FlagCalculator.GetBitArrayFromIds(userData.ToneFlgArray, gameDataService.GetToneFlagArraySize(), logger); + + var titleArray = FlagCalculator.GetBitArrayFromIds(userData.TitleFlgArray, gameDataService.GetTitleFlagArraySize(), logger); + + var recentSongs = await context.SongPlayData + .Where(datum => datum.Baid == request.Baid) + .OrderByDescending(datum => datum.PlayTime) + .ThenByDescending(datum => datum.SongNumber) + .Select(datum => datum.SongId) + .ToArrayAsync(cancellationToken); + + // Use custom implementation as distinctby cannot guarantee preserved element + var recentSet = new OrderedSet(); + foreach (var id in recentSongs) + { + recentSet.Add(id); + if (recentSet.Count == 10) + { + break; + } + } + + recentSongs = recentSet.ToArray(); + + var defaultOptions = new byte[2]; + BinaryPrimitives.WriteInt16LittleEndian(defaultOptions, userData.OptionSetting); + + var difficultySettingArray = JsonHelper.GetUIntArrayFromJson(userData.DifficultySettingArray, 3, logger, nameof(userData.DifficultySettingArray)); + for (int i = 0; i < 3; i++) + { + if (difficultySettingArray[i] >= 2) + { + difficultySettingArray[i] -= 1; + } + } + + var difficultyPlayedArray = JsonHelper.GetUIntArrayFromJson(userData.DifficultyPlayedArray, 3, logger, nameof(userData.DifficultyPlayedArray)); + + var response = new CommonUserDataResponse + { + Result = 1, + ToneFlg = toneArray, + TitleFlg = titleArray, + ReleaseSongFlg = releaseSongArray, + UraReleaseSongFlg = uraSongArray, + AryFavoriteSongNoes = userData.FavoriteSongsArray, + AryRecentSongNoes = recentSongs, + DefaultOptionSetting = defaultOptions, + NotesPosition = userData.NotesPosition, + IsVoiceOn = userData.IsVoiceOn, + IsSkipOn = userData.IsSkipOn, + DifficultySettingCourse = difficultySettingArray[0], + DifficultySettingStar = difficultySettingArray[1], + DifficultySettingSort = difficultySettingArray[2], + DifficultyPlayedCourse = difficultyPlayedArray[0], + DifficultyPlayedStar = difficultyPlayedArray[1], + DifficultyPlayedSort = difficultyPlayedArray[2], + SongRecentCnt = (uint)recentSongs.Length, + IsChallengecompe = false, + // TODO: Other fields + }; + + return response; + } +} \ No newline at end of file diff --git a/TaikoLocalServer/Mappers/SongPurchaseMappers.cs b/TaikoLocalServer/Mappers/SongPurchaseMappers.cs new file mode 100644 index 0000000..d623676 --- /dev/null +++ b/TaikoLocalServer/Mappers/SongPurchaseMappers.cs @@ -0,0 +1,17 @@ +using Riok.Mapperly.Abstractions; +using TaikoLocalServer.Handlers; +using TaikoLocalServer.Models.Application; + +namespace TaikoLocalServer.Mappers; + +[Mapper] +public static partial class SongPurchaseMappers +{ + public static partial SongPurchaseResponse MapTo3906(CommonSongPurchaseResponse response); + + public static partial Models.v3209.SongPurchaseResponse MapTo3209(CommonSongPurchaseResponse response); + + public static partial PurchaseSongCommand MapToCommand(SongPurchaseRequest request); + + public static partial PurchaseSongCommand MapToCommand(Models.v3209.SongPurchaseRequest request); +} \ No newline at end of file diff --git a/TaikoLocalServer/Mappers/UserDataMappers.cs b/TaikoLocalServer/Mappers/UserDataMappers.cs new file mode 100644 index 0000000..29f5821 --- /dev/null +++ b/TaikoLocalServer/Mappers/UserDataMappers.cs @@ -0,0 +1,12 @@ +using Riok.Mapperly.Abstractions; +using TaikoLocalServer.Models.Application; + +namespace TaikoLocalServer.Mappers; + +[Mapper] +public static partial class UserDataMappers +{ + public static partial UserDataResponse MapTo3906(CommonUserDataResponse response); + + public static partial Models.v3209.UserDataResponse MapTo3209(CommonUserDataResponse response); +} \ No newline at end of file diff --git a/TaikoLocalServer/Models/Application/CommonSongPurchaseResponse.cs b/TaikoLocalServer/Models/Application/CommonSongPurchaseResponse.cs new file mode 100644 index 0000000..89ec9fe --- /dev/null +++ b/TaikoLocalServer/Models/Application/CommonSongPurchaseResponse.cs @@ -0,0 +1,8 @@ +namespace TaikoLocalServer.Models.Application; + +public class CommonSongPurchaseResponse +{ + public uint Result { get; set; } + + public int TokenCount { get; set; } +} \ No newline at end of file diff --git a/TaikoLocalServer/Models/Application/CommonUserDataResponse.cs b/TaikoLocalServer/Models/Application/CommonUserDataResponse.cs new file mode 100644 index 0000000..4df06eb --- /dev/null +++ b/TaikoLocalServer/Models/Application/CommonUserDataResponse.cs @@ -0,0 +1,29 @@ +namespace TaikoLocalServer.Models.Application; + +public class CommonUserDataResponse +{ + public uint Result { get; set; } + public byte[] ToneFlg { get; set; } = []; + public byte[] TitleFlg { get; set; } = []; + public byte[] ReleaseSongFlg { get; set; } = []; + public byte[] UraReleaseSongFlg { get; set; } = []; + public uint[] AryFavoriteSongNoes { get; set; } = []; + public uint[] AryRecentSongNoes { get; set; } = []; + public uint DispScoreType { get; set; } + public uint DispLevelChassis { get; set; } + public uint DispLevelSelf { get; set; } + public bool IsDispTojiruOn { get; set; } + public byte[] DefaultOptionSetting { get; set; } = []; + public int NotesPosition { get; set; } + public bool IsVoiceOn { get; set; } + public bool IsSkipOn { get; set; } + public uint DifficultySettingCourse { get; set; } + public uint DifficultySettingStar { get; set; } + public uint DifficultySettingSort { get; set; } + public uint DifficultyPlayedCourse { get; set; } + public uint DifficultyPlayedStar { get; set; } + public uint DifficultyPlayedSort { get; set; } + public uint TotalCreditCnt { get; set; } + public uint SongRecentCnt { get; set; } + public bool IsChallengecompe { get; set; } +} \ No newline at end of file