Skip to content

Commit c47385a

Browse files
committed
feat(bests): Add support for collaborative charts and theoretical score processing
- Add DfGatewayService and LxnsGatewayService for unified handling of external API calls - Add ServiceExecutionHelper to provide common utility methods (parameter validation, HTTP status mapping, etc.) - Refactor BestsService and add ProcessBestsByTagsAsync method to support collaborative chart processing - Implement collaborative chart score calculation logic in DfBestsService and LxnsBestsService - Add theoretical score (riren) generation feature for displaying theoretical maximum scores - Refactor list service and extract BuildListRecords method to reuse record filtering logic - Optimize list statistics counting implementation, using arrays and Interlocked for improved performance
1 parent 37b4436 commit c47385a

File tree

9 files changed

+424
-165
lines changed

9 files changed

+424
-165
lines changed

src/Services/BestsService.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
using Grpc.Core;
12
using Limekuma.Prober.Common;
23
using Limekuma.Utils;
4+
using System.Collections.Immutable;
35

46
namespace Limekuma.Services;
57

@@ -12,4 +14,41 @@ private static async Task PrepareDataAsync(CommonUser user, IReadOnlyList<Common
1214
await ServiceHelper.PrepareRecordDataAsync(bestsEver);
1315
await ServiceHelper.PrepareRecordDataAsync(bestsCurrent);
1416
}
15-
}
17+
18+
private static async Task<(ImmutableArray<CommonRecord> BestEver, ImmutableArray<CommonRecord> BestCurrent, int EverTotal, int CurrentTotal, CommonUser?)>
19+
ProcessBestsByTagsAsync(
20+
IReadOnlyList<string> tags,
21+
string condition,
22+
ImmutableArray<CommonRecord> records,
23+
Func<string, Task<(CommonUser, ImmutableArray<CommonRecord>)>> secondDataProvider)
24+
{
25+
if (ScoreProcesserHelper.GetProcesserByTags(tags) is not { } selectedProcesser)
26+
{
27+
throw new RpcException(new(StatusCode.InvalidArgument, "Invalid arguments"));
28+
}
29+
30+
CommonUser? user2p = null;
31+
bool mayMask = ServiceExecutionHelper.HasMaskedScores(records);
32+
ServiceExecutionHelper.EnsurePermission(!(mayMask && selectedProcesser.MaskMutex), "Mask enabled");
33+
34+
ImmutableArray<CommonRecord> bestEver;
35+
ImmutableArray<CommonRecord> bestCurrent;
36+
if (selectedProcesser.RequireSecondData)
37+
{
38+
(user2p, ImmutableArray<CommonRecord> records2p) = await secondDataProvider(condition);
39+
(bestEver, bestCurrent) = selectedProcesser.Processer.Process(records, records2p);
40+
}
41+
else
42+
{
43+
(Func<CommonRecord, bool> predicate, bool maskMutex) = ScoreFilterHelper.GetPredicateByTags(tags, condition);
44+
ServiceExecutionHelper.EnsurePermission(!(mayMask && maskMutex), "Mask enabled");
45+
46+
ImmutableArray<CommonRecord> filteredRecords = [.. records.Where(predicate).SortRecordForBests()];
47+
(bestEver, bestCurrent) = selectedProcesser.Processer.Process(filteredRecords);
48+
}
49+
50+
int everTotal = bestEver.Sum(x => x.DXRating);
51+
int currentTotal = bestCurrent.Sum(x => x.DXRating);
52+
return (bestEver, bestCurrent, everTotal, currentTotal, user2p);
53+
}
54+
}

src/Services/DfBestsService.cs

Lines changed: 110 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,39 @@
11
using Grpc.Core;
22
using Limekuma.Prober.Common;
3-
using Limekuma.Prober.DivingFish;
3+
using Limekuma.Prober.DivingFish.Enums;
44
using Limekuma.Prober.DivingFish.Models;
55
using Limekuma.Render;
66
using Limekuma.Utils;
77
using SixLabors.ImageSharp;
88
using System.Collections.Immutable;
9-
using System.Net;
9+
using System.Text.Json.Serialization;
1010

