1
0
mirror of synced 2024-09-24 03:18:27 +02: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

1
.gitignore vendored
View File

@ -396,3 +396,4 @@ FodyWeavers.xsd
# JetBrains Rider
*.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)

116
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.
### 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
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.
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:
1. Extract the server release anywhere
```
music_attribute.bin
musicinfo.bin
music_order.bin
wordlist.bin
2. From the game files, copy `music_attribute.bin`, `music_order.bin`, `musicinfo.bin` and `wordlist.bin` to `wwwroot/data` folder.
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.
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.
6. Modify comfig.toml from TAL, edit the following line:
```toml
server = "https://divamodarchive.com" # Change https://divamodarchive.com to your/server's ip, like 192.168.1.100
```
Extract them (you can use [7zip](https://www.7-zip.org)) and rename the extracted file like so:
7. Now the game should be able to connect.
```
music_attribute -> music_attribute.json
musicinfo -> musicinfo.json
music_order -> music_order.json
wordlist -> wordlist.json
```
### About certificates
Then put these in TaikoLocalServer's `wwwroot/data` folder.
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:
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"
```
DNS Name=nbgi-amnet.jp
DNS Name=v402-front.mucha-prd.nbgi-amnet.jp
DNS Name=*.mucha-prd.nbgi-amnet.jp
DNS Name=localhost
DNS Name=vsapi.taiko-p.jp
```
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)
You will need to modify hosts file to use them.

View File

@ -5,5 +5,5 @@ public enum PlayMode
Normal = 0,
DanMode = 1,
// 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 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 BestRate { get; set; }
@ -37,4 +42,8 @@ public class SongBestData
public uint HitCount { 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 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 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 int MUSIC_ID_MAX_EXPANDED = 9000;
public const string DEFAULT_DB_NAME = "taiko.db3";
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";

View File

@ -116,7 +116,7 @@ public static class FlagCalculator
{
if (id >= bitArraySize)
{
logger.LogWarning("Id out of range!");
logger.LogWarning("Id {Id} out of range!", id);
continue;
}
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<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)
{
@ -30,5 +32,28 @@ public partial class TaikoDbContext
.HasForeignKey(d => new {d.Baid, d.DanId})
.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")]
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;
}

View File

@ -5,9 +5,9 @@ namespace TaikoLocalServer.Controllers.AmUpdater;
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;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
using SharedProject.Models.Responses;
using TaikoLocalServer.Services.Interfaces;
using System.Collections.Immutable;
using SharedProject.Models.Responses;
namespace TaikoLocalServer.Controllers.Api;
@ -11,10 +11,14 @@ public class PlayDataController : BaseController<PlayDataController>
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.songBestDatumService = songBestDatumService;
this.songPlayDatumService = songPlayDatumService;
}
[HttpGet("{baid}")]
@ -27,6 +31,16 @@ public class PlayDataController : BaseController<PlayDataController>
}
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 favoriteSet = favoriteSongs.ToHashSet();
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.Responses;
using SharedProject.Utils;
using TaikoLocalServer.Services;
using TaikoLocalServer.Services.Interfaces;
using Throw;
namespace TaikoLocalServer.Controllers.Api;

View File

@ -1,5 +1,4 @@
using System.Text.Json;
using TaikoLocalServer.Services.Interfaces;
using Throw;
namespace TaikoLocalServer.Controllers.Game;
@ -16,13 +15,16 @@ public class BaidController : BaseController<BaidController>
private readonly IDanScoreDatumService danScoreDatumService;
private readonly IAiDatumService aiDatumService;
public BaidController(IUserDatumService userDatumService, ICardService cardService,
ISongBestDatumService songBestDatumService, IDanScoreDatumService danScoreDatumService)
ISongBestDatumService songBestDatumService, IDanScoreDatumService danScoreDatumService, IAiDatumService aiDatumService)
{
this.userDatumService = userDatumService;
this.cardService = cardService;
this.songBestDatumService = songBestDatumService;
this.danScoreDatumService = danScoreDatumService;
this.aiDatumService = aiDatumService;
}
@ -112,6 +114,11 @@ public class BaidController : BaseController<BaidController>
var genericInfoFlgLength = genericInfoFlg.Any()? genericInfoFlg.Max() + 1 : 0;
var genericInfoFlgArray = FlagCalculator.GetBitArrayFromIds(genericInfoFlg, (int)genericInfoFlgLength, Logger);
var aiRank = (uint)(userData.AiWinCount / 10);
if (aiRank > 11)
{
aiRank = 11;
}
response = new BAIDResponse
{
Result = 1,
@ -158,8 +165,8 @@ public class BaidController : BaseController<BaidController>
IsDispAchievementTypeSet = true,
LastPlayMode = userData.LastPlayMode,
IsDispSouuchiOn = true,
AiRank = 0,
AiTotalWin = 0,
AiRank = aiRank,
AiTotalWin = (uint)userData.AiWinCount,
Accesstoken = "123456",
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;
@ -8,9 +10,12 @@ public class CrownsDataController : BaseController<CrownsDataController>
{
private readonly ISongBestDatumService songBestDatumService;
public CrownsDataController(ISongBestDatumService songBestDatumService)
private readonly ServerSettings settings;
public CrownsDataController(ISongBestDatumService songBestDatumService, IOptions<ServerSettings> settings)
{
this.songBestDatumService = songBestDatumService;
this.settings = settings.Value;
}
[HttpPost]
@ -21,10 +26,11 @@ public class CrownsDataController : BaseController<CrownsDataController>
var songBestData = await songBestDatumService.GetAllSongBestData(request.Baid);
var crown = new ushort[Constants.CROWN_FLAG_ARRAY_SIZE];
var dondafulCrown = new byte[Constants.DONDAFUL_CROWN_FLAG_ARRAY_SIZE];
var songIdMax = settings.EnableMoreSongs ? Constants.MUSIC_ID_MAX_EXPANDED : Constants.MUSIC_ID_MAX;
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;
dondafulCrown[songId] = songBestData

View File

@ -1,21 +1,32 @@
namespace TaikoLocalServer.Controllers.Game;
using Throw;
namespace TaikoLocalServer.Controllers.Game;
[Route("/v12r03/chassis/getaidata.php")]
[ApiController]
public class GetAiDataController : BaseController<GetAiDataController>
{
private readonly IUserDatumService userDatumService;
public GetAiDataController(IUserDatumService userDatumService)
{
this.userDatumService = userDatumService;
}
[HttpPost]
[Produces("application/protobuf")]
public IActionResult GetAiData([FromBody] GetAiDataRequest request)
public async Task<IActionResult> GetAiData([FromBody] GetAiDataRequest request)
{
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
{
Result = 1,
TotalWinnings = 1,
InputMedian = "1",
InputVariance = "0.576389"
TotalWinnings = (uint)user.AiWinCount,
InputMedian = "1000",
InputVariance = "2000"
};
return Ok(response);

View File

@ -1,12 +1,21 @@
namespace TaikoLocalServer.Controllers.Game;
using Throw;
namespace TaikoLocalServer.Controllers.Game;
[Route("/v12r03/chassis/getaiscore.php")]
[ApiController]
public class GetAiScoreController : BaseController<GetAiScoreController>
{
private readonly IAiDatumService aiDatumService;
public GetAiScoreController(IAiDatumService aiDatumService)
{
this.aiDatumService = aiDatumService;
}
[HttpPost]
[Produces("application/protobuf")]
public IActionResult GetAiScore([FromBody] GetAiScoreRequest request)
public async Task<IActionResult> GetAiScore([FromBody] GetAiScoreRequest request)
{
Logger.LogInformation("GetAiScore request : {Request}", request.Stringify());
@ -15,9 +24,33 @@ public class GetAiScoreController : BaseController<GetAiScoreController>
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
// 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,
Crown = (uint)CrownType.Clear,
@ -66,7 +99,7 @@ public class GetAiScoreController : BaseController<GetAiScoreController>
OkCnt = 50,
NgCnt = 25,
PoundCnt = 12
});
});*/
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")]
[ApiController]

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
using System.Buffers.Binary;
using System.Globalization;
using System.Text.Json;
using TaikoLocalServer.Services.Interfaces;
using Throw;
namespace TaikoLocalServer.Controllers.Game;
@ -20,13 +19,16 @@ public class PlayResultController : BaseController<PlayResultController>
private readonly IDanScoreDatumService danScoreDatumService;
private readonly IAiDatumService aiDatumService;
public PlayResultController(IUserDatumService userDatumService, ISongPlayDatumService songPlayDatumService,
ISongBestDatumService songBestDatumService, IDanScoreDatumService danScoreDatumService)
ISongBestDatumService songBestDatumService, IDanScoreDatumService danScoreDatumService, IAiDatumService aiDatumService)
{
this.userDatumService = userDatumService;
this.songPlayDatumService = songPlayDatumService;
this.songBestDatumService = songBestDatumService;
this.danScoreDatumService = danScoreDatumService;
this.aiDatumService = aiDatumService;
}
[HttpPost]
@ -77,9 +79,7 @@ public class PlayResultController : BaseController<PlayResultController>
if (playMode == PlayMode.AiBattle)
{
// 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 UpdateAiBattleData(request, stageData);
}
await UpdateBestData(request, stageData, bestData);
@ -163,7 +163,7 @@ public class PlayResultController : BaseController<PlayResultController>
ComboCount = stageData.ComboCnt,
HitCount = stageData.HitCnt,
DrumrollCount = stageData.PoundCnt,
Crown = PlayResultToCrown(stageData),
Crown = PlayResultToCrown(stageData.PlayResult, stageData.OkCnt),
Score = stageData.PlayScore,
ScoreRate = stageData.ScoreRate,
ScoreRank = (ScoreRank)stageData.ScoreRank,
@ -229,6 +229,7 @@ public class PlayResultController : BaseController<PlayResultController>
userdata.GenericInfoFlgArray =
UpdateJsonUintFlagArray(userdata.GenericInfoFlgArray, playResultData.GetGenericInfoNoes, nameof(userdata.GenericInfoFlgArray));
userdata.AiWinCount += playResultData.AryStageInfoes.Count(data => data.IsWin);
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
var crown = PlayResultToCrown(stageData);
var crown = PlayResultToCrown(stageData.PlayResult, stageData.OkCnt);
bestDatum.UpdateBestData(crown, stageData.ScoreRank, stageData.PlayScore, stageData.ScoreRate);
@ -308,23 +309,69 @@ public class PlayResultController : BaseController<PlayResultController>
}
// 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++)
{
// Only update crown if it's a higher crown than the previous best crown
var difficulty = (Difficulty)stageData.Level;
difficulty.Throw().IfOutOfRange();
var existing = await aiDatumService.GetSongAiScore(request.BaidConf,
stageData.SongNo, difficulty);
// Maybe have a "SectionNo" variable for which section number it is on the DB
// compare DB.SectionNo == i
// if any aspect of the section is higher than the previous best, update it
// Similar to Dan best play updates
if (existing is null)
{
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);
}
}*/
private static CrownType PlayResultToCrown(StageData stageData)
await aiDatumService.InsertSongAiScore(aiScoreDatum);
return;
}
for (var index = 0; index < stageData.ArySectionDatas.Count; index++)
{
var crown = (CrownType)stageData.PlayResult;
if (crown == CrownType.Gold && stageData.OkCnt == 0)
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 sectionData = stageData.ArySectionDatas[index];
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;
}

View File

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

View File

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

View File

@ -10,3 +10,5 @@ global using TaikoLocalServer.Common.Utils;
global using TaikoLocalServer.Context;
global using TaikoLocalServer.Entities;
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
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")
@ -197,6 +256,9 @@ namespace TaikoLocalServer.Migrations
b.Property<uint>("AchievementDisplayDifficulty")
.HasColumnType("INTEGER");
b.Property<int>("AiWinCount")
.HasColumnType("INTEGER");
b.Property<uint>("ColorBody")
.HasColumnType("INTEGER");
@ -273,6 +335,29 @@ namespace TaikoLocalServer.Migrations
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")
@ -332,6 +417,11 @@ namespace TaikoLocalServer.Migrations
b.Navigation("Ba");
});
modelBuilder.Entity("TaikoLocalServer.Entities.AiScoreDatum", b =>
{
b.Navigation("AiSectionScoreData");
});
modelBuilder.Entity("TaikoLocalServer.Entities.DanScoreDatum", b =>
{
b.Navigation("DanStageScoreData");

View File

@ -1,87 +1,125 @@
using System.Reflection;
using System.Security.Authentication;
using Microsoft.AspNetCore.HttpLogging;
using Microsoft.AspNetCore.HttpOverrides;
using TaikoLocalServer.Middlewares;
using TaikoLocalServer.Services;
using TaikoLocalServer.Services.Extentions;
using TaikoLocalServer.Services.Interfaces;
using TaikoLocalServer.Settings;
using Throw;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
// Manually enable tls 1.0
builder.WebHost.UseKestrel(kestrelOptions =>
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.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.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 =>
{
builder.Host.UseSerilog((context, configuration) =>
{
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!");
}
// Add services to the container.
builder.Services.AddOptions();
builder.Services.AddSingleton<IGameDataService, GameDataService>();
builder.Services.Configure<ServerSettings>(builder.Configuration.GetSection(nameof(ServerSettings)));
builder.Services.AddControllers().AddProtoBufNet();
builder.Services.AddDbContext<TaikoDbContext>(option =>
{
var dbName = builder.Configuration["DbFileName"];
if (string.IsNullOrEmpty(dbName))
{
dbName = Constants.DEFAULT_DB_NAME;
}
var path = Path.Combine(PathHelper.GetRootPath(), dbName);
option.UseSqlite($"Data Source={path}");
});
builder.Services.AddHttpLogging(options =>
{
options.LoggingFields = HttpLoggingFields.RequestProperties |
HttpLoggingFields.ResponseStatusCode;
});
builder.Services.AddMemoryCache();
builder.Services.AddCors(options =>
{
options.AddPolicy("DevCorsPolicy", policy =>
});
builder.Services.AddMemoryCache();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAllCorsPolicy", policy =>
{
policy
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
builder.Services.AddTaikoDbServices();
});
builder.Services.AddTaikoDbServices();
var app = builder.Build();
var app = builder.Build();
// Migrate db
using (var scope = app.Services.CreateScope())
{
// Migrate db
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<TaikoDbContext>();
db.Database.Migrate();
}
}
var gameDataService = app.Services.GetService<IGameDataService>();
gameDataService.ThrowIfNull();
await gameDataService.InitializeAsync();
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);
};
});
// For reverse proxy
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
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("DevCorsPolicy");
// For blazor hosting
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseCors("AllowAllCorsPolicy");
// For blazor hosting
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseHttpLogging();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.UseHttpLogging();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.UseWhen(context => context.Request.Path.StartsWithSegments("/sys/servlet/PowerOn", StringComparison.InvariantCulture),
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/sys/servlet/PowerOn", StringComparison.InvariantCulture),
applicationBuilder => applicationBuilder.UseAllNetRequestMiddleware());
app.Run();
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Unhandled exception");
}
finally
{
Log.Information("Shut down complete");
Log.CloseAndFlush();
}

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 Swan.Mapping;
using TaikoLocalServer.Services.Interfaces;
namespace TaikoLocalServer.Services;

View File

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

View File

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

View File

@ -1,8 +1,8 @@
using System.Collections.Immutable;
using System.Text.Json;
using ICSharpCode.SharpZipLib.GZip;
using SharedProject.Models;
using Swan.Mapping;
using TaikoLocalServer.Services.Interfaces;
using Throw;
namespace TaikoLocalServer.Services;
@ -54,6 +54,10 @@ public class GameDataService : IGameDataService
var danDataPath = Path.Combine(dataPath, Constants.DAN_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 danDataFile = File.OpenRead(danDataPath);
await using var songIntroDataFile = File.OpenRead(songIntroDataPath);
@ -69,6 +73,18 @@ public class GameDataService : IGameDataService
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)
{
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 Swan.Mapping;
using TaikoLocalServer.Services.Interfaces;
using Throw;
namespace TaikoLocalServer.Services;
@ -32,7 +31,7 @@ public class SongBestDatumService : ISongBestDatumService
return;
}
await context.SongBestData.AddAsync(datum);
context.SongBestData.Add(datum);
await context.SaveChangesAsync();
}
@ -43,6 +42,10 @@ public class SongBestDatumService : ISongBestDatumService
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();
foreach (var bestData in result)
{
@ -65,6 +68,16 @@ public class SongBestDatumService : ISongBestDatumService
nameof(SongPlayDatum.DrumrollCount),
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;

View File

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

View File

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

View File

@ -1,8 +1,10 @@
namespace TaikoLocalServer.Settings;
public class UrlSettings
public class ServerSettings
{
public string MuchaUrl { 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>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>0.3.0-alpha</Version>
</PropertyGroup>
<ItemGroup>
@ -11,16 +12,18 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0-preview.7.22376.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-preview.7.22376.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0-preview.7.22376.2">
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0-rc.1.22426.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.1.22426.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0-rc.1.22426.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="protobuf-net" 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.Logging" Version="6.0.2-beta.69" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
@ -32,18 +35,24 @@
<None Update="Certificates\cert.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Certificates\root.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\data\music_attribute.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\dan_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\intro_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</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>

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",
"GameUrl": "vsapi.taiko-p.jp"
"GameUrl": "vsapi.taiko-p.jp",
"EnableMoreSongs": false
},
"DbFileName" : "taiko.db3",
"Logging": {
"LogLevel": {
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
"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": "*",
"Kestrel": {
"Endpoints": {
"Server": {
"BaseServer": {
"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": {

View File

@ -68,9 +68,8 @@
FullWidth="true"
AnchorOrigin="Origin.BottomCenter"
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}/AIBattle")">AI Battle</MudMenuItem>*@
</MudMenu>
</MudStack>
</MudCardActions>

View File

@ -1,11 +1,11 @@
@inject IGameDataService GameDataService
@inject HttpClient Client
@page "/Cards/{baid:int}/TaikoMode"
@page "/Cards/{baid:int}/HighScores"
<MudBreadcrumbs Items="breadcrumbs" Class="px-0"></MudBreadcrumbs>
<h1>Taiko Mode</h1>
<h1>High Scores</h1>
<MudText Typo="Typo.caption">Card: @Baid</MudText>
<MudGrid Class="my-8">
@ -29,7 +29,7 @@
Icon="@GetDifficultyIcon(difficulty)">
@if (songBestDataMap.ContainsKey(difficulty))
{
<MudDataGrid Items="@songBestDataMap[difficulty]"
<MudDataGrid Items="@songBestDataMap[difficulty]" RowClick="(DataGridRowClickEventArgs<SongBestData> _) => { return;}"
ColumnResizeMode="ResizeMode.None" RowsPerPage="25" Elevation="0">
<Columns>
<Column T="SongBestData" Field="@nameof(SongBestData.SongId)" Title="Song" StickyLeft="true">
@ -68,7 +68,7 @@
</MudChip>
</CellTemplate>
</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">
<CellTemplate>
<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>
</Column>
<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.MissCount)" Title="Bad" 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.MissCount)" Title="Bad" 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.LastPlayTime)" Title="Last Played"/>
<Column T="SongBestData" Field="@nameof(SongBestData.ComboCount)" Title="MAX Combo" Sortable="false" />
<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>
<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>
<MudDataGridPager T="SongBestData"/>
</PagerContent>

View File

@ -1,6 +1,9 @@
namespace TaikoWebUI.Pages;
using static MudBlazor.Colors;
using System;
public partial class TaikoMode
namespace TaikoWebUI.Pages;
public partial class HighScores
{
[Parameter]
public int Baid { get; set; }
@ -41,7 +44,7 @@ public partial class TaikoMode
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)
@ -135,5 +138,17 @@ public partial class TaikoMode
_ => ""
};
}
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
{
BaseAddress = new Uri(builder.Configuration.GetValue<string>("BaseUrl"))
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
builder.Services.AddMudServices();
builder.Services.AddSingleton<IGameDataService, GameDataService>();
@ -15,10 +15,6 @@ builder.Services.AddSingleton<IGameDataService, GameDataService>();
var host = builder.Build();
var gameDataService = host.Services.GetRequiredService<IGameDataService>();
#if DEBUG
await gameDataService.InitializeAsync(builder.Configuration.GetValue<string>("DataBaseUrl"));
#else
await gameDataService.InitializeAsync(builder.Configuration.GetValue<string>("BaseUrl"));
#endif
await gameDataService.InitializeAsync(builder.HostEnvironment.BaseAddress);
await host.RunAsync();

View File

@ -1,4 +1,7 @@
using System.Collections.Immutable;
using System.Net;
using System.Text.Json;
using ICSharpCode.SharpZipLib.GZip;
using Swan.Mapping;
using TaikoWebUI.Shared.Models;
@ -27,16 +30,13 @@ public class GameDataService : IGameDataService
public async Task InitializeAsync(string dataBaseUrl)
{
var musicInfo = await client.GetFromJsonAsync<MusicInfo>($"{dataBaseUrl}/data/musicinfo.json");
var wordList = await client.GetFromJsonAsync<WordList>($"{dataBaseUrl}/data/wordlist.json");
var musicOrder = await client.GetFromJsonAsync<MusicOrder>($"{dataBaseUrl}/data/music_order.json");
dataBaseUrl = dataBaseUrl.TrimEnd('/');
var musicInfo = await GetData<MusicInfo>(dataBaseUrl, Constants.MUSIC_INFO_BASE_NAME);
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");
musicInfo.ThrowIfNull();
wordList.ThrowIfNull();
musicOrder.ThrowIfNull();
danData.ThrowIfNull();
danMap = danData.ToImmutableDictionary(data => data.DanId);
// To prevent duplicate entries in wordlist
@ -52,6 +52,36 @@ public class GameDataService : IGameDataService
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)
{
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_COLOR_MAX = 63;
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>
<PackageReference Include="Autocomplete.Clients" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.7" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.9" PrivateAssets="all" />
<PackageReference Include="MudBlazor" Version="6.0.15" />
<PackageReference Include="SharpZipLib" Version="1.4.0" />
<PackageReference Include="Swan.Core" Version="6.0.2-beta.90" />
</ItemGroup>
@ -19,7 +20,19 @@
</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>
</Content>
<Content Update="wwwroot\data\musicinfo.json">
@ -28,9 +41,6 @@
<Content Update="wwwroot\data\wordlist.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\music_order.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</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

@ -30,3 +30,22 @@
position: relative;
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;
}
}