1
0
mirror of synced 2024-11-12 01:20:51 +01:00

Merge pull request #11 from asesidaa/AIBattle

Ai battle
This commit is contained in:
asesidaa 2022-09-24 18:39:30 +08:00 committed by GitHub
commit 799eb85031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 1951 additions and 339 deletions

3
.gitignore vendored
View File

@ -395,4 +395,5 @@ FodyWeavers.xsd
*.msp *.msp
# JetBrains Rider # JetBrains Rider
*.sln.iml *.sln.iml
TaikoLocalServer/wwwroot/data/music_attribute.bin

95
OldSetup.md Normal file
View File

@ -0,0 +1,95 @@
# Old setup
1. Download the latest release of [TaikoArcadeLoader](https://github.com/BroGamer4256/TaikoArcadeLoader) and install it. Make sure to setup TAL using the config.toml and set the `server` parameter to your local IP address.
2. Download the latest release of [TaikoReverseProxy](https://github.com/shiibe/TaikoReverseProxy).
3. In the `Data\x64\datatable` folder of the game, find the following files:
```
music_attribute.bin
musicinfo.bin
music_order.bin
wordlist.bin
```
Extract them (you can use [7zip](https://www.7-zip.org)) and rename the extracted file like so:
```
music_attribute -> music_attribute.json
musicinfo -> musicinfo.json
music_order -> music_order.json
wordlist -> wordlist.json
```
Then put these in TaikoLocalServer's `wwwroot/data` folder.
4. Modify hosts, add the following entries (this step can be done automatically with TaikoReverseProxy, check the config for it):
```
server.ip tenporouter.loc
server.ip naominet.jp
server.ip v402-front.mucha-prd.nbgi-amnet.jp
server.ip vsapi.taiko-p.jp
```
where `server.ip` is your computer's ip (or the server's ip)
5. 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. Run TaikoReverseProxy and TaikoLocalServer, then run the game. You can access the WebUI by going to `https://naominet.jp:10122/` in your browser.
### Server setup (for TAL<2.00 or other loaders)
1. Download the server from release page, extract anywhere
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 naominet.jp
server.ip v402-front.mucha-prd.nbgi-amnet.jp
server.ip vsapi.taiko-p.jp
```
where `server.ip` is your computers ip (or the server's ip)
4. Setup [TaikoReverseProxy](https://github.com/shiibe/TaikoReverseProxy) or [Apache](#apache-setup-optional) as reverse proxy.
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. 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. 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 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.
### Apache Setup (Optional)
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
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.
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)

106
README.md
View File

@ -8,106 +8,38 @@ This is a server for Taiko no Tatsujin Nijiiro ver 08.18
- A working game, with dongle and card reader emulation. You can use [TaikoArcadeLoader](https://github.com/BroGamer4256/TaikoArcadeLoader) for these if you haven't. - A working game, with dongle and card reader emulation. You can use [TaikoArcadeLoader](https://github.com/BroGamer4256/TaikoArcadeLoader) for these if you haven't.
### Tools
- [TaikoArcadeLoader](https://github.com/BroGamer4256/TaikoArcadeLoader): A loader for the game with hardware emulation and other fixes.
- [TaikoReverseProxy](https://github.com/shiibe/TaikoReverseProxy): A no-setup bundled proxy server, use as a user-friendly alternative to Apache.
### Quick Setup ### Quick Setup
With the newest release (>=2.00) of TaikoArcadeLoader, you no longer need to run AMAuthd or AMUpdater. With the newest release (from [this](https://github.com/BroGamer4256/TaikoArcadeLoader/tree/95d633850d89cb7099e98ffe74cd23632fe26e56) commit) of TaikoArcadeLoader, you no longer need to run AMAuthd, AMUpdater or reverse proxy.
1. Download the latest release of [TaikoArcadeLoader](https://github.com/BroGamer4256/TaikoArcadeLoader) and install it. Make sure to setup TAL using the config.toml and set the `server` parameter to your local IP address. 1. Extract the server release anywhere
2. Download the latest release of [TaikoReverseProxy](https://github.com/shiibe/TaikoReverseProxy).
3. In the `Data\x64\datatable` folder of the game, find the following files:
``` 2. From the game files, copy `music_attribute.bin`, `music_order.bin`, `musicinfo.bin` and `wordlist.bin` to `wwwroot/data` folder.
music_attribute.bin
musicinfo.bin
music_order.bin
wordlist.bin
```
Extract them (you can use [7zip](https://www.7-zip.org)) and rename the extracted file like so: 3. (Optional) Instead of direct copy, extract the specified game files (using 7zip), rename them by adding the file extension `.json` and copy the jsons over.
``` 4. (Optional) In `Certificates` folder, import `root.pfx` to trusted root store, `cert.pfx` to personal store. All the other import options can be kept default.
music_attribute -> music_attribute.json
musicinfo -> musicinfo.json
music_order -> music_order.json
wordlist -> wordlist.json
```
Then put these in TaikoLocalServer's `wwwroot/data` folder. 5. Visit http://localhost:5000 and (Optional, only if you have done step 4) https://localhost:10122, if the web ui starts without errors, the config is fine.
4. Modify hosts, add the following entries (this step can be done automatically with TaikoReverseProxy, check the config for it): 6. Modify comfig.toml from TAL, edit the following line:
``` ```toml
server.ip tenporouter.loc server = "https://divamodarchive.com" # Change https://divamodarchive.com to your/server's ip, like 192.168.1.100
server.ip naominet.jp
server.ip v402-front.mucha-prd.nbgi-amnet.jp
server.ip vsapi.taiko-p.jp
``` ```
where `server.ip` is your computer's ip (or the server's ip) 7. Now the game should be able to connect.
5. Open command prompt as admin, navigate to game root folder (where init.ps1 is). Run `regsvr32 .\AMCUS\iauthdll.dll`. It should prompt about success ### About certificates
6. Run TaikoReverseProxy and TaikoLocalServer, then run the game. You can access the WebUI by going to `https://naominet.jp:10122/` in your browser. If you want to change the certificate used by server, just replace `cert.pfx` in `Certificates` folder. The bundled certificate includes the following DNS names:
```
### Server setup (for TAL<2.00 or other loaders) DNS Name=nbgi-amnet.jp
DNS Name=v402-front.mucha-prd.nbgi-amnet.jp
1. Download the server from release page, extract anywhere DNS Name=*.mucha-prd.nbgi-amnet.jp
DNS Name=localhost
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. DNS Name=vsapi.taiko-p.jp
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 naominet.jp
server.ip v402-front.mucha-prd.nbgi-amnet.jp
server.ip vsapi.taiko-p.jp
```
where `server.ip` is your computers ip (or the server's ip)
4. Setup [TaikoReverseProxy](https://github.com/shiibe/TaikoReverseProxy) or [Apache](#apache-setup-optional) as reverse proxy.
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. 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. 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 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.
### Apache Setup (Optional)
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
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. You will need to modify hosts file to use them.
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)

View File

@ -5,5 +5,5 @@ public enum PlayMode
Normal = 0, Normal = 0,
DanMode = 1, DanMode = 1,
// Not sure about this // Not sure about this
AiBattle = 2 AiBattle = 6
} }

View File

@ -0,0 +1,22 @@
using SharedProject.Enums;
namespace SharedProject.Models;
public class AiSectionBestData
{
public int SectionIndex { get; set; }
public CrownType Crown { get; set; }
public bool IsWin { get; set; }
public uint Score { get; set; }
public uint GoodCount { get; set; }
public uint OkCount { get; set; }
public uint MissCount { get; set; }
public uint DrumrollCount { get; set; }
}

View File

@ -14,6 +14,11 @@ public class SongBestData
public Difficulty Difficulty { get; set; } public Difficulty Difficulty { get; set; }
public int PlayCount { get; set; }
public int ClearCount { get; set; }
public int FullComboCount { get; set; }
public int PerfectCount { get; set; }
public uint BestScore { get; set; } public uint BestScore { get; set; }
public uint BestRate { get; set; } public uint BestRate { get; set; }
@ -37,4 +42,8 @@ public class SongBestData
public uint HitCount { get; set; } public uint HitCount { get; set; }
public uint DrumrollCount { get; set; } public uint DrumrollCount { get; set; }
public List<AiSectionBestData> AiSectionBestData { get; set; } = new();
public bool ShowAiData { get; set; }
} }

View File

@ -0,0 +1,14 @@
namespace SharedProject.Utils;
public static class ValueHelpers
{
public static T Min<T>(T a, T b) where T : IComparable
{
return Comparer<T>.Default.Compare(a, b) <= 0 ? a : b;
}
public static T Max<T>(T a, T b) where T : IComparable
{
return Comparer<T>.Default.Compare(a, b) >= 0 ? a : b;
}
}

Binary file not shown.

View File