1111
namespace Limekuma.Services;
1212

1313
public partial class BestsService
1414
{
15+
private static async Task<(CommonUser, ImmutableArray<CommonRecord>)> PrepareDfRecordsForProcessAsync(string token, uint? qq, int? frame, int? plate, int? icon)
16+
{
17+
ServiceExecutionHelper.EnsureArgument(qq.HasValue && frame.HasValue && plate.HasValue && icon.HasValue);
18+
PlayerData player = await DfGatewayService.GetPlayerDataAsync(token, qq!.Value);
19+
20+
CommonUser user = player;
21+
user.FrameId = frame!.Value;
22+
user.PlateId = plate!.Value;
23+
user.IconId = icon!.Value;
24+
return (user, [.. player.Records.ConvertAll<CommonRecord>(_ => _)]);
25+
}
26+
1527
private static async Task<(CommonUser, ImmutableArray<CommonRecord>, ImmutableArray<CommonRecord>, int, int)> PrepareDfDataAsync(
16-
uint qq, int frame, int plate, int icon)
28+
uint? qq, int? frame, int? plate, int? icon)
1729
{
18-
DfResourceClient df = new();
19-
Player player;
20-
try
21-
{
22-
player = await df.GetPlayerAsync(qq);
23-
}
24-
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.BadRequest)
25-
{
26-
throw new RpcException(new(StatusCode.NotFound, ex.Message, ex));
27-
}
28-
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Forbidden)
29-
{
30-
throw new RpcException(new(StatusCode.PermissionDenied, ex.Message, ex));
31-
}
30+
ServiceExecutionHelper.EnsureArgument(qq.HasValue && frame.HasValue && plate.HasValue && icon.HasValue);
31+
Player player = await DfGatewayService.GetPlayerAsync(qq!.Value);
3232

3333
CommonUser user = player;
34-
user.FrameId = frame;
35-
user.PlateId = plate;
36-
user.IconId = icon;
34+
user.FrameId = frame!.Value;
35+
user.PlateId = plate!.Value;
36+
user.IconId = icon!.Value;
3737

3838
ImmutableArray<CommonRecord> bestEver = [.. player.Bests.Ever.ConvertAll<CommonRecord>(_ => _).SortRecordForBests()];
3939
int everTotal = bestEver.Sum(x => x.DXRating);
@@ -46,14 +46,100 @@ public partial class BestsService
4646
return (user, bestEver, bestCurrent, everTotal, currentTotal);
4747
}
4848

