1
0
mirror of synced 2024-11-30 17:24:33 +01:00

Merge remote-tracking branch 'origin/master' into AIBattle

This commit is contained in:
shiibe 2022-09-16 09:53:32 -04:00
commit 2dffe001e0
48 changed files with 2085 additions and 28996 deletions

View File

@ -12,7 +12,10 @@ This is a server for Taiko no Tatsujin Nijiiro ver 08.18
1. Download the server from release page, extract anywhere 1. Download the server from release page, extract anywhere
2. Modify hosts, add the following entries: 2. From game's `Data\x64\datatable` folder, find `music_attribute.bin`, `musicinfo.bin`, `music_order.bin` and `wordlist.bin`, decompress them, add `.json` prefix to them.
The result is `music_attribute.json`, `musicinfo.json`, `music_order.json` and `wordlist.json`. Put the json files under` wwwroot/data` folder in server.
3. Modify hosts, add the following entries:
``` ```
server.ip tenporouter.loc server.ip tenporouter.loc
@ -23,23 +26,40 @@ This is a server for Taiko no Tatsujin Nijiiro ver 08.18
where `server.ip` is your computers ip (or the server's ip) where `server.ip` is your computers ip (or the server's ip)
3. Setup Apache as reverse proxy. Notice the following assumes a windows install, the server also works on Linux, but the guide only covers windows. 4. Setup Apache as reverse proxy. Notice the following assumes a windows install, the server also works on Linux, but the guide only covers windows.
1. Download [Apache](https://www.apachelounge.com/download/), extract anywhere 1. Download [Apache](https://www.apachelounge.com/download/), extract anywhere
2. Copy the content in Apache folder to Apache root folder (and replace) 2. **Copy the content in release rar's Apache folder to installed Apache root folder (and replace, which includes httpd.conf and httpd-vhosts.conf, if no prompt to replace files, you are extracting to wrong folder)**
3. Open `conf/httpd.conf` (under installed Apache folder), find this line (line 37 by default), modify it to your Apache install (extracted) full path
```htaccess
# For example, if your Apache is extracted to C:\users\username\Apache24, then this should be "c:/users/username/Apache24"
Define SRVROOT "d:/Projects/Apache24"
```
4. Open the certs folder Apache root folder, then click on the localhost.crt file and import it to trusted root store.
5. Open the certs folder Apache root folder, then click on the localhost.crt file and import it to trusted root store.
If everything is correct, run bin/httpd.exe, a command prompt will open (and stay open, if it shut down, probably something is not setup correctly) If everything is correct, run bin/httpd.exe, a command prompt will open (and stay open, if it shut down, probably something is not setup correctly)
4. Now run the server, if everything is setup correctly, visit http://localhost:5000, you should be able to see the web ui up and running without errors.
5. Go to game folder, copy the config files (AMConfig.ini and WritableConfig.ini) in the config folder from server release to AMCUS folder and replace the original ones. 5. Now run the server, if everything is setup correctly, visit http://localhost:5000, you should be able to see the web ui up and running without errors. (If you encounter errors in web ui for the first time, try visit https://naominet.jp:10122/)
6. Open command prompt as admin, navigate to game root folder (where init.ps1 is). Run `regsvr32 .\AMCUS\iauthdll.dll`. It should prompt about success. 6. Go to game folder, copy the config files (AMConfig.ini and WritableConfig.ini) in the AMCUS folder from server release to AMCUS folder and replace the original ones.
7. Run AMCUS/AMAuthd.exe, then run AMCUS/AMUpdater.exe. If the updater run and exits without issue, you are ready to run the game and connect to server. 7. Open command prompt as admin, navigate to game root folder (where init.ps1 is). Run `regsvr32 .\AMCUS\iauthdll.dll`. It should prompt about success.
8. Run the game, it should now connect to the server. 8. Run AMCUS/AMAuthd.exe, then run AMCUS/AMUpdater.exe. If the updater run and exits without issue, you are ready to run the game and connect to server.
9. Run the game, it should now connect to the server.
### Run the server on another computer
If you want to run the server on another computer, the procedure is almost identical.
Before you open browser, in `wwwroot/appsettings.json`, change `BaseUrl` to `https://naominet.jp:10122` then instead of visit localhost, visit the server using domain name to test.
Also note that now the cetificate also need to be imported on client computer, or web ui may not work. If you don't need https, change `BaseUrl` to `http://server.ip:80`, and visit on client. The game does not care about certificate.

View File

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace SharedProject.Models;
public class SongIntroductionData
{
[JsonPropertyName("setId")]
public uint SetId { get; set; }
[JsonPropertyName("verupNo")]
public uint VerupNo { get; set; }
[JsonPropertyName("mainSongNo")]
public uint MainSongNo { get; set; }
[JsonPropertyName("subSongNo")]
public uint[]? SubSongNo { get; set; }
}

View File

@ -20,9 +20,36 @@ public class UserSetting
public int NotesPosition { get; set; } public int NotesPosition { get; set; }
public string MyDonName { get; set; } = String.Empty; public string MyDonName { get; set; } = string.Empty;
public string Title { get; set; } = String.Empty; public string Title { get; set; } = string.Empty;
public uint TitlePlateId { get; set; } public uint TitlePlateId { get; set; }
public uint Kigurumi { get; set; }
public uint Head { get; set; }
public uint Body { get; set; }
public uint Face { get; set; }
public uint Puchi { get; set; }
public List<uint> UnlockedKigurumi { get; set; } = new();
public List<uint> UnlockedHead { get; set; } = new();
public List<uint> UnlockedBody { get; set; } = new();
public List<uint> UnlockedFace { get; set; } = new();
public List<uint> UnlockedPuchi { get; set; } = new();
public uint FaceColor { get; set; }
public uint BodyColor { get; set; }
public uint LimbColor { get; set; }
} }

View File

@ -1,4 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Kigurumi/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=musicinfo/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=musicinfo/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Namco/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Namco/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Puchi/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Vocaloid/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=Vocaloid/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

1
TaikoLocalServer/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
wwwroot/data/music_attribute.json

View File

@ -4,9 +4,7 @@ public static class Constants
{ {
public const string DATE_TIME_FORMAT = "yyyyMMddHHmmss"; public const string DATE_TIME_FORMAT = "yyyyMMddHHmmss";
public const int MUSIC_ID_MAX = 1600; public const int MUSIC_ID_MAX = 9000;
public const int MUSIC_FLAG_ARRAY_SIZE = MUSIC_ID_MAX / 8 + 1;
public const int CROWN_FLAG_ARRAY_SIZE = MUSIC_ID_MAX + 1; public const int CROWN_FLAG_ARRAY_SIZE = MUSIC_ID_MAX + 1;
@ -24,7 +22,27 @@ public static class Constants
public const string DAN_DATA_FILE_NAME = "dan_data.json"; public const string DAN_DATA_FILE_NAME = "dan_data.json";
public const string INTRO_DATA_FILE_NAME = "intro_data.json";
public const int MIN_DAN_ID = 1; public const int MIN_DAN_ID = 1;
public const int MAX_DAN_ID = 19; public const int MAX_DAN_ID = 19;
public const int GOT_DAN_BITS = MAX_DAN_ID * 4;
public const int TONE_UID_MAX = 19;
public const int TITLE_UID_MAX = 814;
private const int COSTUME_FLAG_1_ARRAY_SIZE = 154;
private const int COSTUME_FLAG_2_ARRAY_SIZE = 140;
private const int COSTUME_FLAG_3_ARRAY_SIZE = 156;
private const int COSTUME_FLAG_4_ARRAY_SIZE = 58;
private const int COSTUME_FLAG_5_ARRAY_SIZE = 129;
public static readonly int[] CostumeFlagArraySizes =
{
COSTUME_FLAG_1_ARRAY_SIZE,
COSTUME_FLAG_2_ARRAY_SIZE,
COSTUME_FLAG_3_ARRAY_SIZE,
COSTUME_FLAG_4_ARRAY_SIZE,
COSTUME_FLAG_5_ARRAY_SIZE
};
} }

View File

@ -1,48 +0,0 @@
using System.Collections.Immutable;
using System.Text.Json;
using SharedProject.Models;
using Swan.Mapping;
namespace TaikoLocalServer.Common.Utils;
public class DanOdaiDataManager
{
public ImmutableDictionary<uint, GetDanOdaiResponse.OdaiData> OdaiDataList { get; }
static DanOdaiDataManager() {}
private DanOdaiDataManager()
{
var dataPath = PathHelper.GetDataPath();
var filePath = Path.Combine(dataPath, Constants.DAN_DATA_FILE_NAME);
var jsonString = File.ReadAllText(filePath);
var result = JsonSerializer.Deserialize<List<DanData>>(jsonString);
if (result is null)
{
throw new ApplicationException("Cannot parse dan data json!");
}
OdaiDataList = result.ToImmutableDictionary(data => data.DanId, ToResponseOdaiData);
}
private GetDanOdaiResponse.OdaiData ToResponseOdaiData(DanData data)
{
var responseOdaiData = new GetDanOdaiResponse.OdaiData
{
DanId = data.DanId,
Title = data.Title,
VerupNo = data.VerupNo
};
var odaiSongs = data.OdaiSongList.Select(song => song.CopyPropertiesToNew<GetDanOdaiResponse.OdaiData.OdaiSong>());
responseOdaiData.AryOdaiSongs.AddRange(odaiSongs);
var odaiBorders = data.OdaiBorderList.Select(border => border.CopyPropertiesToNew<GetDanOdaiResponse.OdaiData.OdaiBorder>());
responseOdaiData.AryOdaiBorders.AddRange(odaiBorders);
return responseOdaiData;
}
public static DanOdaiDataManager Instance { get; } = new();
}

View File

@ -1,4 +1,5 @@
using System.Collections.Specialized; using System.Collections;
using System.Collections.Specialized;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace TaikoLocalServer.Common.Utils; namespace TaikoLocalServer.Common.Utils;
@ -106,4 +107,22 @@ public static class FlagCalculator
gotDanFlagList.Add(gotDanFlag.Data); gotDanFlagList.Add(gotDanFlag.Data);
return MemoryMarshal.AsBytes(new ReadOnlySpan<int>(gotDanFlagList.ToArray())).ToArray(); return MemoryMarshal.AsBytes(new ReadOnlySpan<int>(gotDanFlagList.ToArray())).ToArray();
} }
public static byte[] GetBitArrayFromIds(IEnumerable<uint> idArray, int bitArraySize, ILogger logger)
{
var result = new byte[bitArraySize / 8 + 1];
var bitSet = new BitArray(bitArraySize + 1);
foreach (var id in idArray)
{
if (id >= bitArraySize)
{
logger.LogWarning("Id out of range!");
continue;
}
bitSet.Set((int)id, true);
}
bitSet.CopyTo(result, 0);
return result;
}
} }

View File

@ -0,0 +1,52 @@
using System.Text.Json;
namespace TaikoLocalServer.Common.Utils;
public static class JsonHelper
{
public static List<uint> GetCostumeDataFromUserData(UserDatum userData, ILogger logger)
{
var costumeData = new List<uint> { 0, 0, 0, 0, 0 };
try
{
costumeData = JsonSerializer.Deserialize<List<uint>>(userData.CostumeData);
}
catch (JsonException e)
{
logger.LogError(e, "Parsing costume json data failed");
}
if (costumeData != null && costumeData.Count >= 5)
{
return costumeData;
}
logger.LogWarning("Costume data is null or count less than 5!");
costumeData = new List<uint> { 0, 0, 0, 0, 0 };
return costumeData;
}
public static List<List<uint>> GetCostumeUnlockDataFromUserData(UserDatum userData, ILogger logger)
{
var costumeUnlockData = new List<List<uint>> { new(), new(), new(), new(), new() };
try
{
costumeUnlockData = JsonSerializer.Deserialize<List<List<uint>>>(userData.CostumeFlgArray);
}
catch (JsonException e)
{
logger.LogError(e, "Parsing costume json data failed");
}
if (costumeUnlockData != null && costumeUnlockData.Count >= 5)
{
return costumeUnlockData;
}
logger.LogWarning("Costume unlock data is null or count less than 5!");
costumeUnlockData = new List<List<uint>> { new(), new(), new(), new(), new() };
return costumeUnlockData;
}
}

View File