@ -4,21 +4,14 @@ public static class Constants
{ {
public const string DATE_TIME_FORMAT = "yyyyMMddHHmmss"; public const string DATE_TIME_FORMAT = "yyyyMMddHHmmss";
public const int MUSIC_ID_MAX = 9000; public const int MUSIC_ID_MAX = 1599;
public const int CROWN_FLAG_ARRAY_SIZE = MUSIC_ID_MAX + 1; public const int MUSIC_ID_MAX_EXPANDED = 9000;
public const int DONDAFUL_CROWN_FLAG_ARRAY_SIZE = MUSIC_ID_MAX + 1;
public const int KIWAMI_SCORE_RANK_ARRAY_SIZE = MUSIC_ID_MAX + 1;
public const int IKI_CORE_RANK_ARRAY_SIZE = MUSIC_ID_MAX + 1;
public const int MIYABI_CORE_RANK_ARRAY_SIZE = MUSIC_ID_MAX + 1;
public const string DEFAULT_DB_NAME = "taiko.db3"; public const string DEFAULT_DB_NAME = "taiko.db3";
public const string MUSIC_ATTRIBUTE_FILE_NAME = "music_attribute.json"; public const string MUSIC_ATTRIBUTE_FILE_NAME = "music_attribute.json";
public const string MUSIC_ATTRIBUTE_COMPRESSED_FILE_NAME = "music_attribute.bin";
public const string DAN_DATA_FILE_NAME = "dan_data.json"; public const string DAN_DATA_FILE_NAME = "dan_data.json";

View File

@ -116,7 +116,7 @@ public static class FlagCalculator
{ {
if (id >= bitArraySize) if (id >= bitArraySize)
{ {
logger.LogWarning("Id out of range!"); logger.LogWarning("Id {Id} out of range!", id);
continue; continue;
} }
bitSet.Set((int)id, true); bitSet.Set((int)id, true);

View File

@ -4,6 +4,8 @@ public partial class TaikoDbContext
{ {
public virtual DbSet<DanScoreDatum> DanScoreData { get; set; } = null!; public virtual DbSet<DanScoreDatum> DanScoreData { get; set; } = null!;
public virtual DbSet<DanStageScoreDatum> DanStageScoreData { get; set; } = null!; public virtual DbSet<DanStageScoreDatum> DanStageScoreData { get; set; } = null!;
public virtual DbSet<AiScoreDatum> AiScoreData { get; set; } = null!;
public virtual DbSet<AiSectionScoreDatum> AiSectionScoreData { get; set; } = null!;
partial void OnModelCreatingPartial(ModelBuilder modelBuilder) partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
{ {
@ -30,5 +32,28 @@ public partial class TaikoDbContext
.HasForeignKey(d => new {d.Baid, d.DanId}) .HasForeignKey(d => new {d.Baid, d.DanId})
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity<AiScoreDatum>(entity =>
{
entity.HasKey(e => new { e.Baid, e.SongId, e.Difficulty });
entity.HasOne(d => d.Ba)
.WithMany()
.HasPrincipalKey(p => p.Baid)
.HasForeignKey(d => d.Baid)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<AiSectionScoreDatum>(entity =>
{
entity.HasKey(e => new { e.Baid, e.SongId, e.Difficulty, e.SectionIndex });
entity.HasOne(d => d.Parent)
.WithMany(p => p.AiSectionScoreData)
.HasPrincipalKey(p => new {p.Baid, p.SongId, p.Difficulty })
.HasForeignKey(d => new {d.Baid, d.SongId, d.Difficulty })
.OnDelete(DeleteBehavior.Cascade);
});
} }
} }

View File

@ -7,9 +7,9 @@ namespace TaikoLocalServer.Controllers.AmAuth;
[Route("/sys/servlet/PowerOn")] [Route("/sys/servlet/PowerOn")]
public class PowerOnController : BaseController<PowerOnController> public class PowerOnController : BaseController<PowerOnController>
{ {
private readonly UrlSettings settings; private readonly ServerSettings settings;
public PowerOnController(IOptions<UrlSettings> settings) public PowerOnController(IOptions<ServerSettings> settings)
{ {
this.settings = settings.Value; this.settings = settings.Value;
} }

View File

@ -5,9 +5,9 @@ namespace TaikoLocalServer.Controllers.AmUpdater;
public class MuchaController : BaseController<MuchaController> public class MuchaController : BaseController<MuchaController>
{ {
private readonly UrlSettings settings; private readonly ServerSettings settings;
public MuchaController(IOptions<UrlSettings> settings) public MuchaController(IOptions<ServerSettings> settings)
{ {
this.settings = settings.Value; this.settings = settings.Value;
} }

View File

@ -1,6 +1,4 @@
using TaikoLocalServer.Services.Interfaces; namespace TaikoLocalServer.Controllers.Api;
namespace TaikoLocalServer.Controllers.Api;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]

View File

@ -1,7 +1,6 @@
using SharedProject.Models; using SharedProject.Models;
using SharedProject.Models.Responses; using SharedProject.Models.Responses;
using Swan.Mapping; using Swan.Mapping;
using TaikoLocalServer.Services.Interfaces;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;

View File

@ -1,7 +1,4 @@
using SharedProject.Models; using SharedProject.Models.Responses;
using SharedProject.Models.Responses;
using Swan.Mapping;
using TaikoLocalServer.Services.Interfaces;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;

View File

@ -1,5 +1,4 @@
using SharedProject.Models.Requests; using SharedProject.Models.Requests;
using TaikoLocalServer.Services.Interfaces;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;

View File

@ -1,5 +1,5 @@
using SharedProject.Models.Responses; using System.Collections.Immutable;
using TaikoLocalServer.Services.Interfaces; using SharedProject.Models.Responses;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;
@ -11,10 +11,14 @@ public class PlayDataController : BaseController<PlayDataController>
private readonly ISongBestDatumService songBestDatumService; private readonly ISongBestDatumService songBestDatumService;
public PlayDataController(IUserDatumService userDatumService, ISongBestDatumService songBestDatumService) private readonly ISongPlayDatumService songPlayDatumService;
public PlayDataController(IUserDatumService userDatumService, ISongBestDatumService songBestDatumService,
ISongPlayDatumService songPlayDatumService)
{ {
this.userDatumService = userDatumService; this.userDatumService = userDatumService;
this.songBestDatumService = songBestDatumService; this.songBestDatumService = songBestDatumService;
this.songPlayDatumService = songPlayDatumService;
} }
[HttpGet("{baid}")] [HttpGet("{baid}")]
@ -27,6 +31,16 @@ public class PlayDataController : BaseController<PlayDataController>
} }
var songBestRecords = await songBestDatumService.GetAllSongBestAsModel(baid); var songBestRecords = await songBestDatumService.GetAllSongBestAsModel(baid);
var playLogs = await songPlayDatumService.GetSongPlayDatumByBaid(baid);
foreach (var songBestData in songBestRecords)
{
var songPlayLogs = playLogs.Where(datum => datum.SongId == songBestData.SongId &&
datum.Difficulty == songBestData.Difficulty).ToList();
songBestData.PlayCount = songPlayLogs.Count;
songBestData.ClearCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Clear);
songBestData.FullComboCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Gold);
songBestData.PerfectCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Dondaful);
}
var favoriteSongs = await userDatumService.GetFavoriteSongIds(baid); var favoriteSongs = await userDatumService.GetFavoriteSongIds(baid);
var favoriteSet = favoriteSongs.ToHashSet(); var favoriteSet = favoriteSongs.ToHashSet();
foreach (var songBestRecord in songBestRecords.Where(songBestRecord => favoriteSet.Contains(songBestRecord.SongId))) foreach (var songBestRecord in songBestRecords.Where(songBestRecord => favoriteSet.Contains(songBestRecord.SongId)))

View File

@ -1,11 +1,6 @@
using System.Buffers.Binary; using System.Text.Json;
using System.Text.Json;
using SharedProject.Models; using SharedProject.Models;
using SharedProject.Models.Responses;
using SharedProject.Utils; using SharedProject.Utils;
using TaikoLocalServer.Services;
using TaikoLocalServer.Services.Interfaces;
using Throw;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;

View File

@ -1,5 +1,4 @@
using System.Text.Json; using System.Text.Json;
using TaikoLocalServer.Services.Interfaces;
using Throw; using Throw;
namespace TaikoLocalServer.Controllers.Game; namespace TaikoLocalServer.Controllers.Game;
@ -16,13 +15,16 @@ public class BaidController : BaseController<BaidController>
private readonly IDanScoreDatumService danScoreDatumService; private readonly IDanScoreDatumService danScoreDatumService;
private readonly IAiDatumService aiDatumService;
public BaidController(IUserDatumService userDatumService, ICardService cardService, public BaidController(IUserDatumService userDatumService, ICardService cardService,
ISongBestDatumService songBestDatumService, IDanScoreDatumService danScoreDatumService) ISongBestDatumService songBestDatumService, IDanScoreDatumService danScoreDatumService, IAiDatumService aiDatumService)
{ {
this.userDatumService = userDatumService; this.userDatumService = userDatumService;
this.cardService = cardService; this.cardService = cardService;
this.songBestDatumService = songBestDatumService; this.songBestDatumService = songBestDatumService;
this.danScoreDatumService = danScoreDatumService; this.danScoreDatumService = danScoreDatumService;
this.aiDatumService = aiDatumService;
} }
@ -112,6 +114,11 @@ public class BaidController : BaseController<BaidController>
var genericInfoFlgLength = genericInfoFlg.Any()? genericInfoFlg.Max() + 1 : 0; var genericInfoFlgLength = genericInfoFlg.Any()? genericInfoFlg.Max() + 1 : 0;
var genericInfoFlgArray = FlagCalculator.GetBitArrayFromIds(genericInfoFlg, (int)genericInfoFlgLength, Logger); var genericInfoFlgArray = FlagCalculator.GetBitArrayFromIds(genericInfoFlg, (int)genericInfoFlgLength, Logger);
var aiRank = (uint)(userData.AiWinCount / 10);
if (aiRank > 11)
{
aiRank = 11;
}
response = new BAIDResponse response = new BAIDResponse
{ {
Result = 1, Result = 1,
@ -158,8 +165,8 @@ public class BaidController : BaseController<BaidController>
IsDispAchievementTypeSet = true, IsDispAchievementTypeSet = true,
LastPlayMode = userData.LastPlayMode, LastPlayMode = userData.LastPlayMode,
IsDispSouuchiOn = true, IsDispSouuchiOn = true,
AiRank = 0, AiRank = aiRank,
AiTotalWin = 0, AiTotalWin = (uint)userData.AiWinCount,
Accesstoken = "123456", Accesstoken = "123456",
ContentInfo = GZipBytesUtil.GetGZipBytes(new byte[10]) ContentInfo = GZipBytesUtil.GetGZipBytes(new byte[10])
}; };

View File

@ -1,4 +1,6 @@
using TaikoLocalServer.Services.Interfaces; using System.Runtime.InteropServices;
using Microsoft.Extensions.Options;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Game; namespace TaikoLocalServer.Controllers.Game;
@ -8,9 +10,12 @@ public class CrownsDataController : BaseController<CrownsDataController>
{ {
private readonly ISongBestDatumService songBestDatumService; private readonly ISongBestDatumService songBestDatumService;
public CrownsDataController(ISongBestDatumService songBestDatumService) private readonly ServerSettings settings;
public CrownsDataController(ISongBestDatumService songBestDatumService, IOptions<ServerSettings> settings)
{ {
this.songBestDatumService = songBestDatumService; this.songBestDatumService = songBestDatumService;
this.settings = settings.Value;
} }
[HttpPost] [HttpPost]
@ -21,10 +26,11 @@ public class CrownsDataController : BaseController<CrownsDataController>
var songBestData = await songBestDatumService.GetAllSongBestData(request.Baid); var songBestData = await songBestDatumService.GetAllSongBestData(request.Baid);
var crown = new ushort[Constants.CROWN_FLAG_ARRAY_SIZE]; var songIdMax = settings.EnableMoreSongs ? Constants.MUSIC_ID_MAX_EXPANDED : Constants.MUSIC_ID_MAX;
var dondafulCrown = new byte[Constants.DONDAFUL_CROWN_FLAG_ARRAY_SIZE]; var crown = new ushort[songIdMax + 1];
var dondafulCrown = new byte[songIdMax + 1];
for (var songId = 0; songId < Constants.MUSIC_ID_MAX; songId++) for (var songId = 0; songId < songIdMax; songId++)
{ {
var id = songId; var id = songId;
dondafulCrown[songId] = songBestData dondafulCrown[songId] = songBestData
@ -41,7 +47,7 @@ public class CrownsDataController : BaseController<CrownsDataController>
// Calculate flag according to difficulty // Calculate flag according to difficulty
.Aggregate((ushort)0, (flag, datum) => FlagCalculator.ComputeCrownFlag(flag, datum.BestCrown, datum.Difficulty)); .Aggregate((ushort)0, (flag, datum) => FlagCalculator.ComputeCrownFlag(flag, datum.BestCrown, datum.Difficulty));
} }
var response = new CrownsDataResponse var response = new CrownsDataResponse
{ {
Result = 1, Result = 1,

View File

@ -1,21 +1,32 @@
namespace TaikoLocalServer.Controllers.Game; using Throw;
namespace TaikoLocalServer.Controllers.Game;
[Route("/v12r03/chassis/getaidata.php")] [Route("/v12r03/chassis/getaidata.php")]
[ApiController] [ApiController]
public class GetAiDataController : BaseController<GetAiDataController> public class GetAiDataController : BaseController<GetAiDataController>
{ {
private readonly IUserDatumService userDatumService;
public GetAiDataController(IUserDatumService userDatumService)
{
this.userDatumService = userDatumService;
}
[HttpPost] [HttpPost]
[Produces("application/protobuf")] [Produces("application/protobuf")]
public IActionResult GetAiData([FromBody] GetAiDataRequest request) public async Task<IActionResult> GetAiData([FromBody] GetAiDataRequest request)
{ {
Logger.LogInformation("GetAiData request : {Request}", request.Stringify()); Logger.LogInformation("GetAiData request : {Request}", request.Stringify());
var user = await userDatumService.GetFirstUserDatumOrNull(request.Baid);
user.ThrowIfNull($"User with baid {request.Baid} does not exist!");
var response = new GetAiDataResponse var response = new GetAiDataResponse
{ {
Result = 1, Result = 1,
TotalWinnings = 1, TotalWinnings = (uint)user.AiWinCount,
InputMedian = "1", InputMedian = "1000",
InputVariance = "0.576389" InputVariance = "2000"
}; };
return Ok(response); return Ok(response);

View File

@ -1,12 +1,21 @@
namespace TaikoLocalServer.Controllers.Game; using Throw;
namespace TaikoLocalServer.Controllers.Game;
[Route("/v12r03/chassis/getaiscore.php")] [Route("/v12r03/chassis/getaiscore.php")]
[ApiController] [ApiController]
public class GetAiScoreController : BaseController<GetAiScoreController> public class GetAiScoreController : BaseController<GetAiScoreController>
{ {
private readonly IAiDatumService aiDatumService;
public GetAiScoreController(IAiDatumService aiDatumService)
{
this.aiDatumService = aiDatumService;
}
[HttpPost] [HttpPost]
[Produces("application/protobuf")] [Produces("application/protobuf")]
public IActionResult GetAiScore([FromBody] GetAiScoreRequest request) public async Task<IActionResult> GetAiScore([FromBody] GetAiScoreRequest request)
{ {
Logger.LogInformation("GetAiScore request : {Request}", request.Stringify()); Logger.LogInformation("GetAiScore request : {Request}", request.Stringify());
@ -15,9 +24,33 @@ public class GetAiScoreController : BaseController<GetAiScoreController>
Result = 1 Result = 1
}; };
var difficulty = (Difficulty)request.Level;
difficulty.Throw().IfOutOfRange();
var aiData = await aiDatumService.GetSongAiScore(request.Baid, request.SongNo, difficulty);
if (aiData is null)
{
return Ok(response);
}
for (var index = 0; index < aiData.AiSectionScoreData.Count; index++)
{
var sectionScoreDatum = aiData.AiSectionScoreData[index];
response.AryBestSectionDatas.Add(new GetAiScoreResponse.AiBestSectionData
{
Crown = (uint)sectionScoreDatum.Crown,
GoodCnt = sectionScoreDatum.GoodCount,
OkCnt = sectionScoreDatum.OkCount,
NgCnt = sectionScoreDatum.MissCount,
PoundCnt = sectionScoreDatum.DrumrollCount,
Score = sectionScoreDatum.Score,
SectionNo = (uint)index
});
}
// There's either 3 or 5 total sections // There's either 3 or 5 total sections
// SectionNo doesn't seem to actually affect which section is being assigned to, only the List order matters // SectionNo doesn't seem to actually affect which section is being assigned to, only the List order matters
response.AryBestSectionDatas.Add(new GetAiScoreResponse.AiBestSectionData() /*response.AryBestSectionDatas.Add(new GetAiScoreResponse.AiBestSectionData()
{ {
SectionNo = 1, SectionNo = 1,
Crown = (uint)CrownType.Clear, Crown = (uint)CrownType.Clear,
@ -66,7 +99,7 @@ public class GetAiScoreController : BaseController<GetAiScoreController>
OkCnt = 50, OkCnt = 50,
NgCnt = 25, NgCnt = 25,
PoundCnt = 12 PoundCnt = 12
}); });*/
return Ok(response); return Ok(response);
} }

View File

@ -1,6 +1,4 @@
using TaikoLocalServer.Services.Interfaces; namespace TaikoLocalServer.Controllers.Game;
namespace TaikoLocalServer.Controllers.Game;
[Route("/v12r03/chassis/getdanodai.php")] [Route("/v12r03/chassis/getdanodai.php")]
[ApiController] [ApiController]

View File

@ -1,6 +1,4 @@
using TaikoLocalServer.Services.Interfaces; namespace TaikoLocalServer.Controllers.Game;
namespace TaikoLocalServer.Controllers.Game;
[Route("/v12r03/chassis/getdanscore.php")] [Route("/v12r03/chassis/getdanscore.php")]
[ApiController] [ApiController]

View File

@ -1,4 +1,5 @@
using TaikoLocalServer.Services.Interfaces; using Microsoft.Extensions.Options;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Game; namespace TaikoLocalServer.Controllers.Game;
@ -7,10 +8,13 @@ namespace TaikoLocalServer.Controllers.Game;
public class GetScoreRankController : BaseController<GetScoreRankController> public class GetScoreRankController : BaseController<GetScoreRankController>
{ {
private readonly ISongBestDatumService songBestDatumService; private readonly ISongBestDatumService songBestDatumService;
private readonly ServerSettings settings;
public GetScoreRankController(ISongBestDatumService songBestDatumService) public GetScoreRankController(ISongBestDatumService songBestDatumService, IOptions<ServerSettings> settings)
{ {
this.songBestDatumService = songBestDatumService; this.songBestDatumService = songBestDatumService;
this.settings = settings.Value;
} }
[HttpPost] [HttpPost]
@ -18,12 +22,13 @@ public class GetScoreRankController : BaseController<GetScoreRankController>
public async Task<IActionResult> GetScoreRank([FromBody] GetScoreRankRequest request) public async Task<IActionResult> GetScoreRank([FromBody] GetScoreRankRequest request)
{ {
Logger.LogInformation("GetScoreRank request : {Request}", request.Stringify()); Logger.LogInformation("GetScoreRank request : {Request}", request.Stringify());
var kiwamiScores = new byte[Constants.KIWAMI_SCORE_RANK_ARRAY_SIZE]; var songIdMax = settings.EnableMoreSongs ? Constants.MUSIC_ID_MAX_EXPANDED : Constants.MUSIC_ID_MAX;
var miyabiScores = new ushort[Constants.MIYABI_CORE_RANK_ARRAY_SIZE]; var kiwamiScores = new byte[songIdMax + 1];
var ikiScores = new ushort[Constants.IKI_CORE_RANK_ARRAY_SIZE]; var miyabiScores = new ushort[songIdMax + 1];
var ikiScores = new ushort[songIdMax + 1];
var songBestData = await songBestDatumService.GetAllSongBestData(request.Baid); var songBestData = await songBestDatumService.GetAllSongBestData(request.Baid);
for (var songId = 0; songId < Constants.MUSIC_ID_MAX; songId++) for (var songId = 0; songId < songIdMax; songId++)
{ {
var id = songId; var id = songId;
kiwamiScores[songId] = songBestData kiwamiScores[songId] = songBestData

View File

@ -1,6 +1,4 @@
using TaikoLocalServer.Services.Interfaces; namespace TaikoLocalServer.Controllers.Game;
namespace TaikoLocalServer.Controllers.Game;
[Route("/v12r03/chassis/getsongintroduction.php")] [Route("/v12r03/chassis/getsongintroduction.php")]
[ApiController] [ApiController]

View File

@ -1,4 +1,5 @@
using TaikoLocalServer.Services.Interfaces; using Microsoft.Extensions.Options;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Game; namespace TaikoLocalServer.Controllers.Game;
@ -8,9 +9,12 @@ public class InitialDataCheckController : BaseController<InitialDataCheckControl
{ {
private readonly IGameDataService gameDataService; private readonly IGameDataService gameDataService;
public InitialDataCheckController(IGameDataService gameDataService) private readonly ServerSettings settings;
public InitialDataCheckController(IGameDataService gameDataService, IOptions<ServerSettings> settings)
{ {
this.gameDataService = gameDataService; this.gameDataService = gameDataService;
this.settings = settings.Value;
} }
[HttpPost] [HttpPost]
@ -19,8 +23,9 @@ public class InitialDataCheckController : BaseController<InitialDataCheckControl
{ {
Logger.LogInformation("Initial data check request: {Request}", request.Stringify()); Logger.LogInformation("Initial data check request: {Request}", request.Stringify());
var songIdMax = settings.EnableMoreSongs ? Constants.MUSIC_ID_MAX_EXPANDED : Constants.MUSIC_ID_MAX;
var enabledArray = var enabledArray =
FlagCalculator.GetBitArrayFromIds(gameDataService.GetMusicList(), Constants.MUSIC_ID_MAX, Logger); FlagCalculator.GetBitArrayFromIds(gameDataService.GetMusicList(), songIdMax, Logger);
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++)
@ -46,7 +51,7 @@ public class InitialDataCheckController : BaseController<InitialDataCheckControl
{ {
Result = 1, Result = 1,
IsDanplay = true, IsDanplay = true,
IsAibattle = false, IsAibattle = true,
IsClose = false, IsClose = false,
DefaultSongFlg = enabledArray, DefaultSongFlg = enabledArray,
AchievementSongBit = enabledArray, AchievementSongBit = enabledArray,

View File

@ -1,7 +1,4 @@
using TaikoLocalServer.Services; namespace TaikoLocalServer.Controllers.Game;
using TaikoLocalServer.Services.Interfaces;
namespace TaikoLocalServer.Controllers.Game;
[Route("/v12r03/chassis/mydonentry.php")] [Route("/v12r03/chassis/mydonentry.php")]
[ApiController] [ApiController]

View File

@ -1,7 +1,6 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
using TaikoLocalServer.Services.Interfaces;
using Throw; using Throw;
namespace TaikoLocalServer.Controllers.Game; namespace TaikoLocalServer.Controllers.Game;
@ -20,13 +19,16 @@ public class PlayResultController : BaseController<PlayResultController>
private readonly IDanScoreDatumService danScoreDatumService; private readonly IDanScoreDatumService danScoreDatumService;
private readonly IAiDatumService aiDatumService;
public PlayResultController(IUserDatumService userDatumService, ISongPlayDatumService songPlayDatumService, public PlayResultController(IUserDatumService userDatumService, ISongPlayDatumService songPlayDatumService,
ISongBestDatumService songBestDatumService, IDanScoreDatumService danScoreDatumService) ISongBestDatumService songBestDatumService, IDanScoreDatumService danScoreDatumService, IAiDatumService aiDatumService)
{ {
this.userDatumService = userDatumService; this.userDatumService = userDatumService;
this.songPlayDatumService = songPlayDatumService; this.songPlayDatumService = songPlayDatumService;
this.songBestDatumService = songBestDatumService; this.songBestDatumService = songBestDatumService;
this.danScoreDatumService = danScoreDatumService; this.danScoreDatumService = danScoreDatumService;
this.aiDatumService = aiDatumService;
} }
[HttpPost] [HttpPost]
@ -77,9 +79,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?
// I have no clue how to update input median or variance
} }
await UpdateBestData(request, stageData, bestData); await UpdateBestData(request, stageData, bestData);
@ -163,7 +163,7 @@ public class PlayResultController : BaseController<PlayResultController>
ComboCount = stageData.ComboCnt, ComboCount = stageData.ComboCnt,
HitCount = stageData.HitCnt, HitCount = stageData.HitCnt,
DrumrollCount = stageData.PoundCnt, DrumrollCount = stageData.PoundCnt,
Crown = PlayResultToCrown(stageData), Crown = PlayResultToCrown(stageData.PlayResult, stageData.OkCnt),
Score = stageData.PlayScore, Score = stageData.PlayScore,
ScoreRate = stageData.ScoreRate, ScoreRate = stageData.ScoreRate,
ScoreRank = (ScoreRank)stageData.ScoreRank, ScoreRank = (ScoreRank)stageData.ScoreRank,
@ -229,6 +229,7 @@ public class PlayResultController : BaseController<PlayResultController>
userdata.GenericInfoFlgArray = userdata.GenericInfoFlgArray =
UpdateJsonUintFlagArray(userdata.GenericInfoFlgArray, playResultData.GetGenericInfoNoes, nameof(userdata.GenericInfoFlgArray)); UpdateJsonUintFlagArray(userdata.GenericInfoFlgArray, playResultData.GetGenericInfoNoes, nameof(userdata.GenericInfoFlgArray));
userdata.AiWinCount += playResultData.AryStageInfoes.Count(data => data.IsWin);
await userDatumService.UpdateUserDatum(userdata); await userDatumService.UpdateUserDatum(userdata);
} }
@ -300,7 +301,7 @@ public class PlayResultController : BaseController<PlayResultController>
}); });
// Determine whether it is dondaful crown as this is not reflected by play result // Determine whether it is dondaful crown as this is not reflected by play result
var crown = PlayResultToCrown(stageData); var crown = PlayResultToCrown(stageData.PlayResult, stageData.OkCnt);
bestDatum.UpdateBestData(crown, stageData.ScoreRank, stageData.PlayScore, stageData.ScoreRate); bestDatum.UpdateBestData(crown, stageData.ScoreRank, stageData.PlayScore, stageData.ScoreRate);
@ -308,23 +309,69 @@ public class PlayResultController : BaseController<PlayResultController>
} }
// TODO: AI battle // TODO: AI battle
/*private async Task UpdateAiBattleData(PlayResultRequest request, StageData stageData) private async Task UpdateAiBattleData(PlayResultRequest request, StageData stageData)
{ {
for (int i = 0; i < stageData.ArySectionDatas.Count; i++) var difficulty = (Difficulty)stageData.Level;
difficulty.Throw().IfOutOfRange();
var existing = await aiDatumService.GetSongAiScore(request.BaidConf,
stageData.SongNo, difficulty);
if (existing is null)
{ {
// Only update crown if it's a higher crown than the previous best crown var aiScoreDatum = new AiScoreDatum
{
Baid = request.BaidConf,
SongId = stageData.SongNo,
Difficulty = difficulty,
IsWin = stageData.IsWin
};
for (var index = 0; index < stageData.ArySectionDatas.Count; index++)
{
AddNewAiSectionScore(request, stageData, index, difficulty, aiScoreDatum);
}
// Maybe have a "SectionNo" variable for which section number it is on the DB await aiDatumService.InsertSongAiScore(aiScoreDatum);
// compare DB.SectionNo == i return;
// if any aspect of the section is higher than the previous best, update it
// Similar to Dan best play updates
} }
}*/
private static CrownType PlayResultToCrown(StageData stageData) for (var index = 0; index < stageData.ArySectionDatas.Count; index++)
{
var sectionData = stageData.ArySectionDatas[index];
if (index < existing.AiSectionScoreData.Count)
{
existing.AiSectionScoreData[index].UpdateBest(sectionData);
}
else
{
AddNewAiSectionScore(request,stageData,index,difficulty,existing);
}
}
await aiDatumService.UpdateSongAiScore(existing);
}
private static void AddNewAiSectionScore(PlayResultRequest request, StageData stageData, int index, Difficulty difficulty,
AiScoreDatum aiScoreDatum)
{ {
var crown = (CrownType)stageData.PlayResult; var sectionData = stageData.ArySectionDatas[index];
if (crown == CrownType.Gold && stageData.OkCnt == 0) var aiSectionScoreDatum = new AiSectionScoreDatum
{
Baid = request.BaidConf,
SongId = stageData.SongNo,
Difficulty = difficulty,
SectionIndex = index,
OkCount = sectionData.OkCnt,
MissCount = sectionData.NgCnt
};
aiSectionScoreDatum.UpdateBest(sectionData);
aiScoreDatum.AiSectionScoreData.Add(aiSectionScoreDatum);
}
private static CrownType PlayResultToCrown(uint playResult, uint okCount)
{
var crown = (CrownType)playResult;
if (crown == CrownType.Gold && okCount == 0)
{ {
crown = CrownType.Dondaful; crown = CrownType.Dondaful;
} }

View File

@ -1,5 +1,4 @@
using TaikoLocalServer.Services.Interfaces; using Throw;
using Throw;
namespace TaikoLocalServer.Controllers.Game; namespace TaikoLocalServer.Controllers.Game;

View File

@ -1,6 +1,7 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text.Json; using System.Text.Json;
using TaikoLocalServer.Services.Interfaces; using Microsoft.Extensions.Options;
using TaikoLocalServer.Settings;
using Throw; using Throw;
namespace TaikoLocalServer.Controllers.Game; namespace TaikoLocalServer.Controllers.Game;
@ -14,12 +15,16 @@ public class UserDataController : BaseController<UserDataController>
private readonly ISongPlayDatumService songPlayDatumService; private readonly ISongPlayDatumService songPlayDatumService;
private readonly IGameDataService gameDataService; private readonly IGameDataService gameDataService;
private readonly ServerSettings settings;
public UserDataController(IUserDatumService userDatumService, ISongPlayDatumService songPlayDatumService, IGameDataService gameDataService) public UserDataController(IUserDatumService userDatumService, ISongPlayDatumService songPlayDatumService,
IGameDataService gameDataService, IOptions<ServerSettings> settings)
{ {
this.userDatumService = userDatumService; this.userDatumService = userDatumService;
this.songPlayDatumService = songPlayDatumService; this.songPlayDatumService = songPlayDatumService;
this.gameDataService = gameDataService; this.gameDataService = gameDataService;
this.settings = settings.Value;
} }
[HttpPost] [HttpPost]
@ -28,11 +33,12 @@ public class UserDataController : BaseController<UserDataController>
{ {
Logger.LogInformation("UserData request : {Request}", request.Stringify()); Logger.LogInformation("UserData request : {Request}", request.Stringify());
var songIdMax = settings.EnableMoreSongs ? Constants.MUSIC_ID_MAX_EXPANDED : Constants.MUSIC_ID_MAX;
var releaseSongArray = var releaseSongArray =
FlagCalculator.GetBitArrayFromIds(gameDataService.GetMusicList(), Constants.MUSIC_ID_MAX, Logger); FlagCalculator.GetBitArrayFromIds(gameDataService.GetMusicList(), songIdMax, Logger);
var uraSongArray = var uraSongArray =
FlagCalculator.GetBitArrayFromIds(gameDataService.GetMusicWithUraList(), Constants.MUSIC_ID_MAX, Logger); FlagCalculator.GetBitArrayFromIds(gameDataService.GetMusicWithUraList(), songIdMax, Logger);
var userData = await userDatumService.GetFirstUserDatumOrDefault(request.Baid); var userData = await userDatumService.GetFirstUserDatumOrDefault(request.Baid);

View File

@ -0,0 +1,16 @@
namespace TaikoLocalServer.Entities;
public class AiScoreDatum
{
public uint Baid { get; set; }
public uint SongId { get; set; }
public Difficulty Difficulty { get; set; }
public bool IsWin { get; set; }
public List<AiSectionScoreDatum> AiSectionScoreData { get; set; } = new();
public virtual Card? Ba { get; set; }
}

View File

@ -0,0 +1,47 @@
using SharedProject.Utils;
namespace TaikoLocalServer.Entities;
public class AiSectionScoreDatum
{
public uint Baid { get; set; }
public uint SongId { get; set; }
public Difficulty Difficulty { get; set; }
public int SectionIndex { get; set; }
public CrownType Crown { get; set; }
public bool IsWin { get; set; }
public uint Score { get; set; }
public uint GoodCount { get; set; }
public uint OkCount { get; set; }
public uint MissCount { get; set; }
public uint DrumrollCount { get; set; }
public AiScoreDatum Parent { get; set; } = null!;
public void UpdateBest(PlayResultDataRequest.StageData.AiStageSectionData sectionData)
{
var crown = (CrownType)sectionData.Crown;
if (crown == CrownType.Gold && sectionData.OkCnt == 0)
{
crown = CrownType.Dondaful;
}
IsWin = sectionData.IsWin ? sectionData.IsWin : IsWin;
Crown = ValueHelpers.Max(crown, Crown);
Score = ValueHelpers.Max(sectionData.Score, Score);
GoodCount = ValueHelpers.Max(sectionData.GoodCnt, GoodCount);
OkCount = ValueHelpers.Min(sectionData.OkCnt, OkCount);
MissCount = ValueHelpers.Min(sectionData.NgCnt, MissCount);
DrumrollCount = ValueHelpers.Max(sectionData.PoundCnt, DrumrollCount);
}
}

View File

@ -26,6 +26,7 @@
public bool DisplayAchievement { get; set; } public bool DisplayAchievement { get; set; }
public Difficulty AchievementDisplayDifficulty { get; set; } public Difficulty AchievementDisplayDifficulty { get; set; }
public int AiWinCount { get; set; }
public virtual Card? Ba { get; set; } public virtual Card? Ba { get; set; }
} }
} }

View File

@ -9,4 +9,6 @@ global using TaikoLocalServer.Common;
global using TaikoLocalServer.Common.Utils; global using TaikoLocalServer.Common.Utils;
global using TaikoLocalServer.Context; global using TaikoLocalServer.Context;
global using TaikoLocalServer.Entities; global using TaikoLocalServer.Entities;
global using TaikoLocalServer.Models; global using TaikoLocalServer.Models;
global using TaikoLocalServer.Services;
global using TaikoLocalServer.Services.Interfaces;

View File

@ -0,0 +1,432 @@
// <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("20220917180457_AddAiBattle")]
partial class AddAiBattle
{
/// <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.AiScoreDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<bool>("IsWin")
.HasColumnType("INTEGER");
b.HasKey("Baid", "SongId", "Difficulty");
b.ToTable("AiScoreData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.AiSectionScoreDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<int>("SectionIndex")
.HasColumnType("INTEGER");
b.Property<int>("Crown")
.HasColumnType("INTEGER");
b.Property<uint>("DrumrollCount")
.HasColumnType("INTEGER");
b.Property<uint>("GoodCount")
.HasColumnType("INTEGER");
b.Property<bool>("IsWin")
.HasColumnType("INTEGER");
b.Property<uint>("MissCount")
.HasColumnType("INTEGER");
b.Property<uint>("OkCount")
.HasColumnType("INTEGER");
b.Property<uint>("Score")
.HasColumnType("INTEGER");
b.HasKey("Baid", "SongId", "Difficulty", "SectionIndex");
b.ToTable("AiSectionScoreData");
});
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.AiScoreDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
.WithMany()
.HasForeignKey("Baid")
.HasPrincipalKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.AiSectionScoreDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.AiScoreDatum", "Parent")
.WithMany("AiSectionScoreData")
.HasForeignKey("Baid", "SongId", "Difficulty")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Parent");
});
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.AiScoreDatum", b =>
{
b.Navigation("AiSectionScoreData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b =>
{
b.Navigation("DanStageScoreData");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,71 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TaikoLocalServer.Migrations
{
/// <inheritdoc />
public partial class AddAiBattle : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AiScoreData",
columns: table => new
{
Baid = table.Column<uint>(type: "INTEGER", nullable: false),
SongId = table.Column<uint>(type: "INTEGER", nullable: false),
Difficulty = table.Column<uint>(type: "INTEGER", nullable: false),
IsWin = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AiScoreData", x => new { x.Baid, x.SongId, x.Difficulty });
table.ForeignKey(
name: "FK_AiScoreData_Card_Baid",
column: x => x.Baid,
principalTable: "Card",
principalColumn: "Baid",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AiSectionScoreData",
columns: table => new
{
Baid = table.Column<uint>(type: "INTEGER", nullable: false),
SongId = table.Column<uint>(type: "INTEGER", nullable: false),
Difficulty = table.Column<uint>(type: "INTEGER", nullable: false),
SectionIndex = table.Column<int>(type: "INTEGER", nullable: false),
Crown = table.Column<int>(type: "INTEGER", nullable: false),
IsWin = table.Column<bool>(type: "INTEGER", nullable: false),
Score = table.Column<uint>(type: "INTEGER", nullable: false),
GoodCount = table.Column<uint>(type: "INTEGER", nullable: false),
OkCount = table.Column<uint>(type: "INTEGER", nullable: false),
MissCount = table.Column<uint>(type: "INTEGER", nullable: false),
DrumrollCount = table.Column<uint>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AiSectionScoreData", x => new { x.Baid, x.SongId, x.Difficulty, x.SectionIndex });
table.ForeignKey(
name: "FK_AiSectionScoreData_AiScoreData_Baid_SongId_Difficulty",
columns: x => new { x.Baid, x.SongId, x.Difficulty },
principalTable: "AiScoreData",
principalColumns: new[] { "Baid", "SongId", "Difficulty" },
onDelete: ReferentialAction.Cascade);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AiSectionScoreData");
migrationBuilder.DropTable(
name: "AiScoreData");
}
}
}

View File

@ -0,0 +1,435 @@
// <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("20220919022643_AddWinCountToUserdata")]
partial class AddWinCountToUserdata
{
/// <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.AiScoreDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<bool>("IsWin")
.HasColumnType("INTEGER");
b.HasKey("Baid", "SongId", "Difficulty");
b.ToTable("AiScoreData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.AiSectionScoreDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<int>("SectionIndex")
.HasColumnType("INTEGER");
b.Property<int>("Crown")
.HasColumnType("INTEGER");
b.Property<uint>("DrumrollCount")
.HasColumnType("INTEGER");
b.Property<uint>("GoodCount")
.HasColumnType("INTEGER");
b.Property<bool>("IsWin")
.HasColumnType("INTEGER");
b.Property<uint>("MissCount")
.HasColumnType("INTEGER");
b.Property<uint>("OkCount")
.HasColumnType("INTEGER");
b.Property<uint>("Score")
.HasColumnType("INTEGER");
b.HasKey("Baid", "SongId", "Difficulty", "SectionIndex");
b.ToTable("AiSectionScoreData");
});
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<int>("AiWinCount")
.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.AiScoreDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
.WithMany()
.HasForeignKey("Baid")
.HasPrincipalKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.AiSectionScoreDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.AiScoreDatum", "Parent")
.WithMany("AiSectionScoreData")
.HasForeignKey("Baid", "SongId", "Difficulty")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Parent");
});
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.AiScoreDatum", b =>
{
b.Navigation("AiSectionScoreData");
});
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 AddWinCountToUserdata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "AiWinCount",
table: "UserData",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AiWinCount",
table: "UserData");
}
}
}

View File

@ -17,6 +17,65 @@ namespace TaikoLocalServer.Migrations
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.0-preview.7.22376.2"); modelBuilder.HasAnnotation("ProductVersion", "7.0.0-preview.7.22376.2");
modelBuilder.Entity("TaikoLocalServer.Entities.AiScoreDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<bool>("IsWin")
.HasColumnType("INTEGER");
b.HasKey("Baid", "SongId", "Difficulty");
b.ToTable("AiScoreData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.AiSectionScoreDatum", b =>
{
b.Property<uint>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<int>("SectionIndex")
.HasColumnType("INTEGER");
b.Property<int>("Crown")
.HasColumnType("INTEGER");
b.Property<uint>("DrumrollCount")
.HasColumnType("INTEGER");
b.Property<uint>("GoodCount")
.HasColumnType("INTEGER");
b.Property<bool>("IsWin")
.HasColumnType("INTEGER");
b.Property<uint>("MissCount")
.HasColumnType("INTEGER");
b.Property<uint>("OkCount")
.HasColumnType("INTEGER");
b.Property<uint>("Score")
.HasColumnType("INTEGER");
b.HasKey("Baid", "SongId", "Difficulty", "SectionIndex");
b.ToTable("AiSectionScoreData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.Card", b => modelBuilder.Entity("TaikoLocalServer.Entities.Card", b =>
{ {
b.Property<string>("AccessCode") b.Property<string>("AccessCode")
@ -197,6 +256,9 @@ namespace TaikoLocalServer.Migrations
b.Property<uint>("AchievementDisplayDifficulty") b.Property<uint>("AchievementDisplayDifficulty")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("AiWinCount")
.HasColumnType("INTEGER");
b.Property<uint>("ColorBody") b.Property<uint>("ColorBody")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -273,6 +335,29 @@ namespace TaikoLocalServer.Migrations
b.ToTable("UserData"); b.ToTable("UserData");
}); });
modelBuilder.Entity("TaikoLocalServer.Entities.AiScoreDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
.WithMany()
.HasForeignKey("Baid")
.HasPrincipalKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.AiSectionScoreDatum", b =>
{
b.HasOne("TaikoLocalServer.Entities.AiScoreDatum", "Parent")
.WithMany("AiSectionScoreData")
.HasForeignKey("Baid", "SongId", "Difficulty")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Parent");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b => modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b =>
{ {
b.HasOne("TaikoLocalServer.Entities.Card", "Ba") b.HasOne("TaikoLocalServer.Entities.Card", "Ba")
@ -332,6 +417,11 @@ namespace TaikoLocalServer.Migrations
b.Navigation("Ba"); b.Navigation("Ba");
}); });
modelBuilder.Entity("TaikoLocalServer.Entities.AiScoreDatum", b =>
{
b.Navigation("AiSectionScoreData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b => modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b =>
{ {
b.Navigation("DanStageScoreData"); b.Navigation("DanStageScoreData");

View File

@ -1,87 +1,125 @@
using System.Reflection;
using System.Security.Authentication; using System.Security.Authentication;
using Microsoft.AspNetCore.HttpLogging;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using TaikoLocalServer.Middlewares; using TaikoLocalServer.Middlewares;
using TaikoLocalServer.Services;
using TaikoLocalServer.Services.Extentions; using TaikoLocalServer.Services.Extentions;
using TaikoLocalServer.Services.Interfaces;
using TaikoLocalServer.Settings; using TaikoLocalServer.Settings;
using Throw; using Throw;
using Serilog;
var builder = WebApplication.CreateBuilder(args); Log.Logger = new LoggerConfiguration()
// Manually enable tls 1.0 .WriteTo.Console()
builder.WebHost.UseKestrel(kestrelOptions => .CreateBootstrapLogger();
var version = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion;
Log.Information("TaikoLocalServer version {Version}", version);
Log.Information("Server starting up...");
try
{ {
kestrelOptions.ConfigureHttpsDefaults(httpsOptions => var builder = WebApplication.CreateBuilder(args);
// Manually enable tls 1.0
builder.WebHost.UseKestrel(kestrelOptions =>
{ {
httpsOptions.SslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13; kestrelOptions.ConfigureHttpsDefaults(options =>
options.SslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13);
}); });
});
// Add services to the container. builder.Host.UseSerilog((context, configuration) =>
builder.Services.AddOptions();
builder.Services.AddSingleton<IGameDataService, GameDataService>();
builder.Services.Configure<UrlSettings>(builder.Configuration.GetSection(nameof(UrlSettings)));
builder.Services.AddControllers().AddProtoBufNet();
builder.Services.AddDbContext<TaikoDbContext>(option =>
{
var dbName = builder.Configuration["DbFileName"];
if (string.IsNullOrEmpty(dbName))
{ {
dbName = Constants.DEFAULT_DB_NAME; configuration.WriteTo.Console().ReadFrom.Configuration(context.Configuration);
});
if (builder.Configuration.GetValue<bool>("ServerSettings:EnableMoreSongs"))
{
Log.Warning("Song limit expanded! Currently the game has issue loading crown/score rank and " +
"probably more server related data for songs with id > 1599. " +
"Also, the game can have random crashes because of that! Use at your own risk!");
} }
var path = Path.Combine(PathHelper.GetRootPath(), dbName);
option.UseSqlite($"Data Source={path}"); // Add services to the container.
}); builder.Services.AddOptions();
builder.Services.AddHttpLogging(options => builder.Services.AddSingleton<IGameDataService, GameDataService>();
{ builder.Services.Configure<ServerSettings>(builder.Configuration.GetSection(nameof(ServerSettings)));
options.LoggingFields = HttpLoggingFields.RequestProperties | builder.Services.AddControllers().AddProtoBufNet();
HttpLoggingFields.ResponseStatusCode; builder.Services.AddDbContext<TaikoDbContext>(option =>
});
builder.Services.AddMemoryCache();
builder.Services.AddCors(options =>
{
options.AddPolicy("DevCorsPolicy", policy =>
{ {
policy var dbName = builder.Configuration["DbFileName"];
.AllowAnyOrigin() if (string.IsNullOrEmpty(dbName))
.AllowAnyMethod() {
.AllowAnyHeader(); dbName = Constants.DEFAULT_DB_NAME;
}
var path = Path.Combine(PathHelper.GetRootPath(), dbName);
option.UseSqlite($"Data Source={path}");
}); });
}); builder.Services.AddMemoryCache();
builder.Services.AddTaikoDbServices(); builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAllCorsPolicy", policy =>
{
policy
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
builder.Services.AddTaikoDbServices();
var app = builder.Build(); var app = builder.Build();
// Migrate db // Migrate db
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
var db = scope.ServiceProvider.GetRequiredService<TaikoDbContext>(); var db = scope.ServiceProvider.GetRequiredService<TaikoDbContext>();
db.Database.Migrate(); db.Database.Migrate();
}
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms, " +
"request host: {RequestHost}";
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
};
});
var gameDataService = app.Services.GetService<IGameDataService>();
gameDataService.ThrowIfNull();
await gameDataService.InitializeAsync();
// For reverse proxy
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseCors("AllowAllCorsPolicy");
// For blazor hosting
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseHttpLogging();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/sys/servlet/PowerOn", StringComparison.InvariantCulture),
applicationBuilder => applicationBuilder.UseAllNetRequestMiddleware());
app.Run();
} }
catch (Exception ex)
var gameDataService = app.Services.GetService<IGameDataService>();
gameDataService.ThrowIfNull();
await gameDataService.InitializeAsync();
// For reverse proxy
app.UseForwardedHeaders(new ForwardedHeadersOptions
{ {
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto Log.Fatal(ex, "Unhandled exception");
}); }
finally
app.UseCors("DevCorsPolicy"); {
// For blazor hosting Log.Information("Shut down complete");
app.UseBlazorFrameworkFiles(); Log.CloseAndFlush();
app.UseStaticFiles(); }
app.UseRouting();
app.UseHttpLogging();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.UseWhen(context => context.Request.Path.StartsWithSegments("/sys/servlet/PowerOn", StringComparison.InvariantCulture),
applicationBuilder => applicationBuilder.UseAllNetRequestMiddleware());
app.Run();

View File

@ -0,0 +1,49 @@
using Throw;
namespace TaikoLocalServer.Services;
public class AiDatumService : IAiDatumService
{
private readonly TaikoDbContext context;
public AiDatumService(TaikoDbContext context)
{
this.context = context;
}
public async Task<List<AiScoreDatum>> GetAllAiScoreById(uint baid)
{
return await context.AiScoreData.Where(datum => datum.Baid == baid)
.Include(datum => datum.AiSectionScoreData)
.ToListAsync();
}
public async Task<AiScoreDatum?> GetSongAiScore(uint baid, uint songId, Difficulty difficulty)
{
return await context.AiScoreData.Where(datum => datum.Baid == baid &&
datum.SongId == songId &&
datum.Difficulty == difficulty)
.Include(datum => datum.AiSectionScoreData)
.FirstOrDefaultAsync();
}
public async Task UpdateSongAiScore(AiScoreDatum datum)
{
var existing = await context.AiScoreData.FindAsync(datum.Baid, datum.SongId, datum.Difficulty);
existing.ThrowIfNull("Cannot update a non-existing ai score!");
context.AiScoreData.Update(datum);
await context.SaveChangesAsync();
}
public async Task InsertSongAiScore(AiScoreDatum datum)
{
var existing = await context.AiScoreData.FindAsync(datum.Baid, datum.SongId, datum.Difficulty);
if (existing is not null)
{
throw new ArgumentException("Ai score already exists!", nameof(datum));
}
context.AiScoreData.Add(datum);
await context.SaveChangesAsync();
}
}

View File

@ -1,6 +1,5 @@
using SharedProject.Models; using SharedProject.Models;
using Swan.Mapping; using Swan.Mapping;
using TaikoLocalServer.Services.Interfaces;
namespace TaikoLocalServer.Services; namespace TaikoLocalServer.Services;

View File

@ -1,6 +1,4 @@
using TaikoLocalServer.Services.Interfaces; namespace TaikoLocalServer.Services;
namespace TaikoLocalServer.Services;
public class DanScoreDatumService : IDanScoreDatumService public class DanScoreDatumService : IDanScoreDatumService
{ {
@ -30,7 +28,7 @@ public class DanScoreDatumService : IDanScoreDatumService
var existing = await context.DanScoreData.FindAsync(datum.Baid, datum.DanId); var existing = await context.DanScoreData.FindAsync(datum.Baid, datum.DanId);
if (existing is null) if (existing is null)
{ {
await context.DanScoreData.AddAsync(datum); context.DanScoreData.Add(datum);
await context.SaveChangesAsync(); await context.SaveChangesAsync();
return; return;
} }

View File

@ -1,6 +1,4 @@
using TaikoLocalServer.Services.Interfaces; namespace TaikoLocalServer.Services.Extentions;
namespace TaikoLocalServer.Services.Extentions;
public static class ServiceExtensions public static class ServiceExtensions
{ {
@ -11,6 +9,7 @@ public static class ServiceExtensions
services.AddScoped<ISongPlayDatumService, SongPlayDatumService>(); services.AddScoped<ISongPlayDatumService, SongPlayDatumService>();
services.AddScoped<ISongBestDatumService, SongBestDatumService>(); services.AddScoped<ISongBestDatumService, SongBestDatumService>();
services.AddScoped<IDanScoreDatumService, DanScoreDatumService>(); services.AddScoped<IDanScoreDatumService, DanScoreDatumService>();
services.AddScoped<IAiDatumService, AiDatumService>();
return services; return services;
} }

View File

@ -1,8 +1,8 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Text.Json; using System.Text.Json;
using ICSharpCode.SharpZipLib.GZip;
using SharedProject.Models; using SharedProject.Models;
using Swan.Mapping; using Swan.Mapping;
using TaikoLocalServer.Services.Interfaces;
using Throw; using Throw;
namespace TaikoLocalServer.Services; namespace TaikoLocalServer.Services;
@ -54,6 +54,10 @@ public class GameDataService : IGameDataService
var danDataPath = Path.Combine(dataPath, Constants.DAN_DATA_FILE_NAME); var danDataPath = Path.Combine(dataPath, Constants.DAN_DATA_FILE_NAME);
var songIntroDataPath = Path.Combine(dataPath, Constants.INTRO_DATA_FILE_NAME); var songIntroDataPath = Path.Combine(dataPath, Constants.INTRO_DATA_FILE_NAME);
if (!File.Exists(musicAttributePath))
{
TryDecompressMusicAttribute();
}
await using var musicAttributeFile = File.OpenRead(musicAttributePath); await using var musicAttributeFile = File.OpenRead(musicAttributePath);
await using var danDataFile = File.OpenRead(danDataPath); await using var danDataFile = File.OpenRead(danDataPath);
await using var songIntroDataFile = File.OpenRead(songIntroDataPath); await using var songIntroDataFile = File.OpenRead(songIntroDataPath);
@ -69,6 +73,18 @@ public class GameDataService : IGameDataService
InitializeIntroData(introData); InitializeIntroData(introData);
} }
private static void TryDecompressMusicAttribute()
{
var dataPath = PathHelper.GetDataPath();
var musicAttributePath = Path.Combine(dataPath, Constants.MUSIC_ATTRIBUTE_FILE_NAME);
var compressedMusicAttributePath = Path.Combine(dataPath, Constants.MUSIC_ATTRIBUTE_COMPRESSED_FILE_NAME);
using var compressed = File.Open(compressedMusicAttributePath, FileMode.Open);
using var output = File.Create(musicAttributePath);
GZip.Decompress(compressed, output, true);
}
private void InitializeIntroData(List<SongIntroductionData>? introData) private void InitializeIntroData(List<SongIntroductionData>? introData)
{ {
introData.ThrowIfNull("Shouldn't happen!"); introData.ThrowIfNull("Shouldn't happen!");

View File

@ -0,0 +1,12 @@
namespace TaikoLocalServer.Services.Interfaces;
public interface IAiDatumService
{
public Task<List<AiScoreDatum>> GetAllAiScoreById(uint baid);
public Task<AiScoreDatum?> GetSongAiScore(uint baid, uint songId, Difficulty difficulty);
public Task UpdateSongAiScore(AiScoreDatum datum);
public Task InsertSongAiScore(AiScoreDatum datum);
}

View File

@ -1,6 +1,5 @@
using SharedProject.Models; using SharedProject.Models;
using Swan.Mapping; using Swan.Mapping;
using TaikoLocalServer.Services.Interfaces;
using Throw; using Throw;
namespace TaikoLocalServer.Services; namespace TaikoLocalServer.Services;
@ -32,7 +31,7 @@ public class SongBestDatumService : ISongBestDatumService
return; return;
} }
await context.SongBestData.AddAsync(datum); context.SongBestData.Add(datum);
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
@ -43,6 +42,10 @@ public class SongBestDatumService : ISongBestDatumService
var result = songbestDbData.Select(datum => datum.CopyPropertiesToNew<SongBestData>()).ToList(); var result = songbestDbData.Select(datum => datum.CopyPropertiesToNew<SongBestData>()).ToList();
var aiSectionBest = await context.AiScoreData.Where(datum => datum.Baid == baid)
.Include(datum => datum.AiSectionScoreData)
.ToListAsync();
var playLogs = await context.SongPlayData.Where(datum => datum.Baid == baid).ToListAsync(); var playLogs = await context.SongPlayData.Where(datum => datum.Baid == baid).ToListAsync();
foreach (var bestData in result) foreach (var bestData in result)
{ {
@ -65,6 +68,16 @@ public class SongBestDatumService : ISongBestDatumService
nameof(SongPlayDatum.DrumrollCount), nameof(SongPlayDatum.DrumrollCount),
nameof(SongPlayDatum.ComboCount) nameof(SongPlayDatum.ComboCount)
); );
var aiSection = aiSectionBest.FirstOrDefault(datum => datum.Difficulty == bestData.Difficulty &&
datum.SongId == bestData.SongId);
if (aiSection is null)
{
continue;
}
bestData.AiSectionBestData = aiSection.AiSectionScoreData
.Select(datum => datum.CopyPropertiesToNew<AiSectionBestData>()).ToList();
} }
return result; return result;

View File

@ -1,6 +1,4 @@
using TaikoLocalServer.Services.Interfaces; namespace TaikoLocalServer.Services;
namespace TaikoLocalServer.Services;
class SongPlayDatumService : ISongPlayDatumService class SongPlayDatumService : ISongPlayDatumService
{ {
@ -18,7 +16,7 @@ class SongPlayDatumService : ISongPlayDatumService
public async Task AddSongPlayDatum(SongPlayDatum datum) public async Task AddSongPlayDatum(SongPlayDatum datum)
{ {
await context.SongPlayData.AddAsync(datum); context.SongPlayData.Add(datum);
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
} }

View File

@ -1,5 +1,4 @@
using System.Text.Json; using System.Text.Json;
using TaikoLocalServer.Services.Interfaces;
using Throw; using Throw;
namespace TaikoLocalServer.Services; namespace TaikoLocalServer.Services;

View File

@ -1,8 +1,10 @@
namespace TaikoLocalServer.Settings; namespace TaikoLocalServer.Settings;
public class UrlSettings public class ServerSettings
{ {
public string MuchaUrl { get; set; } = string.Empty; public string MuchaUrl { get; set; } = string.Empty;
public string GameUrl { get; set; } = string.Empty; public string GameUrl { get; set; } = string.Empty;
public bool EnableMoreSongs { get; set; }
} }

View File

@ -4,6 +4,7 @@
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Version>0.3.0-alpha</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -11,16 +12,18 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0-preview.7.22376.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0-rc.1.22426.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-preview.7.22376.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.1.22426.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0-preview.7.22376.2"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0-rc.1.22426.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="protobuf-net" Version="3.1.17" /> <PackageReference Include="protobuf-net" Version="3.1.17" />
<PackageReference Include="protobuf-net.AspNetCore" Version="3.1.17" /> <PackageReference Include="protobuf-net.AspNetCore" Version="3.1.17" />
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="Serilog.AspNetCore" Version="6.1.0-dev-00281" />
<PackageReference Include="Serilog.Expressions" Version="3.4.1-dev-00095" />
<PackageReference Include="SharpZipLib" Version="1.4.0" />
<PackageReference Include="Swan.Core" Version="6.0.2-beta.90" /> <PackageReference Include="Swan.Core" Version="6.0.2-beta.90" />
<PackageReference Include="Swan.Logging" Version="6.0.2-beta.69" /> <PackageReference Include="Swan.Logging" Version="6.0.2-beta.69" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
@ -32,18 +35,24 @@
<None Update="Certificates\cert.pfx"> <None Update="Certificates\cert.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="Certificates\root.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Update="wwwroot\data\music_attribute.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\dan_data.json"> <Content Update="wwwroot\data\dan_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Update="wwwroot\data\intro_data.json"> <Content Update="wwwroot\data\intro_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Update="wwwroot\data\music_attribute.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\music_attribute.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Information"
}
}
}

View File

@ -1,21 +1,56 @@
{ {
"UrlSettings": { "ServerSettings": {
"MuchaUrl": "https://v402-front.mucha-prd.nbgi-amnet.jp:10122", "MuchaUrl": "https://v402-front.mucha-prd.nbgi-amnet.jp:10122",
"GameUrl": "vsapi.taiko-p.jp" "GameUrl": "vsapi.taiko-p.jp",
"EnableMoreSongs": false
}, },
"DbFileName" : "taiko.db3", "DbFileName" : "taiko.db3",
"Logging": {
"LogLevel": { "Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning", "Override": {
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information" "Microsoft": "Warning",
} "Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Filter": [
{
"Name": "ByExcluding",
"Args": {
"expression": "@mt = 'An unhandled exception has occurred while executing the request.'"
}
}
],
"WriteTo": [
{
"Name": "File",
"Args": { "path": "./Logs/log-.txt", "rollingInterval": "Day" }
}
]
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"Kestrel": { "Kestrel": {
"Endpoints": { "Endpoints": {
"Server": { "BaseServer": {
"Url": "http://0.0.0.0:5000" "Url": "http://0.0.0.0:5000"
},
"AmAuthServer": {
"Url": "http://0.0.0.0:80"
},
"MuchaServer": {
"Url": "https://0.0.0.0:10122"
},
"GameServer1": {
"Url": "https://0.0.0.0:54430"
},
"GameServer2": {
"Url": "https://0.0.0.0:54431"
} }
}, },
"Certificates": { "Certificates": {

View File

@ -68,9 +68,8 @@
FullWidth="true" FullWidth="true"
AnchorOrigin="Origin.BottomCenter" AnchorOrigin="Origin.BottomCenter"
TransformOrigin="Origin.TopCenter"> TransformOrigin="Origin.TopCenter">
<MudMenuItem Href="@($"Cards/{user.Baid}/TaikoMode")">Taiko Mode</MudMenuItem> <MudMenuItem Href="@($"Cards/{user.Baid}/HighScores")">High Scores</MudMenuItem>
<MudMenuItem Href="@($"Cards/{user.Baid}/DaniDojo")">Dani Dojo</MudMenuItem> <MudMenuItem Href="@($"Cards/{user.Baid}/DaniDojo")">Dani Dojo</MudMenuItem>
@*<MudMenuItem Href="@($"Cards/{user.Baid}/AIBattle")">AI Battle</MudMenuItem>*@
</MudMenu> </MudMenu>
</MudStack> </MudStack>
</MudCardActions> </MudCardActions>

View File

@ -1,11 +1,11 @@
@inject IGameDataService GameDataService @inject IGameDataService GameDataService
@inject HttpClient Client @inject HttpClient Client
@page "/Cards/{baid:int}/TaikoMode" @page "/Cards/{baid:int}/HighScores"
<MudBreadcrumbs Items="breadcrumbs" Class="px-0"></MudBreadcrumbs> <MudBreadcrumbs Items="breadcrumbs" Class="px-0"></MudBreadcrumbs>
<h1>Taiko Mode</h1> <h1>High Scores</h1>
<MudText Typo="Typo.caption">Card: @Baid</MudText> <MudText Typo="Typo.caption">Card: @Baid</MudText>
<MudGrid Class="my-8"> <MudGrid Class="my-8">
@ -29,7 +29,7 @@
Icon="@GetDifficultyIcon(difficulty)"> Icon="@GetDifficultyIcon(difficulty)">
@if (songBestDataMap.ContainsKey(difficulty)) @if (songBestDataMap.ContainsKey(difficulty))
{ {
<MudDataGrid Items="@songBestDataMap[difficulty]" <MudDataGrid Items="@songBestDataMap[difficulty]" RowClick="(DataGridRowClickEventArgs<SongBestData> _) => { return;}"
ColumnResizeMode="ResizeMode.None" RowsPerPage="25" Elevation="0"> ColumnResizeMode="ResizeMode.None" RowsPerPage="25" Elevation="0">
<Columns> <Columns>
<Column T="SongBestData" Field="@nameof(SongBestData.SongId)" Title="Song" StickyLeft="true"> <Column T="SongBestData" Field="@nameof(SongBestData.SongId)" Title="Song" StickyLeft="true">
@ -68,7 +68,7 @@
</MudChip> </MudChip>
</CellTemplate> </CellTemplate>
</Column> </Column>
<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>
<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="@(GetCrownText(context.Item.BestCrown))" title="@(GetCrownText(context.Item.BestCrown))" style="@IconStyle" />
@ -82,13 +82,73 @@
} }
</CellTemplate> </CellTemplate>
</Column> </Column>
<Column T="SongBestData" Field="@nameof(SongBestData.GoodCount)" Title="Good" Sortable="false"/> <Column T="SongBestData" Field="@nameof(SongBestData.GoodCount)" Title="Good" Sortable="false" />
<Column T="SongBestData" Field="@nameof(SongBestData.OkCount)" Title="OK" Sortable="false"/> <Column T="SongBestData" Field="@nameof(SongBestData.OkCount)" Title="OK" Sortable="false" />
<Column T="SongBestData" Field="@nameof(SongBestData.MissCount)" Title="Bad" Sortable="false"/> <Column T="SongBestData" Field="@nameof(SongBestData.MissCount)" Title="Bad" Sortable="false" />
<Column T="SongBestData" Field="@nameof(SongBestData.DrumrollCount)" Title="Drumroll" Sortable="false"/> <Column T="SongBestData" Field="@nameof(SongBestData.DrumrollCount)" Title="Drumroll" Sortable="false"/>
<Column T="SongBestData" Field="@nameof(SongBestData.ComboCount)" Title="MAX Combo" Sortable="false"/> <Column T="SongBestData" Field="@nameof(SongBestData.ComboCount)" Title="MAX Combo" Sortable="false" />
<Column T="SongBestData" Field="@nameof(SongBestData.LastPlayTime)" Title="Last Played"/> <Column T="SongBestData" Field="@nameof(SongBestData.ShowAiData)" Title="AI Battle Data">
<CellTemplate>
<MudButton Variant="Variant.Outlined" Size="Size.Small"
OnClick="@(() => ToggleShowAiData(context.Item))"
Disabled="@(!IsAiDataPresent(context.Item))">
@(context.Item.ShowAiData ? "Hide" : "Show")
</MudButton>
</CellTemplate>
</Column>
<Column T="SongBestData" Field="@nameof(SongBestData.LastPlayTime)" Title="Last Played" Hideable="true" />
<Column T="SongBestData" Field="@nameof(SongBestData.PlayCount)" Title="Total Plays" Hideable="true" />
<Column T="SongBestData" Field="@nameof(SongBestData.ClearCount)" Title="Total Clears" Hideable="true" />
<Column T="SongBestData" Field="@nameof(SongBestData.FullComboCount)" Title="Total Full Combos" Hideable="true" />
<Column T="SongBestData" Field="@nameof(SongBestData.PerfectCount)" Title="Total Donderful Combos" Hideable="true" />
</Columns> </Columns>
<ChildRowContent>
@if (context.Item.ShowAiData)
{
<tr>
<td colspan="1" class="pa-3 ai-battle-td">
<MudText Typo="Typo.body2" Style="font-weight: bold">
AI Battle Data
</MudText>
</td>
<td colspan="16">
<MudTable Elevation="0" ReadOnly="true"
Items="@context.Item.AiSectionBestData" Context="aiSectionContext">
<HeaderContent>
<MudTh>Section No.</MudTh>
<MudTh>Result</MudTh>
<MudTh>Score</MudTh>
<MudTh>Crown</MudTh>
<MudTh>Good</MudTh>
<MudTh>OK</MudTh>
<MudTh>Bad</MudTh>
<MudTh>Drumroll</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@(aiSectionContext.SectionIndex + 1)</MudTd>
<MudTd>
@if (@aiSectionContext.IsWin) {
<img src="@($"/images/ai_Win.png")" alt="Win" title="Win" style="@IconStyle" />
}
else
{
<img src="@($"/images/ai_Lose.png")" alt="Lose" title="Lose" style="@IconStyle" />
}
</MudTd>
<MudTd>@aiSectionContext.Score</MudTd>
<MudTd>
<img src="@($"/images/ai_crown_{aiSectionContext.Crown}.png")" alt="@(GetCrownText(aiSectionContext.Crown))" title="@(GetCrownText(aiSectionContext.Crown))" style="@IconStyle" />
</MudTd>
<MudTd>@aiSectionContext.GoodCount</MudTd>
<MudTd>@aiSectionContext.OkCount</MudTd>
<MudTd>@aiSectionContext.MissCount</MudTd>
<MudTd>@aiSectionContext.DrumrollCount</MudTd>
</RowTemplate>
</MudTable>
</td>
</tr>
}
</ChildRowContent>
<PagerContent> <PagerContent>
<MudDataGridPager T="SongBestData"/> <MudDataGridPager T="SongBestData"/>
</PagerContent> </PagerContent>

View File

@ -1,12 +1,15 @@
namespace TaikoWebUI.Pages; using static MudBlazor.Colors;
using System;
public partial class TaikoMode namespace TaikoWebUI.Pages;
public partial class HighScores
{ {
[Parameter] [Parameter]
public int Baid { get; set; } public int Baid { get; set; }
private const string IconStyle = "width:25px; height:25px;"; private const string IconStyle = "width:25px; height:25px;";
private SongBestResponse? response; private SongBestResponse? response;
private Dictionary<Difficulty, List<SongBestData>> songBestDataMap = new(); private Dictionary<Difficulty, List<SongBestData>> songBestDataMap = new();
@ -41,7 +44,7 @@ public partial class TaikoMode
breadcrumbs.Add(new BreadcrumbItem($"Card: {Baid}", href: null, disabled: true)); breadcrumbs.Add(new BreadcrumbItem($"Card: {Baid}", href: null, disabled: true));
breadcrumbs.Add(new BreadcrumbItem("Taiko Mode", href: $"/Cards/{Baid}/TaikoMode", disabled: false)); breadcrumbs.Add(new BreadcrumbItem("High Scores", href: $"/Cards/{Baid}/HighScores", disabled: false));
} }
private async Task OnFavoriteToggled(SongBestData data) private async Task OnFavoriteToggled(SongBestData data)
@ -133,7 +136,19 @@ public partial class TaikoMode
SongGenre.Variety => "background: #1dc83b; color: #fff", SongGenre.Variety => "background: #1dc83b; color: #fff",
SongGenre.Classical => "background: #bfa356", SongGenre.Classical => "background: #bfa356",
_ => "" _ => ""
}; };
}
private static void ToggleShowAiData(SongBestData data)
{
data.ShowAiData = !data.ShowAiData;
}
private static bool IsAiDataPresent(SongBestData data)
{
var aiData = data.AiSectionBestData;
return aiData.Count > 0;
} }
} }

View File

@ -7,7 +7,7 @@ builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddSingleton(sp => new HttpClient builder.Services.AddSingleton(sp => new HttpClient
{ {
BaseAddress = new Uri(builder.Configuration.GetValue<string>("BaseUrl")) BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
}); });
builder.Services.AddMudServices(); builder.Services.AddMudServices();
builder.Services.AddSingleton<IGameDataService, GameDataService>(); builder.Services.AddSingleton<IGameDataService, GameDataService>();
@ -15,10 +15,6 @@ builder.Services.AddSingleton<IGameDataService, GameDataService>();
var host = builder.Build(); var host = builder.Build();
var gameDataService = host.Services.GetRequiredService<IGameDataService>(); var gameDataService = host.Services.GetRequiredService<IGameDataService>();
#if DEBUG await gameDataService.InitializeAsync(builder.HostEnvironment.BaseAddress);
await gameDataService.InitializeAsync(builder.Configuration.GetValue<string>("DataBaseUrl"));
#else
await gameDataService.InitializeAsync(builder.Configuration.GetValue<string>("BaseUrl"));
#endif
await host.RunAsync(); await host.RunAsync();

View File

@ -1,4 +1,7 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Net;
using System.Text.Json;
using ICSharpCode.SharpZipLib.GZip;
using Swan.Mapping; using Swan.Mapping;
using TaikoWebUI.Shared.Models; using TaikoWebUI.Shared.Models;
@ -27,16 +30,13 @@ public class GameDataService : IGameDataService
public async Task InitializeAsync(string dataBaseUrl) public async Task InitializeAsync(string dataBaseUrl)
{ {
var musicInfo = await client.GetFromJsonAsync<MusicInfo>($"{dataBaseUrl}/data/musicinfo.json"); dataBaseUrl = dataBaseUrl.TrimEnd('/');
var wordList = await client.GetFromJsonAsync<WordList>($"{dataBaseUrl}/data/wordlist.json"); var musicInfo = await GetData<MusicInfo>(dataBaseUrl, Constants.MUSIC_INFO_BASE_NAME);
var musicOrder = await client.GetFromJsonAsync<MusicOrder>($"{dataBaseUrl}/data/music_order.json"); var wordList = await GetData<WordList>(dataBaseUrl, Constants.WORD_LIST_BASE_NAME);
var musicOrder = await GetData<MusicOrder>(dataBaseUrl, Constants.MUSIC_ORDER_BASE_NAME);
var danData = await client.GetFromJsonAsync<List<DanData>>($"{dataBaseUrl}/data/dan_data.json"); var danData = await client.GetFromJsonAsync<List<DanData>>($"{dataBaseUrl}/data/dan_data.json");
musicInfo.ThrowIfNull();
wordList.ThrowIfNull();
musicOrder.ThrowIfNull();
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
@ -52,6 +52,36 @@ public class GameDataService : IGameDataService
await Task.Run(() => InitializeTitles(dict)); await Task.Run(() => InitializeTitles(dict));
} }
private async Task<T> GetData<T>(string dataBaseUrl, string fileBaseName) where T : notnull
{
T? data;
try
{
data = await client.GetFromJsonAsync<T>($"{dataBaseUrl}/data/{fileBaseName}.json");
data.ThrowIfNull();
return data;
}
catch (HttpRequestException e)
{
if (e.StatusCode != HttpStatusCode.NotFound)
{
throw;
}
await using var compressed = await client.GetStreamAsync($"{dataBaseUrl}/data/{fileBaseName}.bin");
await using var gZipInputStream = new GZipInputStream(compressed);
using var decompressed = new MemoryStream();
// Decompress
await gZipInputStream.CopyToAsync(decompressed);
// Reset stream for reading
decompressed.Position = 0;
data = await JsonSerializer.DeserializeAsync<T>(decompressed);
data.ThrowIfNull();
return data;
}
}
public string GetMusicNameBySongId(uint songId) public string GetMusicNameBySongId(uint songId)
{ {
return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.SongName : string.Empty; return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.SongName : string.Empty;

View File

@ -9,4 +9,8 @@ public static class Constants
public const int COSTUME_PUCHI_MAX = 129; public const int COSTUME_PUCHI_MAX = 129;
public const int COSTUME_COLOR_MAX = 63; public const int COSTUME_COLOR_MAX = 63;
public const int PLAYER_TITLE_MAX = 750; public const int PLAYER_TITLE_MAX = 750;
public const string MUSIC_INFO_BASE_NAME = "musicinfo";
public const string WORD_LIST_BASE_NAME = "wordlist";
public const string MUSIC_ORDER_BASE_NAME = "music_order";
} }

View File

@ -8,9 +8,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Autocomplete.Clients" Version="1.1.0" /> <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.9" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.7" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.9" PrivateAssets="all" />
<PackageReference Include="MudBlazor" Version="6.0.15" /> <PackageReference Include="MudBlazor" Version="6.0.15" />
<PackageReference Include="SharpZipLib" Version="1.4.0" />
<PackageReference Include="Swan.Core" Version="6.0.2-beta.90" /> <PackageReference Include="Swan.Core" Version="6.0.2-beta.90" />
</ItemGroup> </ItemGroup>
@ -19,7 +20,19 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Update="wwwroot\appsettings.json"> <Content Update="wwwroot\data\musicinfo.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\music_order.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\wordlist.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\music_attribute.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\music_order.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Update="wwwroot\data\musicinfo.json"> <Content Update="wwwroot\data\musicinfo.json">
@ -28,9 +41,6 @@
<Content Update="wwwroot\data\wordlist.json"> <Content Update="wwwroot\data\wordlist.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Update="wwwroot\data\music_order.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,3 +0,0 @@
{
"DataBaseUrl": "http://localhost:5000"
}

View File

@ -1,3 +0,0 @@
{
"BaseUrl": "http://localhost:5000"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -29,4 +29,23 @@
border: 1px solid black; border: 1px solid black;
position: relative; position: relative;
top: 2px; top: 2px;
}
.columns-panel {
column-count: 2;
}
.ai-battle-td {
position: sticky;
left: 0;
top: 0;
z-index: 99;
background: #FAFAFA;
display: none;
}
@media only screen and (min-width: 600px) {
.ai-battle-td {
display: revert;
}
} }