49+
private static async Task<(CommonUser, ImmutableArray<CommonRecord>, ImmutableArray<CommonRecord>, int, int)> PrepareRiRenDfDataAsync()
50+
{
51+
List<CommonRecord> allRecords = [];
52+
foreach (Song song in Songs.Shared)
53+
{
54+
if (!int.TryParse(song.Id, out int id))
55+
{
56+
continue;
57+
}
58+
59+
int chartCount = Math.Min(song.Charts.Count, Math.Min(song.LevelValues.Count, song.Levels.Count));
60+
for (int i = 0; i < chartCount; i++)
61+
{
62+
allRecords.Add(new Record()
63+
{
64+
Achievements = 101,
65+
ComboFlag = ComboFlags.AllPerfectPlus,
66+
Difficulty = song.Type is SongTypes.Utage ? Difficulties.Utage : (Difficulties)(i + 1),
67+
DifficultyIndex = i,
68+
DXRating = (int)(song.LevelValues[i] * 22.512),
69+
DXScore = song.Charts[i].Notes.Total * 3,
70+
Id = id,
71+
Level = song.Levels[i],
72+
LevelValue = song.LevelValues[i],
73+
Rank = Ranks.SSSPlus,
74+
SyncFlag = SyncFlags.FullSyncDXPlus,
75+
Title = song.Title,
76+
Type = song.Type
77+
});
78+
}
79+
}
80+
81+
IEnumerable<CommonRecord> sortedRecords = allRecords.SortRecordForBests();
82+
ImmutableArray<CommonRecord> bestEver = [.. sortedRecords.Where(record => !record.Chart.Song.InCurrentGenre).Take(35)];
83+
ImmutableArray<CommonRecord> bestCurrent = [.. sortedRecords.Where(record => record.Chart.Song.InCurrentGenre).Take(15)];
84+
int everTotal = bestEver.Sum(x => x.DXRating);
85+
int currentTotal = bestCurrent.Sum(x => x.DXRating);
86+
CommonUser user = new()
87+
{
88+
Name = "DXKuma",
89+
Rating = everTotal + currentTotal,
90+
TrophyColor = TrophyColor.Rainbow,
91+
TrophyText = "でらっくま",
92+
CourseRank = CommonCourseRank.Urakaiden,
93+
ClassRank = ClassRank.LEGEND,
94+
IconId = 1,
95+
PlateId = 1,
96+
FrameId = 1
97+
};
98+
99+
await PrepareDataAsync(user, bestEver, bestCurrent);
100+
return (user, bestEver, bestCurrent, everTotal, currentTotal);
101+
}
102+
49103
public override async Task GetFromDivingFish(DivingFishBestsRequest request,
50104
IServerStreamWriter<ImageResponse> responseStream, ServerCallContext context)
51105
{
52-
(CommonUser user, ImmutableArray<CommonRecord> bestEver, ImmutableArray<CommonRecord> bestCurrent, int everTotal,
53-
int currentTotal) = await PrepareDfDataAsync(request.Qq, request.Frame, request.Plate, request.Icon);
106+
CommonUser user;
107+
CommonUser? user2p = null;
108+
ImmutableArray<CommonRecord> bestEver;
109+
ImmutableArray<CommonRecord> bestCurrent;
110+
int everTotal;
111+
int currentTotal;
112+
if (ScoreProcesserHelper.GetProcesserByTags(request.Tags) is not null)
113+
{
114+
(user, ImmutableArray<CommonRecord> records) = await PrepareDfRecordsForProcessAsync(request.Token, request.Qq, request.Frame, request.Plate, request.Icon);
115+
(bestEver, bestCurrent, everTotal, currentTotal, user2p) = await ProcessBestsByTagsAsync(
116+
request.Tags,
117+
request.Condition,
118+
records,
119+
async condition =>
120+
{
121+
CoopExtraInfo extraInfo = ServiceExecutionHelper.DeserializeOrThrow<CoopExtraInfo>(condition, "Invalid arguments");
122+
return await PrepareDfRecordsForProcessAsync(request.Token, extraInfo.Qq, extraInfo.Frame, extraInfo.Plate, extraInfo.Icon);
123+
});
124+
}
125+
else if (request.Tags.Contains("common"))
126+
{
127+
(user, bestEver, bestCurrent, everTotal, currentTotal) = await PrepareDfDataAsync(request.Qq, request.Frame, request.Plate, request.Icon);
128+
}
129+
else if (request.Tags.Contains("riren"))
130+
{
131+
(user, bestEver, bestCurrent, everTotal, currentTotal) = await PrepareRiRenDfDataAsync();
132+
}
133+
else
134+
{
135+
throw new RpcException(new(StatusCode.InvalidArgument, "Invalid arguments"));
136+
}
137+
54138
using Image bestsImage = await new Drawer().DrawBestsAsync(user, bestEver, bestCurrent, everTotal, currentTotal,
55-
request.Condition, "divingfish", request.Tags);
139+
request.Condition, "divingfish", request.Tags, user2p);
56140

57141
await responseStream.WriteToResponseAsync(bestsImage);
58142
}
59-
}
143+
144+
public record CoopExtraInfo([property: JsonPropertyName("qq")] uint Qq, [property: JsonPropertyName("frame")] int Frame, [property: JsonPropertyName("plate")] int Plate, [property: JsonPropertyName("icon")] int Icon);
145+
}