@ -1,42 +0,0 @@
using System.Text.Json;
namespace TaikoLocalServer.Common.Utils;
public class MusicAttributeManager
{
public readonly Dictionary<uint,MusicAttribute> MusicAttributes;
static MusicAttributeManager()
{
}
private MusicAttributeManager()
{
var dataPath = PathHelper.GetDataPath();
var filePath = Path.Combine(dataPath, Constants.MUSIC_ATTRIBUTE_FILE_NAME);
var jsonString = File.ReadAllText(filePath);
var result = JsonSerializer.Deserialize<List<MusicAttribute>>(jsonString);
if (result is null)
{
throw new ApplicationException("Cannot parse music attribute json!");
}
MusicAttributes = result.ToDictionary(attribute => attribute.MusicId);
Musics = MusicAttributes.Select(pair => pair.Key)
.ToList();
Musics.Sort();
MusicsWithUra = MusicAttributes.Where(attribute => attribute.Value.HasUra)
.Select(pair => pair.Key)
.ToList();
MusicsWithUra.Sort();
}
public static MusicAttributeManager Instance { get; } = new();
public readonly List<uint> Musics;
public readonly List<uint> MusicsWithUra;
}

View File

@ -2,7 +2,7 @@
public static class PathHelper public static class PathHelper
{ {
public static string GetDataPath() public static string GetRootPath()
{ {
var path = Environment.ProcessPath; var path = Environment.ProcessPath;
if (path is null) if (path is null)
@ -16,4 +16,9 @@ public static class PathHelper
} }
return Path.Combine(parentPath.ToString(), "wwwroot"); return Path.Combine(parentPath.ToString(), "wwwroot");
} }
public static string GetDataPath()
{
return Path.Combine(GetRootPath(), "data");
}
} }

View File

@ -22,7 +22,7 @@
{ {
return; return;
} }
var path = Path.Combine(PathHelper.GetDataPath(), Constants.DEFAULT_DB_NAME); var path = Path.Combine(PathHelper.GetRootPath(), Constants.DEFAULT_DB_NAME);
optionsBuilder.UseSqlite($"Data Source={path}"); optionsBuilder.UseSqlite($"Data Source={path}");
} }

View File

@ -1,9 +1,11 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text.Json;
using SharedProject.Models; using SharedProject.Models;
using SharedProject.Models.Responses; using SharedProject.Models.Responses;
using SharedProject.Utils; using SharedProject.Utils;
using TaikoLocalServer.Services; using TaikoLocalServer.Services;
using TaikoLocalServer.Services.Interfaces; using TaikoLocalServer.Services.Interfaces;
using Throw;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;
@ -28,6 +30,10 @@ public class UserSettingsController : BaseController<UserSettingsController>
return NotFound(); return NotFound();
} }
var costumeData = JsonHelper.GetCostumeDataFromUserData(user, Logger);
var costumeUnlockData = JsonHelper.GetCostumeUnlockDataFromUserData(user, Logger);
var response = new UserSetting var response = new UserSetting
{ {
AchievementDisplayDifficulty = user.AchievementDisplayDifficulty, AchievementDisplayDifficulty = user.AchievementDisplayDifficulty,
@ -40,7 +46,20 @@ public class UserSettingsController : BaseController<UserSettingsController>
ToneId = user.SelectedToneId, ToneId = user.SelectedToneId,
MyDonName = user.MyDonName, MyDonName = user.MyDonName,
Title = user.Title, Title = user.Title,
TitlePlateId = user.TitlePlateId TitlePlateId = user.TitlePlateId,
Kigurumi = costumeData[0],
Head = costumeData[1],
Body = costumeData[2],
Face = costumeData[3],
Puchi = costumeData[4],
UnlockedKigurumi = costumeUnlockData[0],
UnlockedHead = costumeUnlockData[1],
UnlockedBody = costumeUnlockData[2],
UnlockedFace = costumeUnlockData[3],
UnlockedPuchi = costumeUnlockData[4],
BodyColor = user.ColorBody,
FaceColor = user.ColorFace,
LimbColor = user.ColorLimb
}; };
return Ok(response); return Ok(response);
} }
@ -55,6 +74,15 @@ public class UserSettingsController : BaseController<UserSettingsController>
return NotFound(); return NotFound();
} }
var costumes = new List<uint>
{
userSetting.Kigurumi,
userSetting.Head,
userSetting.Body,
userSetting.Face,
userSetting.Puchi,
};
user.IsSkipOn = userSetting.IsSkipOn; user.IsSkipOn = userSetting.IsSkipOn;
user.IsVoiceOn = userSetting.IsVoiceOn; user.IsVoiceOn = userSetting.IsVoiceOn;
user.DisplayAchievement = userSetting.IsDisplayAchievement; user.DisplayAchievement = userSetting.IsDisplayAchievement;
@ -66,6 +94,11 @@ public class UserSettingsController : BaseController<UserSettingsController>
user.MyDonName = userSetting.MyDonName; user.MyDonName = userSetting.MyDonName;
user.Title = userSetting.Title; user.Title = userSetting.Title;
user.TitlePlateId = userSetting.TitlePlateId; user.TitlePlateId = userSetting.TitlePlateId;
user.ColorBody = userSetting.BodyColor;
user.ColorFace = userSetting.FaceColor;
user.ColorLimb = userSetting.LimbColor;
user.CostumeData = JsonSerializer.Serialize(costumes);
await userDatumService.UpdateUserDatum(user); await userDatumService.UpdateUserDatum(user);

View File

@ -1,5 +1,6 @@
using System.Text.Json; using System.Text.Json;
using TaikoLocalServer.Services.Interfaces; using TaikoLocalServer.Services.Interfaces;
using Throw;
namespace TaikoLocalServer.Controllers.Game; namespace TaikoLocalServer.Controllers.Game;
@ -73,42 +74,18 @@ public class BaidController : BaseController<BaidController>
datum.Difficulty == achievementDisplayDifficulty : datum.Difficulty == achievementDisplayDifficulty :
datum.Difficulty is Difficulty.Oni or Difficulty.UraOni).ToList(); datum.Difficulty is Difficulty.Oni or Difficulty.UraOni).ToList();
var crownCount = new uint[3]; var crownCount = CalculateCrownCount(songCountData);
foreach (var crownType in Enum.GetValues<CrownType>())
{
if (crownType != CrownType.None)
{
crownCount[(int)crownType - 1] = (uint)songCountData.Count(datum => datum.BestCrown == crownType);
}
}
var scoreRankCount = new uint[7]; var scoreRankCount = CalculateScoreRankCount(songCountData);
foreach (var scoreRankType in Enum.GetValues<ScoreRank>())
{
if (scoreRankType != ScoreRank.None)
{
scoreRankCount[(int)scoreRankType - 2] = (uint)songCountData.Count(datum => datum.BestScoreRank == scoreRankType);
}
}
var costumeData = new List<uint>{ 0, 0, 0, 0, 0 }; var costumeData = JsonHelper.GetCostumeDataFromUserData(userData, Logger);
try
{
costumeData = JsonSerializer.Deserialize<List<uint>>(userData.CostumeData);
}
catch (JsonException e)
{
Logger.LogError(e, "Parsing costume json data failed");
}
if (costumeData == null || costumeData.Count < 5)
{
Logger.LogWarning("Costume data is null or count less than 5!");
costumeData = new List<uint> { 0, 0, 0, 0, 0 };
}
var costumeFlag = new byte[10]; var costumeArrays = JsonHelper.GetCostumeUnlockDataFromUserData(userData, Logger);
Array.Fill(costumeFlag, byte.MaxValue);
var costumeFlagArrays = Constants.CostumeFlagArraySizes
.Select((size, index) => FlagCalculator.GetBitArrayFromIds(costumeArrays[index], size, Logger))
.ToList();
var danData = await danScoreDatumService.GetDanScoreDatumByBaid(baid); var danData = await danScoreDatumService.GetDanScoreDatumByBaid(baid);
@ -118,6 +95,23 @@ public class BaidController : BaseController<BaidController>
.Max(); .Max();
var gotDanFlagArray = FlagCalculator.ComputeGotDanFlags(danData); var gotDanFlagArray = FlagCalculator.ComputeGotDanFlags(danData);
var genericInfoFlg = Array.Empty<uint>();
try
{
genericInfoFlg = JsonSerializer.Deserialize<uint[]>(userData.GenericInfoFlgArray);
}
catch (JsonException e)
{
Logger.LogError(e, "Parsing genericinfo 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
genericInfoFlg.ThrowIfNull("Genericinfo flg should never be null!");
var genericInfoFlgLength = genericInfoFlg.Any()? genericInfoFlg.Max() + 1 : 0;
var genericInfoFlgArray = FlagCalculator.GetBitArrayFromIds(genericInfoFlg, (int)genericInfoFlgLength, Logger);
response = new BAIDResponse response = new BAIDResponse
{ {
Result = 1, Result = 1,
@ -145,18 +139,18 @@ public class BaidController : BaseController<BaidController>
Costume4 = costumeData[3], Costume4 = costumeData[3],
Costume5 = costumeData[4] Costume5 = costumeData[4]
}, },
CostumeFlg1 = costumeFlag, CostumeFlg1 = costumeFlagArrays[0],
CostumeFlg2 = costumeFlag, CostumeFlg2 = costumeFlagArrays[1],
CostumeFlg3 = costumeFlag, CostumeFlg3 = costumeFlagArrays[2],
CostumeFlg4 = costumeFlag, CostumeFlg4 = costumeFlagArrays[3],
CostumeFlg5 = costumeFlag, CostumeFlg5 = costumeFlagArrays[4],
LastPlayDatetime = userData.LastPlayDatetime.ToString(Constants.DATE_TIME_FORMAT), LastPlayDatetime = userData.LastPlayDatetime.ToString(Constants.DATE_TIME_FORMAT),
IsDispDanOn = userData.DisplayDan, IsDispDanOn = userData.DisplayDan,
GotDanMax = maxDan, GotDanMax = maxDan,
GotDanFlg = gotDanFlagArray, GotDanFlg = gotDanFlagArray,
GotDanextraFlg = new byte[20], GotDanextraFlg = new byte[20],
DefaultToneSetting = userData.SelectedToneId, DefaultToneSetting = userData.SelectedToneId,
GenericInfoFlg = new byte[10], GenericInfoFlg = genericInfoFlgArray,
AryCrownCounts = crownCount, AryCrownCounts = crownCount,
AryScoreRankCounts = scoreRankCount, AryScoreRankCounts = scoreRankCount,
IsDispAchievementOn = userData.DisplayAchievement, IsDispAchievementOn = userData.DisplayAchievement,
@ -173,4 +167,32 @@ public class BaidController : BaseController<BaidController>
return Ok(response); return Ok(response);
} }
private static uint[] CalculateScoreRankCount(IReadOnlyCollection<SongBestDatum> songCountData)
{
var scoreRankCount = new uint[7];
foreach (var scoreRankType in Enum.GetValues<ScoreRank>())
{
if (scoreRankType != ScoreRank.None)
{
scoreRankCount[(int)scoreRankType - 2] =
(uint)songCountData.Count(datum => datum.BestScoreRank == scoreRankType);
}
}
return scoreRankCount;
}
private static uint[] CalculateCrownCount(IReadOnlyCollection<SongBestDatum> songCountData)
{
var crownCount = new uint[3];
foreach (var crownType in Enum.GetValues<CrownType>())
{
if (crownType != CrownType.None)
{
crownCount[(int)crownType - 1] = (uint)songCountData.Count(datum => datum.BestCrown == crownType);
}
}
return crownCount;
}
} }

View File

