1
0
mirror of synced 2025-02-16 10:52:34 +01:00

Stage Changes

This commit is contained in:
asesidaa 2024-03-15 16:59:22 +08:00
parent a88965bdd1
commit adb101261a
11 changed files with 264 additions and 201 deletions

View File

@ -35,7 +35,7 @@ namespace GameDatabase.Entities
public Difficulty AchievementDisplayDifficulty { get; set; }
public int AiWinCount { get; set; }
public List<Token> Tokens { get; set; } = new();
public uint[] UnlockedSongIdList { get; set; } = Array.Empty<uint>();
public List<uint> UnlockedSongIdList { get; set; } = [];
public bool IsAdmin { get; set; }
}
}

View File

@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>11</LangVersion>
<LangVersion>12</LangVersion>
</PropertyGroup>
<ItemGroup>

View File

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

View File

@ -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<UserDataController>
{
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<ServerSettings> 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<IActionResult> 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<IActionResult> GetUserData3209([FromBody] Models.v3209.UserDataRequest request)
{
Logger.LogInformation("UserData request : {Request}", request.Stringify());
var unlockedSongIdList = userData.UnlockedSongIdList;/*new List<uint>();
try
{
unlockedSongIdList = !string.IsNullOrEmpty(userData.UnlockedSongIdList)
? JsonSerializer.Deserialize<List<uint>>(userData.UnlockedSongIdList)
: new List<uint>();
}
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<uint>();
try
{
toneFlg = JsonSerializer.Deserialize<uint[]>(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<uint>();
try
{
titleFlg = JsonSerializer.Deserialize<uint[]>(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<uint>();
foreach (var id in recentSongs)
{
recentSet.Add(id);
if (recentSet.Count == 10)
{
break;
}
}
recentSongs = recentSet.ToArray();
var favoriteSongs = userData.FavoriteSongsArray;/*Array.Empty<uint>();
try
{
favoriteSongs = JsonSerializer.Deserialize<uint[]>(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);
}

View File

@ -30,6 +30,27 @@ public class VerifyQrCodeController : BaseController<VerifyQrCodeController>
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)
{

View File

@ -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<CommonSongPurchaseResponse>;
public class PurchaseSongCommandHandler(TaikoDbContext context, ILogger<PurchaseSongCommandHandler> logger)
: IRequestHandler<PurchaseSongCommand, CommonSongPurchaseResponse>
{
public async Task<CommonSongPurchaseResponse> 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 };
}
}

View File

@ -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<CommonUserDataResponse>;
public class UserDataQueryHandler(TaikoDbContext context, IGameDataService gameDataService, ILogger<UserDataQueryHandler> logger)
: IRequestHandler<UserDataQuery, CommonUserDataResponse>
{
public async Task<CommonUserDataResponse> 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<uint>();
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;
}
}

View File

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

View File

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

View File

@ -0,0 +1,8 @@
namespace TaikoLocalServer.Models.Application;
public class CommonSongPurchaseResponse
{
public uint Result { get; set; }
public int TokenCount { get; set; }
}

View File

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