src/Services/DfGatewayService.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using Grpc.Core;
2+
using Limekuma.Prober.DivingFish;
3+
using Limekuma.Prober.DivingFish.Models;
4+
using System.Net;
5+
6+
namespace Limekuma.Services;
7+
8+
internal static class DfGatewayService
9+
{
10+
internal static Task<PlayerData> GetPlayerDataAsync(string token, uint qq)
11+
{
12+
DfDeveloperClient client = new(token);
13+
return ServiceExecutionHelper.ExecuteWithHttpMappingAsync(
14+
() => client.GetPlayerDataAsync(qq),
15+
(HttpStatusCode.BadRequest, StatusCode.NotFound),
16+
(HttpStatusCode.Forbidden, StatusCode.PermissionDenied));
17+
}
18+
19+
internal static Task<Player> GetPlayerAsync(uint qq)
20+
{
21+
DfResourceClient client = new();
22+
return ServiceExecutionHelper.ExecuteWithHttpMappingAsync(
23+
() => client.GetPlayerAsync(qq),
24+
(HttpStatusCode.BadRequest, StatusCode.NotFound),
25+
(HttpStatusCode.Forbidden, StatusCode.PermissionDenied));
26+
}
27+
}

src/Services/DfListService.cs

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
using Grpc.Core;
22
using Limekuma.Prober.Common;
3-
using Limekuma.Prober.DivingFish;
43
using Limekuma.Prober.DivingFish.Models;
54
using Limekuma.Render;
65
using Limekuma.Utils;
76
using SixLabors.ImageSharp;
87
using System.Collections.Immutable;
9-
using System.Net;
108

119
namespace Limekuma.Services;
1210

@@ -15,38 +13,20 @@ public partial class ListService
1513
public override async Task GetFromDivingFish(DivingFishListRequest request,
1614
IServerStreamWriter<ImageResponse> responseStream, ServerCallContext context)
1715
{
18-
DfDeveloperClient df = new(request.Token);
19-
PlayerData player;
20-
try
21-
{
22-
player = await df.GetPlayerDataAsync(request.Qq);
23-
}
24-
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.BadRequest)
25-
{
26-
throw new RpcException(new(StatusCode.NotFound, ex.Message, ex));
27-
}
28-
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Forbidden)
29-
{
30-
throw new RpcException(new(StatusCode.PermissionDenied, ex.Message, ex));
31-
}
16+
PlayerData player = await DfGatewayService.GetPlayerDataAsync(request.Token, request.Qq);
3217

3318
CommonUser user = player;
3419
user.PlateId = request.Plate;
3520
user.IconId = request.Icon;
3621

37-
(Func<CommonRecord, bool> predicate, bool maskMutex) = ScoreFilterHelper.GetPredicateByTags(request.Tags, request.Condition);
38-
bool mayMask = player.Records.Any(r => r.DXScore is 0 && (r.DXScoreRank > 0 || r.Rank > Ranks.A));
39-
if (mayMask && maskMutex)
40-
{
41-
throw new RpcException(new(StatusCode.PermissionDenied, "Mask enabled"));
42-
}
22+
(ImmutableArray<CommonRecord> records, bool mayMask) =
23+
BuildListRecords(request.Tags, request.Condition, player.Records.ConvertAll<CommonRecord>(_ => _));
4324

44-
ImmutableArray<CommonRecord> records = [.. player.Records.ConvertAll<CommonRecord>(_ => _).Where(predicate).SortRecordForList()];
4525
(ImmutableArray<int> counts, int startIndex, int endIndex) = await PrepareDataAsync(user, records, request.Page);
4626

4727
using Image listImage = await new Drawer().DrawListAsync(user, records[startIndex..endIndex], request.Page, counts,
4828
records.Length, startIndex, request.Condition, mayMask, "divingfish", request.Tags);
4929

5030
await responseStream.WriteToResponseAsync(listImage);
5131
}
52-
}
32+
}

0 commit comments

Comments
 (0)