@ -1,9 +1,18 @@
namespace TaikoLocalServer.Controllers.Game; using TaikoLocalServer.Services.Interfaces;
namespace TaikoLocalServer.Controllers.Game;
[Route("/v12r03/chassis/getdanodai.php")] [Route("/v12r03/chassis/getdanodai.php")]
[ApiController] [ApiController]
public class GetDanOdaiController : BaseController<GetDanOdaiController> public class GetDanOdaiController : BaseController<GetDanOdaiController>
{ {
private readonly IGameDataService gameDataService;
public GetDanOdaiController(IGameDataService gameDataService)
{
this.gameDataService = gameDataService;
}
[HttpPost] [HttpPost]
[Produces("application/protobuf")] [Produces("application/protobuf")]
public IActionResult GetDanOdai([FromBody] GetDanOdaiRequest request) public IActionResult GetDanOdai([FromBody] GetDanOdaiRequest request)
@ -19,11 +28,10 @@ public class GetDanOdaiController : BaseController<GetDanOdaiController>
{ {
return Ok(response); return Ok(response);
} }
var manager = DanOdaiDataManager.Instance;
foreach (var danId in request.DanIds) foreach (var danId in request.DanIds)
{ {
manager.OdaiDataList.TryGetValue(danId, out var odaiData); gameDataService.GetDanDataDictionary().TryGetValue(danId, out var odaiData);
if (odaiData is null) if (odaiData is null)
{ {
Logger.LogWarning("Requested dan id {Id} does not exist!", danId); Logger.LogWarning("Requested dan id {Id} does not exist!", danId);

View File

@ -1,9 +1,18 @@
namespace TaikoLocalServer.Controllers.Game; using TaikoLocalServer.Services.Interfaces;
namespace TaikoLocalServer.Controllers.Game;
[Route("/v12r03/chassis/getsongintroduction.php")] [Route("/v12r03/chassis/getsongintroduction.php")]
[ApiController] [ApiController]
public class GetSongIntroductionController : BaseController<GetSongIntroductionController> public class GetSongIntroductionController : BaseController<GetSongIntroductionController>
{ {
private readonly IGameDataService gameDataService;
public GetSongIntroductionController(IGameDataService gameDataService)
{
this.gameDataService = gameDataService;
}
[HttpPost] [HttpPost]
[Produces("application/protobuf")] [Produces("application/protobuf")]
public IActionResult GetSongIntroduction([FromBody] GetSongIntroductionRequest request) public IActionResult GetSongIntroduction([FromBody] GetSongIntroductionRequest request)
@ -14,16 +23,17 @@ public class GetSongIntroductionController : BaseController<GetSongIntroductionC
{ {
Result = 1 Result = 1
}; };
foreach (var setId in request.SetIds) foreach (var setId in request.SetIds)
{ {
response.ArySongIntroductionDatas.Add(new GetSongIntroductionResponse.SongIntroductionData gameDataService.GetSongIntroDictionary().TryGetValue(setId, out var introData);
if (introData is null)
{ {
MainSongNo = 2, Logger.LogWarning("Requested set id {Id} does not exist!", setId);
SubSongNoes = new uint[] {177,193,3,4}, continue;
SetId = setId, }
VerupNo = 1
}); response.ArySongIntroductionDatas.Add(introData);
} }
return Ok(response); return Ok(response);

View File

@ -1,4 +1,4 @@
using System.Collections; using TaikoLocalServer.Services.Interfaces;
namespace TaikoLocalServer.Controllers.Game; namespace TaikoLocalServer.Controllers.Game;
@ -6,21 +6,21 @@ namespace TaikoLocalServer.Controllers.Game;
[Route("/v12r03/chassis/initialdatacheck.php")] [Route("/v12r03/chassis/initialdatacheck.php")]
public class InitialDataCheckController : BaseController<InitialDataCheckController> public class InitialDataCheckController : BaseController<InitialDataCheckController>
{ {
private readonly IGameDataService gameDataService;
public InitialDataCheckController(IGameDataService gameDataService)
{
this.gameDataService = gameDataService;
}
[HttpPost] [HttpPost]
[Produces("application/protobuf")] [Produces("application/protobuf")]
public IActionResult InitialDataCheck([FromBody] InitialdatacheckRequest request) public IActionResult InitialDataCheck([FromBody] InitialdatacheckRequest request)
{ {
Logger.LogInformation("Initial data check request: {Request}", request.Stringify()); Logger.LogInformation("Initial data check request: {Request}", request.Stringify());
var musicAttributeManager = MusicAttributeManager.Instance; var enabledArray =
FlagCalculator.GetBitArrayFromIds(gameDataService.GetMusicList(), Constants.MUSIC_ID_MAX, Logger);
var enabledArray = new byte[Constants.MUSIC_FLAG_ARRAY_SIZE];
var bitSet = new BitArray(Constants.MUSIC_ID_MAX);
foreach (var music in musicAttributeManager.Musics)
{
bitSet.Set((int)music, true);
}
bitSet.CopyTo(enabledArray, 0);
var danData = new List<InitialdatacheckResponse.InformationData>(); var danData = new List<InitialdatacheckResponse.InformationData>();
for (var danId = Constants.MIN_DAN_ID; danId <= Constants.MAX_DAN_ID; danId++) for (var danId = Constants.MIN_DAN_ID; danId <= Constants.MAX_DAN_ID; danId++)
@ -32,6 +32,16 @@ public class InitialDataCheckController : BaseController<InitialDataCheckControl
}); });
} }
var introData = new List<InitialdatacheckResponse.InformationData>();
for (var setId = 1; setId <= gameDataService.GetSongIntroDictionary().Count; setId++)
{
introData.Add(new InitialdatacheckResponse.InformationData
{
InfoId = (uint)setId,
VerupNo = 1
});
}
var response = new InitialdatacheckResponse var response = new InitialdatacheckResponse
{ {
Result = 1, Result = 1,
@ -100,6 +110,7 @@ public class InitialDataCheckController : BaseController<InitialDataCheckControl
});*/ });*/
}; };
response.AryDanOdaiDatas.AddRange(danData); response.AryDanOdaiDatas.AddRange(danData);
response.ArySongIntroductionDatas.AddRange(introData);
return Ok(response); return Ok(response);
} }

View File

@ -40,7 +40,11 @@ public class MyDonEntryController : BaseController<MyDonEntryController>
ColorFace = 0, ColorFace = 0,
ColorBody = 1, ColorBody = 1,
ColorLimb = 3, ColorLimb = 3,
FavoriteSongsArray = "[]" FavoriteSongsArray = "[]",
ToneFlgArray = "[]",
TitleFlgArray = "[]",
CostumeFlgArray = "[[],[],[],[],[]]",
GenericInfoFlgArray = "[]"
}; };
await userDatumService.InsertUserDatum(newUser); await userDatumService.InsertUserDatum(newUser);

View File

@ -65,7 +65,7 @@ public class PlayResultController : BaseController<PlayResultController>
if (playMode == PlayMode.AiBattle) if (playMode == PlayMode.AiBattle)
{ {
await UpdateAiBattleData(request, stageData); // await UpdateAiBattleData(request, stageData);
// Update AI win count here somewhere, or in UpdatePlayData? // Update AI win count here somewhere, or in UpdatePlayData?
// I have no clue how to update input median or variance // I have no clue how to update input median or variance
} }
@ -74,7 +74,7 @@ public class PlayResultController : BaseController<PlayResultController>
await UpdatePlayData(request, songNumber, stageData, lastPlayDatetime); await UpdatePlayData(request, songNumber, stageData, lastPlayDatetime);
} }
return Ok(response); return Ok(response);
} }
@ -191,9 +191,83 @@ public class PlayResultController : BaseController<PlayResultController>
userdata.LastPlayDatetime = lastPlayDatetime; userdata.LastPlayDatetime = lastPlayDatetime;
userdata.LastPlayMode = playResultData.PlayMode; userdata.LastPlayMode = playResultData.PlayMode;
userdata.ToneFlgArray =
UpdateJsonUintFlagArray(userdata.ToneFlgArray, playResultData.GetToneNoes, nameof(userdata.ToneFlgArray));
userdata.TitleFlgArray =
UpdateJsonUintFlagArray(userdata.TitleFlgArray, playResultData.GetTitleNoes, nameof(userdata.TitleFlgArray));
userdata.CostumeFlgArray = UpdateJsonCostumeFlagArray(userdata.CostumeFlgArray,
new[]
{
playResultData.GetCostumeNo1s,
playResultData.GetCostumeNo2s,
playResultData.GetCostumeNo3s,
playResultData.GetCostumeNo4s,
playResultData.GetCostumeNo5s
});
userdata.GenericInfoFlgArray =
UpdateJsonUintFlagArray(userdata.GenericInfoFlgArray, playResultData.GetGenericInfoNoes, nameof(userdata.GenericInfoFlgArray));
await userDatumService.UpdateUserDatum(userdata); await userDatumService.UpdateUserDatum(userdata);
} }
private string UpdateJsonUintFlagArray(string originalValue, IReadOnlyCollection<uint>? newValue, string fieldName)
{
var flgData = new List<uint>();
try
{
flgData = JsonSerializer.Deserialize<List<uint>>(originalValue);
}
catch (JsonException e)
{
Logger.LogError(e, "Parsing {FieldName} json data failed", fieldName);
}
flgData?.AddRange(newValue ?? Array.Empty<uint>());
var flgArray = flgData ?? new List<uint>();
return JsonSerializer.Serialize(flgArray);
}
private string UpdateJsonCostumeFlagArray(string originalValue, IReadOnlyList<IReadOnlyCollection<uint>?>? newValue)
{
var flgData = new List<List<uint>>();
try
{
flgData = JsonSerializer.Deserialize<List<List<uint>>>(originalValue);
}
catch (JsonException e)
{
Logger.LogError(e, "Parsing Costume flag json data failed");
}
if (flgData is null)
{
flgData = new List<List<uint>>();
}
for (var index = 0; index < flgData.Count; index++)
{
var subFlgData = flgData[index];
subFlgData.AddRange(newValue?[index] ?? Array.Empty<uint>());
}
if (flgData.Count >= 5)
{
return JsonSerializer.Serialize(flgData);
}
Logger.LogWarning("Costume flag array count less than 5!");
flgData = new List<List<uint>>
{
new(), new(), new(), new(), new()
};
return JsonSerializer.Serialize(flgData);
}
private async Task UpdateBestData(PlayResultRequest request, StageData stageData, private async Task UpdateBestData(PlayResultRequest request, StageData stageData,
IEnumerable<SongBestDatum> bestData) IEnumerable<SongBestDatum> bestData)
{ {
@ -215,7 +289,8 @@ public class PlayResultController : BaseController<PlayResultController>
await songBestDatumService.UpdateOrInsertSongBestDatum(bestDatum); await songBestDatumService.UpdateOrInsertSongBestDatum(bestDatum);
} }
private async Task UpdateAiBattleData(PlayResultRequest request, StageData stageData) // TODO: AI battle
/*private async Task UpdateAiBattleData(PlayResultRequest request, StageData stageData)
{ {
for (int i = 0; i < stageData.ArySectionDatas.Count; i++) for (int i = 0; i < stageData.ArySectionDatas.Count; i++)
{ {
@ -226,7 +301,7 @@ public class PlayResultController : BaseController<PlayResultController>
// if any aspect of the section is higher than the previous best, update it // if any aspect of the section is higher than the previous best, update it
// Similar to Dan best play updates // Similar to Dan best play updates
} }
} }*/
private static CrownType PlayResultToCrown(StageData stageData) private static CrownType PlayResultToCrown(StageData stageData)
{ {

View File

@ -8,10 +8,13 @@ namespace TaikoLocalServer.Controllers.Game;
public class SelfBestController : BaseController<SelfBestController> public class SelfBestController : BaseController<SelfBestController>
{ {
private readonly ISongBestDatumService songBestDatumService; private readonly ISongBestDatumService songBestDatumService;
private readonly IGameDataService gameDataService;
public SelfBestController(ISongBestDatumService songBestDatumService) public SelfBestController(ISongBestDatumService songBestDatumService, IGameDataService gameDataService)
{ {
this.songBestDatumService = songBestDatumService; this.songBestDatumService = songBestDatumService;
this.gameDataService = gameDataService;
} }
[HttpPost] [HttpPost]
@ -25,8 +28,6 @@ public class SelfBestController : BaseController<SelfBestController>
Result = 1, Result = 1,
Level = request.Level Level = request.Level
}; };
var manager = MusicAttributeManager.Instance;
var requestDifficulty = (Difficulty)request.Level; var requestDifficulty = (Difficulty)request.Level;
requestDifficulty.Throw().IfOutOfRange(); requestDifficulty.Throw().IfOutOfRange();
@ -38,7 +39,7 @@ public class SelfBestController : BaseController<SelfBestController>
.ToList(); .ToList();
foreach (var songNo in request.ArySongNoes) foreach (var songNo in request.ArySongNoes)
{ {
if (!manager.MusicAttributes.ContainsKey(songNo)) if (!gameDataService.GetMusicAttributes().ContainsKey(songNo))
{ {
Logger.LogWarning("Music no {No} is missing!", songNo); Logger.LogWarning("Music no {No} is missing!", songNo);
continue; continue;

View File

@ -1,5 +1,4 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections;
using System.Text.Json; using System.Text.Json;
using TaikoLocalServer.Services.Interfaces; using TaikoLocalServer.Services.Interfaces;
using Throw; using Throw;
@ -13,11 +12,14 @@ public class UserDataController : BaseController<UserDataController>
private readonly IUserDatumService userDatumService; private readonly IUserDatumService userDatumService;
private readonly ISongPlayDatumService songPlayDatumService; private readonly ISongPlayDatumService songPlayDatumService;
private readonly IGameDataService gameDataService;
public UserDataController(IUserDatumService userDatumService, ISongPlayDatumService songPlayDatumService) public UserDataController(IUserDatumService userDatumService, ISongPlayDatumService songPlayDatumService, IGameDataService gameDataService)
{ {
this.userDatumService = userDatumService; this.userDatumService = userDatumService;
this.songPlayDatumService = songPlayDatumService; this.songPlayDatumService = songPlayDatumService;
this.gameDataService = gameDataService;
} }
[HttpPost] [HttpPost]
@ -26,26 +28,45 @@ public class UserDataController : BaseController<UserDataController>
{ {
Logger.LogInformation("UserData request : {Request}", request.Stringify()); Logger.LogInformation("UserData request : {Request}", request.Stringify());
var musicAttributeManager = MusicAttributeManager.Instance; var releaseSongArray =
FlagCalculator.GetBitArrayFromIds(gameDataService.GetMusicList(), Constants.MUSIC_ID_MAX, Logger);
var releaseSongArray = new byte[Constants.MUSIC_FLAG_ARRAY_SIZE]; var uraSongArray =
var bitSet = new BitArray(Constants.MUSIC_ID_MAX); FlagCalculator.GetBitArrayFromIds(gameDataService.GetMusicWithUraList(), Constants.MUSIC_ID_MAX, Logger);
foreach (var music in musicAttributeManager.Musics)
{
bitSet.Set((int)music, true);
}
bitSet.CopyTo(releaseSongArray, 0);
var uraSongArray = new byte[Constants.MUSIC_FLAG_ARRAY_SIZE];
bitSet.SetAll(false);
foreach (var music in musicAttributeManager.MusicsWithUra)
{
bitSet.Set((int)music, true);
}
bitSet.CopyTo(uraSongArray, 0);
var toneArray = new byte[16]; var userData = await userDatumService.GetFirstUserDatumOrDefault(request.Baid);
Array.Fill(toneArray, byte.MaxValue);
var toneFlg = 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!");
var toneArray = FlagCalculator.GetBitArrayFromIds(toneFlg, Constants.TONE_UID_MAX, Logger);
var titleFlg = 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, Constants.TITLE_UID_MAX, Logger);
var recentSongs = (await songPlayDatumService.GetSongPlayDatumByBaid(request.Baid)) var recentSongs = (await songPlayDatumService.GetSongPlayDatumByBaid(request.Baid))
.AsEnumerable() .AsEnumerable()
@ -67,8 +88,6 @@ public class UserDataController : BaseController<UserDataController>
recentSongs = recentSet.ToArray(); recentSongs = recentSet.ToArray();
var userData = await userDatumService.GetFirstUserDatumOrDefault(request.Baid);
var favoriteSongs = Array.Empty<uint>(); var favoriteSongs = Array.Empty<uint>();
try try
{ {
@ -90,7 +109,7 @@ public class UserDataController : BaseController<UserDataController>
{ {
Result = 1, Result = 1,
ToneFlg = toneArray, ToneFlg = toneArray,
// TitleFlg = GZipBytesUtil.GetGZipBytes(new byte[100]), TitleFlg = titleArray,
ReleaseSongFlg = releaseSongArray, ReleaseSongFlg = releaseSongArray,
UraReleaseSongFlg = uraSongArray, UraReleaseSongFlg = uraSongArray,
DefaultOptionSetting = defaultOptions, DefaultOptionSetting = defaultOptions,

View File

@ -6,7 +6,11 @@
public string MyDonName { get; set; } = string.Empty; public string MyDonName { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
public uint TitlePlateId { get; set; } public uint TitlePlateId { get; set; }
public string FavoriteSongsArray { get; set; } = string.Empty; public string FavoriteSongsArray { get; set; } = "[]";
public string ToneFlgArray { get; set; } = "[]";
public string TitleFlgArray { get; set; } = "[]";
public string CostumeFlgArray { get; set; } = "[[],[],[],[],[]]";
public string GenericInfoFlgArray { get; set; } = "[]";
public short OptionSetting { get; set; } public short OptionSetting { get; set; }
public int NotesPosition { get; set; } public int NotesPosition { get; set; }
public bool IsVoiceOn { get; set; } public bool IsVoiceOn { get; set; }
@ -17,7 +21,7 @@
public uint ColorBody { get; set; } public uint ColorBody { get; set; }
public uint ColorFace { get; set; } public uint ColorFace { get; set; }
public uint ColorLimb { get; set; } public uint ColorLimb { get; set; }
public string CostumeData { get; set; } = string.Empty; public string CostumeData { get; set; } = "[[],[],[],[],[]]";
public bool DisplayDan { get; set; } public bool DisplayDan { get; set; }
public bool DisplayAchievement { get; set; } public bool DisplayAchievement { get; set; }
public Difficulty AchievementDisplayDifficulty { get; set; } public Difficulty AchievementDisplayDifficulty { get; set; }

View File

@ -0,0 +1,341 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using TaikoLocalServer.Context;
#nullable disable
namespace TaikoLocalServer.Migrations
{
[DbContext(typeof(TaikoDbContext))]
[Migration("20220914054039_AddRewardFlgs")]
partial class AddRewardFlgs
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.0-preview.7.22376.2");
modelBuilder.Entity("TaikoLocalServer.Entities.Card", b =>
{
b.Property<string>("AccessCode")
.HasColumnType("TEXT");
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.HasKey("AccessCode");
b.HasIndex(new[] { "Baid" }, "IX_Card_Baid")
.IsUnique();
b.ToTable("Card", (string)null);
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("DanId")
.HasColumnType("INTEGER");
b.Property<uint>("ArrivalSongCount")
.HasColumnType("INTEGER");
b.Property<uint>("ClearState")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0u);
b.Property<uint>("ComboCountTotal")
.HasColumnType("INTEGER");
b.Property<uint>("SoulGaugeTotal")
.HasColumnType("INTEGER");
b.HasKey("Baid", "DanId");
b.ToTable("DanScoreData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanStageScoreDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("DanId")
.HasColumnType("INTEGER");
b.Property<uint>("SongNumber")
.HasColumnType("INTEGER");
b.Property<uint>("BadCount")
.HasColumnType("INTEGER");
b.Property<uint>("ComboCount")
.HasColumnType("INTEGER");
b.Property<uint>("DrumrollCount")
.HasColumnType("INTEGER");
b.Property<uint>("GoodCount")
.HasColumnType("INTEGER");
b.Property<uint>("HighScore")
.HasColumnType("INTEGER");
b.Property<uint>("OkCount")
.HasColumnType("INTEGER");
b.Property<uint>("PlayScore")
.HasColumnType("INTEGER");
b.Property<uint>("TotalHitCount")
.HasColumnType("INTEGER");
b.HasKey("Baid", "DanId", "SongNumber");
b.ToTable("DanStageScoreData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.SongBestDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<uint>("BestCrown")
.HasColumnType("INTEGER");
b.Property<uint>("BestRate")
.HasColumnType("INTEGER");
b.Property<uint>("BestScore")
.HasColumnType("INTEGER");
b.Property<uint>("BestScoreRank")
.HasColumnType("INTEGER");
b.HasKey("Baid", "SongId", "Difficulty");
b.ToTable("SongBestData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.SongPlayDatum", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("ComboCount")
.HasColumnType("INTEGER");
b.Property<uint>("Crown")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<uint>("DrumrollCount")
.HasColumnType("INTEGER");
b.Property<uint>("GoodCount")
.HasColumnType("INTEGER");
b.Property<uint>("HitCount")
.HasColumnType("INTEGER");
b.Property<uint>("MissCount")
.HasColumnType("INTEGER");
b.Property<uint>("OkCount")
.HasColumnType("INTEGER");
b.Property<DateTime>("PlayTime")
.HasColumnType("datetime");
b.Property<uint>("Score")
.HasColumnType("INTEGER");
b.Property<uint>("ScoreRank")
.HasColumnType("INTEGER");
b.Property<uint>("ScoreRate")
.HasColumnType("INTEGER");
b.Property<bool>("Skipped")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("SongNumber")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Baid");
b.ToTable("SongPlayData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.UserDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("AchievementDisplayDifficulty")
.HasColumnType("INTEGER");
b.Property<uint>("ColorBody")
.HasColumnType("INTEGER");
b.Property<uint>("ColorFace")
.HasColumnType("INTEGER");
b.Property<uint>("ColorLimb")
.HasColumnType("INTEGER");
b.Property<string>("CostumeData")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CostumeFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("DisplayAchievement")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayDan")
.HasColumnType("INTEGER");
b.Property<string>("FavoriteSongsArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsSkipOn")
.HasColumnType("INTEGER");
b.Property<bool>("IsVoiceOn")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastPlayDatetime")
.HasColumnType("datetime");
b.Property<uint>("LastPlayMode")
.HasColumnType("INTEGER");
b.Property<string>("MyDonName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("NotesPosition")
.HasColumnType("INTEGER");
b.Property<short>("OptionSetting")
.HasColumnType("INTEGER");
b.Property<uint>("SelectedToneId")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("TitleFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<uint>("TitlePlateId")
.HasColumnType("INTEGER");
b.Property<string>("ToneFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Baid");
b.ToTable("UserData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
.WithMany()
.HasForeignKey("Baid")
.HasPrincipalKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanStageScoreDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.DanScoreDatum", "Parent")
.WithMany("DanStageScoreData")
.HasForeignKey("Baid", "DanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Parent");
});
modelBuilder.Entity("TaikoLocalServer.Entities.SongBestDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
.WithMany()
.HasForeignKey("Baid")
.HasPrincipalKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.SongPlayDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
.WithMany()
.HasForeignKey("Baid")
.HasPrincipalKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.UserDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
.WithMany()
.HasForeignKey("Baid")
.HasPrincipalKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b =>
{
b.Navigation("DanStageScoreData");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TaikoLocalServer.Migrations
{
/// <inheritdoc />
public partial class AddRewardFlgs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CostumeFlgArray",
table: "UserData",
type: "TEXT",
nullable: false,
defaultValue: "[[],[],[],[],[]]");
migrationBuilder.AddColumn<string>(
name: "TitleFlgArray",
table: "UserData",
type: "TEXT",
nullable: false,
defaultValue: "[]");
migrationBuilder.AddColumn<string>(
name: "ToneFlgArray",
table: "UserData",
type: "TEXT",
nullable: false,
defaultValue: "[]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CostumeFlgArray",
table: "UserData");
migrationBuilder.DropColumn(
name: "TitleFlgArray",
table: "UserData");
migrationBuilder.DropColumn(
name: "ToneFlgArray",
table: "UserData");
}
}
}

View File

@ -0,0 +1,345 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using TaikoLocalServer.Context;
#nullable disable
namespace TaikoLocalServer.Migrations
{
[DbContext(typeof(TaikoDbContext))]
[Migration("20220916121143_AddGenericInfoFlg")]
partial class AddGenericInfoFlg
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.0-preview.7.22376.2");
modelBuilder.Entity("TaikoLocalServer.Entities.Card", b =>
{
b.Property<string>("AccessCode")
.HasColumnType("TEXT");
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.HasKey("AccessCode");
b.HasIndex(new[] { "Baid" }, "IX_Card_Baid")
.IsUnique();
b.ToTable("Card", (string)null);
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("DanId")
.HasColumnType("INTEGER");
b.Property<uint>("ArrivalSongCount")
.HasColumnType("INTEGER");
b.Property<uint>("ClearState")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0u);
b.Property<uint>("ComboCountTotal")
.HasColumnType("INTEGER");
b.Property<uint>("SoulGaugeTotal")
.HasColumnType("INTEGER");
b.HasKey("Baid", "DanId");
b.ToTable("DanScoreData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanStageScoreDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("DanId")
.HasColumnType("INTEGER");
b.Property<uint>("SongNumber")
.HasColumnType("INTEGER");
b.Property<uint>("BadCount")
.HasColumnType("INTEGER");
b.Property<uint>("ComboCount")
.HasColumnType("INTEGER");
b.Property<uint>("DrumrollCount")
.HasColumnType("INTEGER");
b.Property<uint>("GoodCount")
.HasColumnType("INTEGER");
b.Property<uint>("HighScore")
.HasColumnType("INTEGER");
b.Property<uint>("OkCount")
.HasColumnType("INTEGER");
b.Property<uint>("PlayScore")
.HasColumnType("INTEGER");
b.Property<uint>("TotalHitCount")
.HasColumnType("INTEGER");
b.HasKey("Baid", "DanId", "SongNumber");
b.ToTable("DanStageScoreData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.SongBestDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<uint>("BestCrown")
.HasColumnType("INTEGER");
b.Property<uint>("BestRate")
.HasColumnType("INTEGER");
b.Property<uint>("BestScore")
.HasColumnType("INTEGER");
b.Property<uint>("BestScoreRank")
.HasColumnType("INTEGER");
b.HasKey("Baid", "SongId", "Difficulty");
b.ToTable("SongBestData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.SongPlayDatum", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("ComboCount")
.HasColumnType("INTEGER");
b.Property<uint>("Crown")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<uint>("DrumrollCount")
.HasColumnType("INTEGER");
b.Property<uint>("GoodCount")
.HasColumnType("INTEGER");
b.Property<uint>("HitCount")
.HasColumnType("INTEGER");
b.Property<uint>("MissCount")
.HasColumnType("INTEGER");
b.Property<uint>("OkCount")
.HasColumnType("INTEGER");
b.Property<DateTime>("PlayTime")
.HasColumnType("datetime");
b.Property<uint>("Score")
.HasColumnType("INTEGER");
b.Property<uint>("ScoreRank")
.HasColumnType("INTEGER");
b.Property<uint>("ScoreRate")
.HasColumnType("INTEGER");
b.Property<bool>("Skipped")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("SongNumber")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Baid");
b.ToTable("SongPlayData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.UserDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("AchievementDisplayDifficulty")
.HasColumnType("INTEGER");
b.Property<uint>("ColorBody")
.HasColumnType("INTEGER");
b.Property<uint>("ColorFace")
.HasColumnType("INTEGER");
b.Property<uint>("ColorLimb")
.HasColumnType("INTEGER");
b.Property<string>("CostumeData")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CostumeFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("DisplayAchievement")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayDan")
.HasColumnType("INTEGER");
b.Property<string>("FavoriteSongsArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("GenericInfoFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsSkipOn")
.HasColumnType("INTEGER");
b.Property<bool>("IsVoiceOn")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastPlayDatetime")
.HasColumnType("datetime");
b.Property<uint>("LastPlayMode")
.HasColumnType("INTEGER");
b.Property<string>("MyDonName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("NotesPosition")
.HasColumnType("INTEGER");
b.Property<short>("OptionSetting")
.HasColumnType("INTEGER");
b.Property<uint>("SelectedToneId")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("TitleFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<uint>("TitlePlateId")
.HasColumnType("INTEGER");
b.Property<string>("ToneFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Baid");
b.ToTable("UserData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
.WithMany()
.HasForeignKey("Baid")
.HasPrincipalKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanStageScoreDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.DanScoreDatum", "Parent")
.WithMany("DanStageScoreData")
.HasForeignKey("Baid", "DanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Parent");
});
modelBuilder.Entity("TaikoLocalServer.Entities.SongBestDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
.WithMany()
.HasForeignKey("Baid")
.HasPrincipalKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.SongPlayDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
.WithMany()
.HasForeignKey("Baid")
.HasPrincipalKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.UserDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
.WithMany()
.HasForeignKey("Baid")
.HasPrincipalKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b =>
{
b.Navigation("DanStageScoreData");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TaikoLocalServer.Migrations
{
/// <inheritdoc />
public partial class AddGenericInfoFlg : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "GenericInfoFlgArray",
table: "UserData",
type: "TEXT",
nullable: false,
defaultValue: "[]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "GenericInfoFlgArray",
table: "UserData");
}
}
}

View File

@ -57,7 +57,7 @@ namespace TaikoLocalServer.Migrations
b.HasKey("Baid", "DanId"); b.HasKey("Baid", "DanId");
b.ToTable("DanScoreData", (string)null); b.ToTable("DanScoreData");
}); });
modelBuilder.Entity("TaikoLocalServer.Entities.DanStageScoreDatum", b => modelBuilder.Entity("TaikoLocalServer.Entities.DanStageScoreDatum", b =>
@ -97,7 +97,7 @@ namespace TaikoLocalServer.Migrations
b.HasKey("Baid", "DanId", "SongNumber"); b.HasKey("Baid", "DanId", "SongNumber");
b.ToTable("DanStageScoreData", (string)null); b.ToTable("DanStageScoreData");
}); });
modelBuilder.Entity("TaikoLocalServer.Entities.SongBestDatum", b => modelBuilder.Entity("TaikoLocalServer.Entities.SongBestDatum", b =>
@ -125,7 +125,7 @@ namespace TaikoLocalServer.Migrations
b.HasKey("Baid", "SongId", "Difficulty"); b.HasKey("Baid", "SongId", "Difficulty");
b.ToTable("SongBestData", (string)null); b.ToTable("SongBestData");
}); });
modelBuilder.Entity("TaikoLocalServer.Entities.SongPlayDatum", b => modelBuilder.Entity("TaikoLocalServer.Entities.SongPlayDatum", b =>
@ -186,7 +186,7 @@ namespace TaikoLocalServer.Migrations
b.HasIndex("Baid"); b.HasIndex("Baid");
b.ToTable("SongPlayData", (string)null); b.ToTable("SongPlayData");
}); });
modelBuilder.Entity("TaikoLocalServer.Entities.UserDatum", b => modelBuilder.Entity("TaikoLocalServer.Entities.UserDatum", b =>
@ -210,6 +210,10 @@ namespace TaikoLocalServer.Migrations
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("CostumeFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("DisplayAchievement") b.Property<bool>("DisplayAchievement")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -220,6 +224,10 @@ namespace TaikoLocalServer.Migrations
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("GenericInfoFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsSkipOn") b.Property<bool>("IsSkipOn")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -249,12 +257,20 @@ namespace TaikoLocalServer.Migrations
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("TitleFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<uint>("TitlePlateId") b.Property<uint>("TitlePlateId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("ToneFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Baid"); b.HasKey("Baid");
b.ToTable("UserData", (string)null); b.ToTable("UserData");
}); });
modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b => modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b =>

View File

@ -2,7 +2,7 @@
namespace TaikoLocalServer.Models; namespace TaikoLocalServer.Models;
public class MusicAttribute public class MusicAttributeEntry
{ {
[JsonPropertyName("uniqueId")] [JsonPropertyName("uniqueId")]
public uint MusicId { get; set; } public uint MusicId { get; set; }

View File

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace TaikoLocalServer.Models;
public class MusicAttributes
{
[JsonPropertyName("items")]
public List<MusicAttributeEntry> MusicAttributeEntries { get; set; } = new();
}

View File

@ -6,6 +6,7 @@ using TaikoLocalServer.Services;
using TaikoLocalServer.Services.Extentions; using TaikoLocalServer.Services.Extentions;
using TaikoLocalServer.Services.Interfaces; using TaikoLocalServer.Services.Interfaces;
using TaikoLocalServer.Settings; using TaikoLocalServer.Settings;
using Throw;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Manually enable tls 1.0 // Manually enable tls 1.0
@ -19,6 +20,7 @@ builder.WebHost.UseKestrel(kestrelOptions =>
// Add services to the container. // Add services to the container.
builder.Services.AddOptions(); builder.Services.AddOptions();
builder.Services.AddSingleton<IGameDataService, GameDataService>();
builder.Services.Configure<UrlSettings>(builder.Configuration.GetSection(nameof(UrlSettings))); builder.Services.Configure<UrlSettings>(builder.Configuration.GetSection(nameof(UrlSettings)));
builder.Services.AddControllers().AddProtoBufNet(); builder.Services.AddControllers().AddProtoBufNet();
builder.Services.AddDbContext<TaikoDbContext>(option => builder.Services.AddDbContext<TaikoDbContext>(option =>
@ -28,7 +30,7 @@ builder.Services.AddDbContext<TaikoDbContext>(option =>
{ {
dbName = Constants.DEFAULT_DB_NAME; dbName = Constants.DEFAULT_DB_NAME;
} }
var path = Path.Combine(PathHelper.GetDataPath(), dbName); var path = Path.Combine(PathHelper.GetRootPath(), dbName);
option.UseSqlite($"Data Source={path}"); option.UseSqlite($"Data Source={path}");
}); });
builder.Services.AddHttpLogging(options => builder.Services.AddHttpLogging(options =>
@ -58,6 +60,10 @@ using (var scope = app.Services.CreateScope())
db.Database.Migrate(); db.Database.Migrate();
} }
var gameDataService = app.Services.GetService<IGameDataService>();
gameDataService.ThrowIfNull();
await gameDataService.InitializeAsync();
// For reverse proxy // For reverse proxy
app.UseForwardedHeaders(new ForwardedHeadersOptions app.UseForwardedHeaders(new ForwardedHeadersOptions
{ {

View File

@ -0,0 +1,130 @@
using System.Collections.Immutable;
using System.Text.Json;
using SharedProject.Models;
using Swan.Mapping;
using TaikoLocalServer.Services.Interfaces;
using Throw;
namespace TaikoLocalServer.Services;
public class GameDataService : IGameDataService
{
private ImmutableDictionary<uint, GetDanOdaiResponse.OdaiData> danDataDictionary =
ImmutableDictionary<uint, GetDanOdaiResponse.OdaiData>.Empty;
private ImmutableDictionary<uint, GetSongIntroductionResponse.SongIntroductionData> introDataDictionary =
ImmutableDictionary<uint, GetSongIntroductionResponse.SongIntroductionData>.Empty;
private ImmutableDictionary<uint, MusicAttributeEntry> musicAttributes =
ImmutableDictionary<uint, MusicAttributeEntry>.Empty;
private List<uint> musics = new();
private List<uint> musicsWithUra = new();
public List<uint> GetMusicList()
{
return musics;
}
public List<uint> GetMusicWithUraList()
{
return musicsWithUra;
}
public ImmutableDictionary<uint, MusicAttributeEntry> GetMusicAttributes()
{
return musicAttributes;
}
public ImmutableDictionary<uint, GetDanOdaiResponse.OdaiData> GetDanDataDictionary()
{
return danDataDictionary;
}
public ImmutableDictionary<uint, GetSongIntroductionResponse.SongIntroductionData> GetSongIntroDictionary()
{
return introDataDictionary;
}
public async Task InitializeAsync()
{
var dataPath = PathHelper.GetDataPath();
var musicAttributePath = Path.Combine(dataPath, Constants.MUSIC_ATTRIBUTE_FILE_NAME);
var danDataPath = Path.Combine(dataPath, Constants.DAN_DATA_FILE_NAME);
var songIntroDataPath = Path.Combine(dataPath, Constants.INTRO_DATA_FILE_NAME);
await using var musicAttributeFile = File.OpenRead(musicAttributePath);
await using var danDataFile = File.OpenRead(danDataPath);
await using var songIntroDataFile = File.OpenRead(songIntroDataPath);
var attributesData = await JsonSerializer.DeserializeAsync<MusicAttributes>(musicAttributeFile);
var danData = await JsonSerializer.DeserializeAsync<List<DanData>>(danDataFile);
var introData = await JsonSerializer.DeserializeAsync<List<SongIntroductionData>>(songIntroDataFile);
InitializeMusicAttributes(attributesData);
InitializeDanData(danData);
InitializeIntroData(introData);
}
private void InitializeIntroData(List<SongIntroductionData>? introData)
{
introData.ThrowIfNull("Shouldn't happen!");
introDataDictionary = introData.ToImmutableDictionary(data => data.SetId, ToResponseIntroData);
}
private void InitializeDanData(List<DanData>? danData)
{
danData.ThrowIfNull("Shouldn't happen!");
danDataDictionary = danData.ToImmutableDictionary(data => data.DanId, ToResponseOdaiData);
}
private void InitializeMusicAttributes(MusicAttributes? attributesData)
{
attributesData.ThrowIfNull("Shouldn't happen!");
musicAttributes = attributesData.MusicAttributeEntries.ToImmutableDictionary(attribute => attribute.MusicId);
musics = musicAttributes.Select(pair => pair.Key)
.ToList();
musics.Sort();
musicsWithUra = musicAttributes.Where(attribute => attribute.Value.HasUra)
.Select(pair => pair.Key)
.ToList();
musicsWithUra.Sort();
}
private static GetDanOdaiResponse.OdaiData ToResponseOdaiData(DanData data)
{
var responseOdaiData = new GetDanOdaiResponse.OdaiData
{
DanId = data.DanId,
Title = data.Title,
VerupNo = data.VerupNo
};
var odaiSongs = data.OdaiSongList.Select(song => song.CopyPropertiesToNew<GetDanOdaiResponse.OdaiData.OdaiSong>());
responseOdaiData.AryOdaiSongs.AddRange(odaiSongs);
var odaiBorders = data.OdaiBorderList.Select(border => border.CopyPropertiesToNew<GetDanOdaiResponse.OdaiData.OdaiBorder>());
responseOdaiData.AryOdaiBorders.AddRange(odaiBorders);
return responseOdaiData;
}
private static GetSongIntroductionResponse.SongIntroductionData ToResponseIntroData(SongIntroductionData data)
{
var responseOdaiData = new GetSongIntroductionResponse.SongIntroductionData
{
SetId = data.SetId,
VerupNo = data.VerupNo,
MainSongNo = data.MainSongNo,
SubSongNoes = data.SubSongNo
};
return responseOdaiData;
}
}

View File

@ -0,0 +1,18 @@
using System.Collections.Immutable;
namespace TaikoLocalServer.Services.Interfaces;
public interface IGameDataService
{
public Task InitializeAsync();
public List<uint> GetMusicList();
public List<uint> GetMusicWithUraList();
public ImmutableDictionary<uint, MusicAttributeEntry> GetMusicAttributes();
public ImmutableDictionary<uint, GetDanOdaiResponse.OdaiData> GetDanDataDictionary();
public ImmutableDictionary<uint, GetSongIntroductionResponse.SongIntroductionData> GetSongIntroDictionary();
}

View File

@ -17,4 +17,6 @@ public interface IUserDatumService
public Task<List<uint>> GetFavoriteSongIds(uint baid); public Task<List<uint>> GetFavoriteSongIds(uint baid);
public Task UpdateFavoriteSong(uint baid, uint songId, bool isFavorite); public Task UpdateFavoriteSong(uint baid, uint songId, bool isFavorite);
} }

View File

@ -35,10 +35,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Update="wwwroot\music_attribute.json"> <Content Update="wwwroot\data\music_attribute.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Update="wwwroot\dan_data.json"> <Content Update="wwwroot\data\dan_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\intro_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1,74 @@
[
{
"setId":1,
"verupNo":1,
"mainSongNo":895,
"subSongNo":[894,732,44,921]
},
{
"setId":2,
"verupNo":1,
"mainSongNo":912,
"subSongNo":[827,871,36,227]
},
{
"setId":3,
"verupNo":1,
"mainSongNo":913,
"subSongNo":[460,916,430,872]
},
{
"setId":4,
"verupNo":1,
"mainSongNo":842,
"subSongNo":[7,233,256,831]
},
{
"setId":5,
"verupNo":1,
"mainSongNo":947,
"subSongNo":[926,882,730,695]
},
{
"setId":6,
"verupNo":1,
"mainSongNo":937,
"subSongNo":[828,925,474,924]
},
{
"setId":7,
"verupNo":1,
"mainSongNo":956,
"subSongNo":[839,255,285,187]
},
{
"setId":8,
"verupNo":1,
"mainSongNo":923,
"subSongNo":[729,873,789,893]
},
{
"setId":9,
"verupNo":1,
"mainSongNo":915,
"subSongNo":[726,811,711,303]
},
{
"setId":10,
"verupNo":1,
"mainSongNo":885,
"subSongNo":[837,464,801,18]
},
{
"setId":11,
"verupNo":1,
"mainSongNo":898,
"subSongNo":[47,135,374,792]
},
{
"setId":12,
"verupNo":1,
"mainSongNo":948,
"subSongNo":[412,538,411,413]
}
]

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ global using Microsoft.AspNetCore.Components.Web;
global using MudBlazor; global using MudBlazor;
global using TaikoWebUI; global using TaikoWebUI;
global using TaikoWebUI.Services; global using TaikoWebUI.Services;
global using TaikoWebUI.Shared;
global using SharedProject.Models; global using SharedProject.Models;
global using SharedProject.Models.Requests; global using SharedProject.Models.Requests;
global using SharedProject.Models.Responses; global using SharedProject.Models.Responses;

View File

@ -0,0 +1,105 @@
@using TaikoWebUI.Shared.Models
@using System.Collections.Immutable
@inject IGameDataService GameDataService
<MudDialog>
<DialogContent>
<MudTable Items="@titles" Filter="@Filter" @bind-SelectedItem="@selectedTitle" Height="40vh" Hover="true">
<ColGroup>
<col style="width: 50px;" />
<col />
</ColGroup>
<ToolBarContent>
<MudTextField @bind-Value="searchString" Placeholder="Search" Adornment="Adornment.Start" Immediate="true"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0">
</MudTextField>
</ToolBarContent>
<HeaderContent>
<MudTh>
<MudTableSortLabel SortBy="@(new Func<Title, object>(x => x.TitleId))">
ID
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel SortBy="@(new Func<Title, object>(x => x.TitleName))">
Title
</MudTableSortLabel>
</MudTh>
</HeaderContent>
<RowTemplate>
@{
// Disable nullable warnings, which seems to be a false positive
# pragma warning disable CS8602
}
<MudTd DataLabel="Id" Class="cursor-pointer">@context.TitleId</MudTd>
<MudTd DataLabel="Title" Class="cursor-pointer">@context.TitleName</MudTd>
@{
#pragma warning restore CS8602
}
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
<MudText Class="mt-4 d-block" Typo="Typo.caption"><b>Selected Title:</b> @selectedTitle?.TitleName</MudText>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Ok</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public UserSetting UserSetting { get; set; } = new();
private IEnumerable<Title> titles = new List<Title>();
private Title? selectedTitle;
private string searchString = string.Empty;
protected override void OnInitialized()
{
base.OnInitialized();
var titleSet = GameDataService.GetTitles();
titles = titleSet.ToImmutableList().Sort((title, title1) => title.TitleId.CompareTo(title1.TitleId));
var currentTitle = new Title
{
TitleName = UserSetting.Title
};
if (titleSet.Contains(currentTitle))
{
titleSet.TryGetValue(new Title
{
TitleName = UserSetting.Title
}, out selectedTitle);
}
}
private bool Filter(Title? title)
{
if (title is null)
{
return false;
}
return string.IsNullOrEmpty(searchString) ||
title.TitleName.Contains(searchString, StringComparison.InvariantCultureIgnoreCase);
}
private void Submit()
{
if (selectedTitle is not null)
{
UserSetting.Title = selectedTitle.TitleName;
}
MudDialog.Close(DialogResult.Ok(true));
}
private void Cancel() => MudDialog.Cancel();
}

View File

@ -1,5 +1,7 @@
@page "/Cards/{baid:int}/Profile" @page "/Cards/{baid:int}/Profile"
@inject HttpClient Client @inject HttpClient Client
@inject IGameDataService GameDataService
@inject IDialogService DialogService
<MudBreadcrumbs Items="breadcrumbs" Class="px-0"></MudBreadcrumbs> <MudBreadcrumbs Items="breadcrumbs" Class="px-0"></MudBreadcrumbs>
@ -8,107 +10,205 @@
@if (response is not null) @if (response is not null)
{ {
<MudGrid> <MudGrid Class="my-4 pb-10">
<MudItem xs="12" md="8"> <MudItem xs="12" md="8">
<MudPaper Class="py-8 px-8 my-8" Outlined="true"> <MudPaper Elevation="0" Outlined="true">
<MudStack Spacing="4"> <MudTabs Rounded="true" Border="true" PanelClass="pa-8">
<h2>Profile Options</h2> <MudTabPanel Text="Profile">
<MudStack Spacing="4">
<h2>Profile Options</h2>
<MudTextField @bind-Value="@response.MyDonName" Label="Name"></MudTextField> <MudTextField @bind-Value="@response.MyDonName" Label="Name"></MudTextField>
<MudGrid> <MudGrid>
<MudItem xs="12" md="8"> <MudItem xs="12" md="8">
<MudTextField @bind-Value="@response.Title" Label="Title"></MudTextField> <MudTextField @bind-Value="@response.Title" Label="Title"/>
</MudItem> <MudButton Color="Color.Primary" Class="mt-1" Size="Size.Small" OnClick="@((e)=>OpenChooseTitleDialog())">
<MudItem xs="12" md="4"> Select a Title
<MudSelect @bind-Value="@response.TitlePlateId" Label="Title Plate"> </MudButton>
@for (uint i = 0; i < 8; i++) </MudItem>
{ <MudItem xs="12" md="4">
var index = i; <MudSelect @bind-Value="@response.TitlePlateId" Label="Title Plate">
<MudSelectItem Value="@i">@titlePlateStrings[index]</MudSelectItem> @for (uint i = 0; i < 8; i++)
}
</MudSelect>
</MudItem>
</MudGrid>
<MudSelect @bind-Value="@response.AchievementDisplayDifficulty"
Label="Achievement Panel Difficulty">
@foreach (var item in Enum.GetValues<Difficulty>())
{
<MudSelectItem Value="@item"/>
}
</MudSelect>
<MudSwitch @bind-Checked="@response.IsDisplayAchievement" Label="Display Achievement Panel" Color="Color.Primary"/>
<MudSwitch @bind-Checked="@response.IsDisplayDanOnNamePlate" Label="Display Dan Rank on Name Plate" Color="Color.Primary"/>
</MudStack>
</MudPaper>
<MudPaper Class="py-8 px-8 my-8" Outlined="true">
<MudStack Spacing="4">
<h2>Song Options</h2>
<MudGrid>
<MudItem xs="12" md="4">
<MudStack Spacing="4">
<MudSwitch @bind-Checked="@response.PlaySetting.IsVanishOn" Label="Vanish" Color="Color.Primary"/>
<MudSwitch @bind-Checked="@response.PlaySetting.IsInverseOn" Label="Inverse" Color="Color.Primary"/>
<MudSwitch @bind-Checked="@response.IsSkipOn" Label="Give Up" Color="Color.Primary"/>
<MudSwitch @bind-Checked="@response.IsVoiceOn" Label="Voice" Color="Color.Primary"/>
</MudStack>
</MudItem>
<MudItem xs="12" md="8">
<MudStack Spacing="4">
<MudSelect @bind-Value="@response.PlaySetting.Speed" Label="Speed">
@for (uint i = 0; i < 15; i++)
{ {
var index = i; var index = i;
<MudSelectItem Value="@i">@speedStrings[index]</MudSelectItem> <MudSelectItem Value="@i">@TitlePlateStrings[index]</MudSelectItem>
} }
</MudSelect> </MudSelect>
</MudItem>
</MudGrid>
<MudSelect @bind-Value="@response.PlaySetting.RandomType" <MudSelect @bind-Value="@response.AchievementDisplayDifficulty"
Label="Random"> Label="Achievement Panel Difficulty">
@foreach (var item in Enum.GetValues<RandomType>()) @foreach (var item in Enum.GetValues<Difficulty>())
{ {
<MudSelectItem Value="@item"/> <MudSelectItem Value="@item"/>
} }
</MudSelect> </MudSelect>
<MudSelect @bind-Value="@response.ToneId" Label="Tone"> <MudSwitch @bind-Checked="@response.IsDisplayAchievement" Label="Display Achievement Panel" Color="Color.Primary"/>
@for (uint i = 0; i < 19; i++) <MudSwitch @bind-Checked="@response.IsDisplayDanOnNamePlate" Label="Display Dan Rank on Name Plate" Color="Color.Primary"/>
{ </MudStack>
var index = i; </MudTabPanel>
<MudSelectItem Value="@i">@toneStrings[index]</MudSelectItem>
}
</MudSelect>
<MudSlider Class="mb-8" @bind-Value="@response.NotesPosition" Size="Size.Medium" Min="-5" Max="5" Step="1" TickMarks="true" TickMarkLabels="@notePositionStrings"> <MudTabPanel Text="Costume">
<MudText Typo="Typo.caption">Notes Position</MudText> <MudStack Spacing="4">
</MudSlider> <h2>Costume Options</h2>
</MudStack> <MudGrid>
</MudItem> <MudItem xs="12">
</MudGrid> <MudStack Spacing="4" Class="mb-8">
</MudStack> <MudSelect @bind-Value="@response.Head" Label="Head">
</MudPaper> @for (var i = 0; i < Constants.COSTUME_HEAD_MAX; i++)
</MudItem> {
<MudItem md="4" xs="12" Class="py-8 px-8 my-4 pt-8"> var index = (uint)i;
<MudStack Spacing="4" Style="top:100px" Class="sticky"> var costumeTitle = GameDataService.GetHeadTitle(index);
<MudButton Disabled="@isSavingOptions" <MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
OnClick="SaveOptions" }
Variant="Variant.Filled" </MudSelect>
Color="Color.Primary">
@if (isSavingOptions) <MudSelect @bind-Value="@response.Body" Label="Body">
{ @for (var i = 0; i < Constants.COSTUME_BODY_MAX; i++)
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true"/> {
<MudText Class="ms-2">Saving...</MudText> var index = (uint)i;
} var costumeTitle = GameDataService.GetBodyTitle(index);
else <MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
{ }
<MudIcon Icon="@Icons.Filled.Save" Class="mx-2"></MudIcon> </MudSelect>
<MudText>Save</MudText>
} <MudSelect @bind-Value="@response.Face" Label="Face">
</MudButton> @for (var i = 0; i < Constants.COSTUME_FACE_MAX; i++)
</MudStack> {
</MudItem> var index = (uint)i;
var costumeTitle = GameDataService.GetFaceTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Kigurumi" Label="Kigurumi">
@for (var i = 0; i < Constants.COSTUME_KIGURUMI_MAX; i++)
{
var index = (uint)i;
var costumeTitle = GameDataService.GetKigurumiTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Puchi" Label="Puchi">
@for (var i = 0; i < Constants.COSTUME_PUCHI_MAX; i++)
{
var index = (uint)i;
var costumeTitle = GameDataService.GetPuchiTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true">
<MudSelect @bind-Value="@response.BodyColor" Label="Body Color">
@for (uint i = 0; i < Constants.COSTUME_COLOR_MAX; i++)
{
var index = i;
<MudSelectItem Value="@index">
<div class="color-box" style=@($"background: {CostumeColors[index]}")></div>
@index
</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.FaceColor" Label="Face Color">
@for (uint i = 0; i < Constants.COSTUME_COLOR_MAX; i++)
{
var index = i;
<MudSelectItem Value="@index">
<div class="color-box" style=@($"background: {CostumeColors[index]}")></div>
@index
</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.LimbColor" Label="Limb Color">
@for (uint i = 0; i < Constants.COSTUME_COLOR_MAX; i++)
{
var index = i;
<MudSelectItem Value="@index">
<div class="color-box" style=@($"background: {CostumeColors[index]}")></div>
@index
</MudSelectItem>
}
</MudSelect>
</MudStack>
</MudItem>
</MudGrid>
</MudStack>
</MudTabPanel>
<MudTabPanel Text="Song Options">
<MudStack Spacing="4">
<h2>Song Options</h2>
<MudGrid>
<MudItem xs="12" md="4">
<MudStack Spacing="4">
<MudSwitch @bind-Checked="@response.PlaySetting.IsVanishOn" Label="Vanish" Color="Color.Primary"/>
<MudSwitch @bind-Checked="@response.PlaySetting.IsInverseOn" Label="Inverse" Color="Color.Primary"/>
<MudSwitch @bind-Checked="@response.IsSkipOn" Label="Give Up" Color="Color.Primary"/>
<MudSwitch @bind-Checked="@response.IsVoiceOn" Label="Voice" Color="Color.Primary"/>
</MudStack>
</MudItem>
<MudItem xs="12" md="8">
<MudStack Spacing="4">
<MudSelect @bind-Value="@response.PlaySetting.Speed" Label="Speed">
@for (uint i = 0; i < 15; i++)
{
var index = i;
<MudSelectItem Value="@i">@SpeedStrings[index]</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.PlaySetting.RandomType"
Label="Random">
@foreach (var item in Enum.GetValues<RandomType>())
{
<MudSelectItem Value="@item"/>
}
</MudSelect>
<MudSelect @bind-Value="@response.ToneId" Label="Tone">
@for (uint i = 0; i < 19; i++)
{
var index = i;
<MudSelectItem Value="@i">@ToneStrings[index]</MudSelectItem>
}
</MudSelect>
<MudSlider Class="mb-8" @bind-Value="@response.NotesPosition" Size="Size.Medium" Min="-5" Max="5" Step="1" TickMarks="true" TickMarkLabels="@NotePositionStrings">
<MudText Typo="Typo.caption">Notes Position</MudText>
</MudSlider>
</MudStack>
</MudItem>
</MudGrid>
</MudStack>
</MudTabPanel>
</MudTabs>
</MudPaper>
</MudItem>
<MudItem md="4" xs="12" Class="py-4 px-8">
<MudStack Style="top:100px" Class="sticky">
<MudButton Disabled="@isSavingOptions"
OnClick="SaveOptions"
Variant="Variant.Filled"
Color="Color.Primary">
@if (isSavingOptions)
{
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true"/>
<MudText Class="ms-2">Saving...</MudText>
}
else
{
<MudIcon Icon="@Icons.Filled.Save" Class="mx-2"></MudIcon>
<MudText>Save</MudText>
}
</MudButton>
</MudStack>
</MudItem>
</MudGrid> </MudGrid>
} }

View File

@ -1,4 +1,6 @@
namespace TaikoWebUI.Pages; using TaikoWebUI.Pages.Dialogs;
namespace TaikoWebUI.Pages;
public partial class Profile public partial class Profile
{ {
@ -9,16 +11,31 @@ public partial class Profile
private bool isSavingOptions; private bool isSavingOptions;
private readonly string[] speedStrings = private static readonly string[] CostumeColors =
{
"#F84828", "#68C0C0", "#DC1500", "#F8F0E0", "#009687", "#00BF87",
"#00FF9A", "#66FFC2", "#FFFFFF", "#690000", "#FF0000", "#FF6666",
"#FFB3B3", "#00BCC2", "#00F7FF", "#66FAFF", "#B3FDFF", "#E4E4E4",
"#993800", "#FF5E00", "#FF9E78", "#FFCFB3", "#005199", "#0088FF",
"#66B8FF", "#B3DBFF", "#B9B9B9", "#B37700", "#FFAA00", "#FFCC66",
"#FFE2B3", "#000C80", "#0019FF", "#6675FF", "#B3BAFF", "#858585",
"#B39B00", "#FFDD00", "#FFFF00", "#FFFF71", "#2B0080", "#5500FF",
"#9966FF", "#CCB3FF", "#505050", "#38A100", "#78C900", "#B3FF00",
"#DCFF8A", "#610080", "#C400FF", "#DC66FF", "#EDB3FF", "#232323",
"#006600", "#00B800", "#00FF00", "#8AFF9E", "#990059", "#FF0095",
"#FF66BF", "#FFB3DF", "#000000"
};
private static readonly string[] SpeedStrings =
{ {
"1.0", "1.1", "1.2", "1.3", "1.4", "1.0", "1.1", "1.2", "1.3", "1.4",
"1.5", "1.6", "1.7", "1.8", "1.9", "1.5", "1.6", "1.7", "1.8", "1.9",
"2.0", "2.5", "3.0", "3.5", "4.0" "2.0", "2.5", "3.0", "3.5", "4.0"
}; };
private readonly string[] notePositionStrings = { "-5", "-4", "-3", "-2", "-1", "0", "+1", "+2", "+3", "+4", "+5" }; private static readonly string[] NotePositionStrings = { "-5", "-4", "-3", "-2", "-1", "0", "+1", "+2", "+3", "+4", "+5" };
private readonly string[] toneStrings = private static readonly string[] ToneStrings =
{ {
"Taiko", "Festival", "Dogs & Cats", "Deluxe", "Taiko", "Festival", "Dogs & Cats", "Deluxe",
"Drumset", "Tambourine", "Don Wada", "Clapping", "Drumset", "Tambourine", "Don Wada", "Clapping",
@ -27,13 +44,13 @@ public partial class Profile
"Synth Drum", "Shuriken", "Bubble Pop", "Electric Guitar" "Synth Drum", "Shuriken", "Bubble Pop", "Electric Guitar"
}; };
private readonly string[] titlePlateStrings = private static readonly string[] TitlePlateStrings =
{ {
"Wood", "Rainbow", "Gold", "Purple", "Wood", "Rainbow", "Gold", "Purple",
"AI 1", "AI 2", "AI 3", "AI 4" "AI 1", "AI 2", "AI 3", "AI 4"
}; };
private List<BreadcrumbItem> breadcrumbs = new() private readonly List<BreadcrumbItem> breadcrumbs = new()
{ {
new BreadcrumbItem("Cards", href: "/Cards"), new BreadcrumbItem("Cards", href: "/Cards"),
}; };
@ -55,4 +72,25 @@ public partial class Profile
isSavingOptions = false; isSavingOptions = false;
} }
private async Task OpenChooseTitleDialog()
{
var options = new DialogOptions
{
//CloseButton = false,
CloseOnEscapeKey = false,
DisableBackdropClick = true,
MaxWidth = MaxWidth.Medium,
FullWidth = true
};
var parameters = new DialogParameters
{
["UserSetting"] = response
};
var dialog = DialogService.Show<ChooseTitleDialog>("Player Titles", parameters, options);
var result = await dialog.Result;
if (!result.Cancelled)
{
StateHasChanged();
}
}
} }

View File

@ -30,30 +30,25 @@
@if (songBestDataMap.ContainsKey(difficulty)) @if (songBestDataMap.ContainsKey(difficulty))
{ {
<MudDataGrid Items="@songBestDataMap[difficulty]" <MudDataGrid Items="@songBestDataMap[difficulty]"
ColumnResizeMode="ResizeMode.Container" RowsPerPage="25" Elevation="0"> ColumnResizeMode="ResizeMode.None" RowsPerPage="25" Elevation="0">
<Columns> <Columns>
<Column T="SongBestData" Field="@nameof(SongBestData.SongId)" Title="Song" StickyLeft="true" Class="clm-row-large"> <Column T="SongBestData" Field="@nameof(SongBestData.SongId)" Title="Song" StickyLeft="true">
<CellTemplate> <CellTemplate>
<MudGrid Justify="Justify.Center"> <MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudItem xs="10"> <div style="width:300px">
<MudStack Spacing="0"> <MudText Typo="Typo.body2" Style="font-weight:bold">@context.Item.MusicName</MudText>
<MudText Typo="Typo.body2" Style="font-weight:bold">@context.Item.MusicName</MudText> <MudText Typo="Typo.caption">@context.Item.MusicArtist</MudText>
<MudText Typo="Typo.caption">@context.Item.MusicArtist</MudText> </div>
</MudStack> <div>
</MudItem> <MudToggleIconButton Toggled="@context.Item.IsFavorite"
<MudItem xs="2"> ToggledChanged="@(async () => await OnFavoriteToggled(context.Item))"
<MudStack Justify="Justify.Center" AlignItems="AlignItems.End" Style="height:100%"> Icon="@Icons.Material.Filled.FavoriteBorder" Color="@Color.Secondary"
<MudTooltip Text="@(context.Item.IsFavorite ? "Remove from favorites" : "Add to favorites")" Arrow="true" Placement="Placement.Top"> ToggledIcon="@Icons.Material.Filled.Favorite" ToggledColor="@Color.Secondary"
<MudToggleIconButton Toggled="@context.Item.IsFavorite" Size="Size.Small"
ToggledChanged="@(async () => await OnFavoriteToggled(context.Item))" ToggledSize="Size.Small"
Icon="@Icons.Material.Filled.FavoriteBorder" Color="@Color.Secondary" Title="Add to favorites" ToggledTitle="Remove from favorites"/>
ToggledIcon="@Icons.Material.Filled.Favorite" ToggledColor="@Color.Secondary" </div>
Size="Size.Small" </MudStack>
ToggledSize="Size.Small"/>
</MudTooltip>
</MudStack>
</MudItem>
</MudGrid>
</CellTemplate> </CellTemplate>
</Column> </Column>
<Column T="SongBestData" Field="@nameof(SongBestData.SongId)" Title="Level" Sortable="false"> <Column T="SongBestData" Field="@nameof(SongBestData.SongId)" Title="Level" Sortable="false">
@ -76,18 +71,14 @@
<Column T="SongBestData" Field="@nameof(SongBestData.BestScore)" Title="Best Score"/> <Column T="SongBestData" Field="@nameof(SongBestData.BestScore)" Title="Best Score"/>
<Column T="SongBestData" Field="@nameof(SongBestData.BestCrown)" Title="Best Crown"> <Column T="SongBestData" Field="@nameof(SongBestData.BestCrown)" Title="Best Crown">
<CellTemplate> <CellTemplate>
<MudTooltip Text="@(GetCrownText(context.Item.BestCrown))" Arrow="true" Placement="Placement.Top"> <img src="@($"/images/crown_{context.Item.BestCrown}.png")" alt="@(GetCrownText(context.Item.BestCrown))" title="@(GetCrownText(context.Item.BestCrown))" style="@IconStyle" />
<img src="@($"/images/crown_{context.Item.BestCrown}.png")" alt="@(context.Item.BestCrown)" style="@IconStyle"/>
</MudTooltip>
</CellTemplate> </CellTemplate>
</Column> </Column>
<Column T="SongBestData" Field="@nameof(SongBestData.BestScoreRank)" Title="Best Rank" Sortable="false"> <Column T="SongBestData" Field="@nameof(SongBestData.BestScoreRank)" Title="Best Rank" Sortable="false">
<CellTemplate> <CellTemplate>
@if (context.Item.BestScoreRank is not ScoreRank.None) @if (context.Item.BestScoreRank is not ScoreRank.None)
{ {
<MudTooltip Text="@(GetRankText(context.Item.BestScoreRank))" Arrow="true" Placement="Placement.Top"> <img src="@($"/images/rank_{context.Item.BestScoreRank}.png")" alt="@(GetRankText(context.Item.BestScoreRank))" title="@(GetRankText(context.Item.BestScoreRank))" style="@IconStyle" />
<img src="@($"/images/rank_{context.Item.BestScoreRank}.png")" alt="@(context.Item.BestScoreRank)" style="@IconStyle"/>
</MudTooltip>
} }
</CellTemplate> </CellTemplate>
</Column> </Column>

View File

@ -6,11 +6,19 @@ namespace TaikoWebUI.Services;
public class GameDataService : IGameDataService public class GameDataService : IGameDataService
{ {
private readonly string[] bodyTitles = new string[Constants.COSTUME_BODY_MAX];
private readonly HttpClient client; private readonly HttpClient client;
private readonly string[] faceTitles = new string[Constants.COSTUME_FACE_MAX];
private readonly string[] headTitles = new string[Constants.COSTUME_HEAD_MAX];
private readonly string[] kigurumiMTitles = new string[Constants.COSTUME_KIGURUMI_MAX];
private readonly Dictionary<uint, MusicDetail> musicMap = new(); private readonly Dictionary<uint, MusicDetail> musicMap = new();
private readonly string[] puchiTitles = new string[Constants.COSTUME_PUCHI_MAX];
private ImmutableDictionary<uint, DanData> danMap = null!; private ImmutableDictionary<uint, DanData> danMap = ImmutableDictionary<uint, DanData>.Empty;
private ImmutableHashSet<Title> titles = ImmutableHashSet<Title>.Empty;
public GameDataService(HttpClient client) public GameDataService(HttpClient client)
{ {
@ -30,15 +38,171 @@ public class GameDataService : IGameDataService
danData.ThrowIfNull(); danData.ThrowIfNull();
danMap = danData.ToImmutableDictionary(data => data.DanId); danMap = danData.ToImmutableDictionary(data => data.DanId);
// To prevent duplicate entries in wordlist // To prevent duplicate entries in wordlist
var dict = wordList.WordListEntries.GroupBy(entry => entry.Key) var dict = wordList.WordListEntries.GroupBy(entry => entry.Key)
.ToImmutableDictionary(group => group.Key, group => group.First()); .ToImmutableDictionary(group => group.Key, group => group.First());
await Task.Run(() => InitializeMusicMap(musicInfo, dict, musicOrder));
await Task.Run(() => InitializeHeadTitles(dict));
await Task.Run(() => InitializeFaceTitles(dict));
await Task.Run(() => InitializeBodyTitles(dict));
await Task.Run(() => InitializePuchiTitles(dict));
await Task.Run(() => InitializeKigurumiTitles(dict));
await Task.Run(() => InitializeTitles(dict));
}
public string GetMusicNameBySongId(uint songId)
{
return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.SongName : string.Empty;
}
public string GetMusicArtistBySongId(uint songId)
{
return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.ArtistName : string.Empty;
}
public SongGenre GetMusicGenreBySongId(uint songId)
{
return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.Genre : SongGenre.Variety;
}
public int GetMusicIndexBySongId(uint songId)
{
return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.Index : int.MaxValue;
}
public DanData GetDanDataById(uint danId)
{
return danMap.GetValueOrDefault(danId, new DanData());
}
public int GetMusicStarLevel(uint songId, Difficulty difficulty)
{
var success = musicMap.TryGetValue(songId, out var musicDetail);
return difficulty switch
{
Difficulty.None => throw new ArgumentException("Difficulty cannot be none"),
Difficulty.Easy => success ? musicDetail!.StarEasy : 0,
Difficulty.Normal => success ? musicDetail!.StarNormal : 0,
Difficulty.Hard => success ? musicDetail!.StarHard : 0,
Difficulty.Oni => success ? musicDetail!.StarOni : 0,
Difficulty.UraOni => success ? musicDetail!.StarUra : 0,
_ => throw new ArgumentOutOfRangeException(nameof(difficulty), difficulty, null)
};
}
public string GetHeadTitle(uint index)
{
return index < headTitles.Length ? headTitles[index] : string.Empty;
}
public string GetKigurumiTitle(uint index)
{
return index < kigurumiMTitles.Length ? kigurumiMTitles[index] : string.Empty;
}
public string GetBodyTitle(uint index)
{
return index < bodyTitles.Length ? bodyTitles[index] : string.Empty;
}
public string GetFaceTitle(uint index)
{
return index < faceTitles.Length ? faceTitles[index] : string.Empty;
}
public string GetPuchiTitle(uint index)
{
return index < puchiTitles.Length ? puchiTitles[index] : string.Empty;
}
public ImmutableHashSet<Title> GetTitles()
{
return titles;
}
private void InitializeTitles(ImmutableDictionary<string, WordListEntry> dict)
{
var set = ImmutableHashSet.CreateBuilder<Title>();
for (var i = 1; i < Constants.PLAYER_TITLE_MAX; i++)
{
var key = $"syougou_{i}";
var titleWordlistItem = dict.GetValueOrDefault(key, new WordListEntry());
set.Add(new Title{
TitleName = titleWordlistItem.JapaneseText,
TitleId = i
});
}
titles = set.ToImmutable();
}
private void InitializePuchiTitles(ImmutableDictionary<string, WordListEntry> dict)
{
for (var i = 0; i < Constants.COSTUME_PUCHI_MAX; i++)
{
var key = $"costume_puchi_{i}";
var costumeWordlistItem = dict.GetValueOrDefault(key, new WordListEntry());
puchiTitles[i] = costumeWordlistItem.JapaneseText;
}
}
private void InitializeKigurumiTitles(ImmutableDictionary<string, WordListEntry> dict)
{
for (var i = 0; i < Constants.COSTUME_KIGURUMI_MAX; i++)
{
var key = $"costume_kigurumi_{i}";
var costumeWordlistItem = dict.GetValueOrDefault(key, new WordListEntry());
kigurumiMTitles[i] = costumeWordlistItem.JapaneseText;
}
}
private void InitializeBodyTitles(ImmutableDictionary<string, WordListEntry> dict)
{
for (var i = 0; i < Constants.COSTUME_BODY_MAX; i++)
{
var key = $"costume_body_{i}";
var costumeWordlistItem = dict.GetValueOrDefault(key, new WordListEntry());
bodyTitles[i] = costumeWordlistItem.JapaneseText;
}
}
private void InitializeFaceTitles(ImmutableDictionary<string, WordListEntry> dict)
{
for (var i = 0; i < Constants.COSTUME_FACE_MAX; i++)
{
var key = $"costume_face_{i}";
var costumeWordlistItem = dict.GetValueOrDefault(key, new WordListEntry());
faceTitles[i] = costumeWordlistItem.JapaneseText;
}
}
private void InitializeHeadTitles(ImmutableDictionary<string, WordListEntry> dict)
{
for (var i = 0; i < Constants.COSTUME_HEAD_MAX; i++)
{
var key = $"costume_head_{i}";
var costumeWordlistItem = dict.GetValueOrDefault(key, new WordListEntry());
headTitles[i] = costumeWordlistItem.JapaneseText;
}
}
private void InitializeMusicMap(MusicInfo musicInfo, ImmutableDictionary<string, WordListEntry> dict,
MusicOrder musicOrder)
{
foreach (var music in musicInfo.Items) foreach (var music in musicInfo.Items)
{ {
var songNameKey = $"song_{music.Id}"; var songNameKey = $"song_{music.Id}";
var songArtistKey = $"song_sub_{music.Id}"; var songArtistKey = $"song_sub_{music.Id}";
var musicName = dict.GetValueOrDefault(songNameKey, new WordListEntry()); var musicName = dict.GetValueOrDefault(songNameKey, new WordListEntry());
var musicArtist = dict.GetValueOrDefault(songArtistKey, new WordListEntry()); var musicArtist = dict.GetValueOrDefault(songArtistKey, new WordListEntry());
@ -60,42 +224,4 @@ public class GameDataService : IGameDataService
} }
} }
} }
public string GetMusicNameBySongId(uint songId)
{
return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.SongName : string.Empty;
}
public string GetMusicArtistBySongId(uint songId)
{
return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.ArtistName : string.Empty;
}
public SongGenre GetMusicGenreBySongId(uint songId)
{
return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.Genre : SongGenre.Variety;
}
public int GetMusicIndexBySongId(uint songId)
{
return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.Index : int.MaxValue;
}
public DanData GetDanDataById(uint danId)
{
return danMap.GetValueOrDefault(danId, new DanData());
}
public int GetMusicStarLevel(uint songId, Difficulty difficulty)
{
var success = musicMap.TryGetValue(songId, out var musicDetail);
return difficulty switch
{
Difficulty.None => throw new ArgumentException("Difficulty cannot be none"),
Difficulty.Easy => success ? musicDetail!.StarEasy : 0,
Difficulty.Normal => success ? musicDetail!.StarNormal : 0,
Difficulty.Hard => success ? musicDetail!.StarHard : 0,
Difficulty.Oni => success ? musicDetail!.StarOni : 0,
Difficulty.UraOni => success ? musicDetail!.StarUra : 0,
_ => throw new ArgumentOutOfRangeException(nameof(difficulty), difficulty, null)
};
}
} }

View File

@ -1,4 +1,7 @@
namespace TaikoWebUI.Services; using System.Collections.Immutable;
using TaikoWebUI.Shared.Models;
namespace TaikoWebUI.Services;
public interface IGameDataService public interface IGameDataService
{ {
@ -15,4 +18,12 @@ public interface IGameDataService
public DanData GetDanDataById(uint danId); public DanData GetDanDataById(uint danId);
public int GetMusicStarLevel(uint songId, Difficulty difficulty); public int GetMusicStarLevel(uint songId, Difficulty difficulty);
public string GetHeadTitle(uint index);
public string GetKigurumiTitle(uint index);
public string GetBodyTitle(uint index);
public string GetFaceTitle(uint index);
public string GetPuchiTitle(uint index);
public ImmutableHashSet<Title> GetTitles();
} }

View File

@ -0,0 +1,12 @@
namespace TaikoWebUI.Shared;
public static class Constants
{
public const int COSTUME_HEAD_MAX = 140;
public const int COSTUME_FACE_MAX = 58;
public const int COSTUME_BODY_MAX = 156;
public const int COSTUME_KIGURUMI_MAX = 154;
public const int COSTUME_PUCHI_MAX = 129;
public const int COSTUME_COLOR_MAX = 63;
public const int PLAYER_TITLE_MAX = 750;
}

View File

@ -0,0 +1,23 @@
namespace TaikoWebUI.Shared.Models;
public class Title
{
public int TitleId { get; set; }
public string TitleName { get; init; } = string.Empty;
public override bool Equals(object? obj)
{
if (obj is Title title)
{
return title.TitleName.Equals(TitleName);
}
return false;
}
public override int GetHashCode()
{
return TitleName.GetHashCode();
}
}

View File

@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Autocomplete.Clients" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.7" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.7" PrivateAssets="all" />
<PackageReference Include="MudBlazor" Version="6.0.15" /> <PackageReference Include="MudBlazor" Version="6.0.15" />

View File

@ -18,4 +18,15 @@
.mud-progress-linear.bar-pass-red .mud-typography { .mud-progress-linear.bar-pass-red .mud-typography {
font-weight: bold; font-weight: bold;
color: #333; color: #333;
}
.color-box {
width: 16px;
height: 16px;
border-radius: 9999px;
display: inline-block;
margin-right: 10px;
border: 1px solid black;
position: relative;
top: 2px;
} }