From f7e4e63130b09e828c18b46a0ff6e180efa349bd Mon Sep 17 00:00:00 2001 From: Albie Date: Tue, 27 Dec 2022 16:31:54 +0000 Subject: [PATCH 01/10] move enum to shared lib --- DragonFruit.Six.Api/Enums/PlatformGroup.cs | 14 ++++++++++++++ DragonFruit.Six.Api/Modern/Enums/PlatformGroup.cs | 11 ----------- 2 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 DragonFruit.Six.Api/Enums/PlatformGroup.cs delete mode 100644 DragonFruit.Six.Api/Modern/Enums/PlatformGroup.cs diff --git a/DragonFruit.Six.Api/Enums/PlatformGroup.cs b/DragonFruit.Six.Api/Enums/PlatformGroup.cs new file mode 100644 index 00000000..ca80f4d9 --- /dev/null +++ b/DragonFruit.Six.Api/Enums/PlatformGroup.cs @@ -0,0 +1,14 @@ +// Dragon6 API Copyright DragonFruit Network +// Licensed under Apache-2. Refer to the LICENSE file for more info + +using System; + +namespace DragonFruit.Six.Api.Enums +{ + [Flags] + public enum PlatformGroup + { + PC = 1, + Console = 2 + } +} diff --git a/DragonFruit.Six.Api/Modern/Enums/PlatformGroup.cs b/DragonFruit.Six.Api/Modern/Enums/PlatformGroup.cs deleted file mode 100644 index ac700b0b..00000000 --- a/DragonFruit.Six.Api/Modern/Enums/PlatformGroup.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Dragon6 API Copyright DragonFruit Network -// Licensed under Apache-2. Refer to the LICENSE file for more info - -namespace DragonFruit.Six.Api.Modern.Enums -{ - public enum PlatformGroup : byte - { - PC, - Console - } -} From 74bdfee5efbc140812eb6ed2c518e1dd1a7d528f Mon Sep 17 00:00:00 2001 From: Albie Date: Tue, 27 Dec 2022 16:32:04 +0000 Subject: [PATCH 02/10] add ranked2 stats request --- .../Seasonal/Requests/Ranked2StatsRequest.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 DragonFruit.Six.Api/Seasonal/Requests/Ranked2StatsRequest.cs diff --git a/DragonFruit.Six.Api/Seasonal/Requests/Ranked2StatsRequest.cs b/DragonFruit.Six.Api/Seasonal/Requests/Ranked2StatsRequest.cs new file mode 100644 index 00000000..fb2a3190 --- /dev/null +++ b/DragonFruit.Six.Api/Seasonal/Requests/Ranked2StatsRequest.cs @@ -0,0 +1,34 @@ +// Dragon6 API Copyright DragonFruit Network +// Licensed under Apache-2. Refer to the LICENSE file for more info + +using System.Collections.Generic; +using System.Linq; +using DragonFruit.Data; +using DragonFruit.Data.Parameters; +using DragonFruit.Six.Api.Accounts.Entities; +using DragonFruit.Six.Api.Accounts.Enums; +using DragonFruit.Six.Api.Enums; + +namespace DragonFruit.Six.Api.Seasonal.Requests +{ + public class Ranked2StatsRequest : UbiApiRequest + { + public override string Path => Platform.CrossPlatform.SpaceUrl(2) + "/title/r6s/skill/full_profiles"; + + protected override UbisoftService? RequiredTokenSource => UbisoftService.RainbowSixClient; + + public Ranked2StatsRequest(IEnumerable accounts, PlatformGroup platforms) + { + Accounts = accounts; + PlatformGroups = platforms; + } + + public IEnumerable Accounts { get; set; } + + [QueryParameter("platform_families", EnumHandlingMode.StringLower)] + public PlatformGroup PlatformGroups { get; set; } + + [QueryParameter("profile_ids", CollectionConversionMode.Concatenated)] + private IEnumerable AccountIdentifiers => Accounts.Select(x => x.ProfileId); + } +} From 8a4acf8458273221e00d846d5a9b18f269a2af4e Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 28 Dec 2022 10:29:57 +0000 Subject: [PATCH 03/10] add season stats models --- .../Entities/Ranked2SeasonMatchStats.cs | 25 ++++++++ .../Seasonal/Entities/Ranked2SeasonStats.cs | 58 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonMatchStats.cs create mode 100644 DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonStats.cs diff --git a/DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonMatchStats.cs b/DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonMatchStats.cs new file mode 100644 index 00000000..3b949ba5 --- /dev/null +++ b/DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonMatchStats.cs @@ -0,0 +1,25 @@ +// Dragon6 API Copyright DragonFruit Network +// Licensed under Apache-2. Refer to the LICENSE file for more info + +using System; +using DragonFruit.Six.Api.Utils; +using Newtonsoft.Json; + +namespace DragonFruit.Six.Api.Seasonal.Entities +{ + [Serializable] + [JsonObject(MemberSerialization.OptIn)] + public class Ranked2SeasonMatchStats + { + [JsonProperty("wins")] + public int Wins { get; set; } + + [JsonProperty("losses")] + public int Losses { get; set; } + + [JsonProperty("abandons")] + public int Abandons { get; set; } + + public float WL => RatioUtils.RatioOf(Wins, Losses + Abandons); + } +} diff --git a/DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonStats.cs b/DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonStats.cs new file mode 100644 index 00000000..0e626431 --- /dev/null +++ b/DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonStats.cs @@ -0,0 +1,58 @@ +// Dragon6 API Copyright DragonFruit Network +// Licensed under Apache-2. Refer to the LICENSE file for more info + +using System; +using DragonFruit.Six.Api.Enums; +using DragonFruit.Six.Api.Seasonal.Enums; +using DragonFruit.Six.Api.Utils; +using Newtonsoft.Json; + +namespace DragonFruit.Six.Api.Seasonal.Entities +{ + [Serializable] + [JsonObject(MemberSerialization.OptIn)] + public class Ranked2SeasonStats + { + [JsonProperty("board_id")] + public BoardType Board { get; set; } + + [JsonProperty("id")] + public string ProfileId { get; set; } + + [JsonProperty("season_id")] + public int SeasonId { get; set; } + + [JsonProperty("platform_family")] + public PlatformGroup Platform { get; set; } + + [JsonProperty("rank")] + public int Rank { get; set; } + + [JsonProperty("rank_points")] + public int RankPoints { get; set; } + + [JsonProperty("top_rank_position")] + public int TopRankPosition { get; set; } + + [JsonProperty("max_rank")] + public int MaxRank { get; set; } + + [JsonProperty("max_rank_points")] + public int MaxRankPoints { get; set; } + + public RankInfo RankInfo => Ranks.GetFromId(Rank); + + public RankInfo MaxRankInfo => Ranks.GetFromId(MaxRank); + + [JsonProperty("kills")] + public int Kills { get; set; } + + [JsonProperty("deaths")] + public int Deaths { get; set; } + + [JsonProperty("match_outcomes")] + public Ranked2SeasonMatchStats Matches { get; set; } + + public float KD => RatioUtils.RatioOf(Kills, Deaths); + } +} From 4cd8ddfb14f8e051370ca3a21cfe70dbc55ea1b1 Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 28 Dec 2022 10:30:22 +0000 Subject: [PATCH 04/10] rename deathmatch to warmup --- DragonFruit.Six.Api/Seasonal/Enums/BoardType.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Six.Api/Seasonal/Enums/BoardType.cs b/DragonFruit.Six.Api/Seasonal/Enums/BoardType.cs index 5246dee8..71c01612 100644 --- a/DragonFruit.Six.Api/Seasonal/Enums/BoardType.cs +++ b/DragonFruit.Six.Api/Seasonal/Enums/BoardType.cs @@ -22,11 +22,11 @@ public enum BoardType Casual = 2, [EnumMember(Value = "pvp_warmup")] - Deathmatch = 4, + Warmup = 4, [EnumMember(Value = "pvp_event")] Event = 8, - All = Ranked | Casual | Deathmatch | Event + All = Ranked | Casual | Warmup | Event } } From 67d905ab4d8b5e954ed9da35f2caeb438a62cb83 Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 28 Dec 2022 12:08:20 +0000 Subject: [PATCH 05/10] add tostring override --- DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonStats.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonStats.cs b/DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonStats.cs index 0e626431..2c7ad14f 100644 --- a/DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonStats.cs +++ b/DragonFruit.Six.Api/Seasonal/Entities/Ranked2SeasonStats.cs @@ -54,5 +54,7 @@ public class Ranked2SeasonStats public Ranked2SeasonMatchStats Matches { get; set; } public float KD => RatioUtils.RatioOf(Kills, Deaths); + + public override string ToString() => $"{Platform}/{Board}: {ProfileId}"; } } From fe57e15314dcedbd77ab91f594b5bf41cba5dda9 Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 28 Dec 2022 12:08:32 +0000 Subject: [PATCH 06/10] fix rankv4 not returning correctly --- DragonFruit.Six.Api/Seasonal/Ranks.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DragonFruit.Six.Api/Seasonal/Ranks.cs b/DragonFruit.Six.Api/Seasonal/Ranks.cs index 96a89dcc..e18ae374 100644 --- a/DragonFruit.Six.Api/Seasonal/Ranks.cs +++ b/DragonFruit.Six.Api/Seasonal/Ranks.cs @@ -39,10 +39,10 @@ public static RankInfo GetRank(int identifier, int season = -1, bool isMMR = fal >= 15 and <= 22 => RankingV2, // season 23-27 - <= 27 => RankingV3, + >= 23 and <= 27 => RankingV3, // season 28- (incl. latest season identifier) - _ => RankingV4 + -1 or _ => RankingV4 }; if (isMMR) From 6ff796488d5316ec2f32a1aa869d57c7c3c06607 Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 28 Dec 2022 12:08:41 +0000 Subject: [PATCH 07/10] add ranked extensions and test --- .../Data/SeasonalStatsTests.cs | 10 +++- .../Seasonal/SeasonStatsExtensions.cs | 55 ++++++++++++++++++- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/DragonFruit.Six.Api.Tests/Data/SeasonalStatsTests.cs b/DragonFruit.Six.Api.Tests/Data/SeasonalStatsTests.cs index d7d6dc8e..07c067de 100644 --- a/DragonFruit.Six.Api.Tests/Data/SeasonalStatsTests.cs +++ b/DragonFruit.Six.Api.Tests/Data/SeasonalStatsTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; +using DragonFruit.Six.Api.Enums; using DragonFruit.Six.Api.Seasonal; using DragonFruit.Six.Api.Seasonal.Enums; using NUnit.Framework; @@ -31,7 +32,7 @@ public async Task TestSeasonalRecords(string userId, int season15Rank, int seaso [Test] public async Task TestMultiPlatformMultiVersionRanked() { - var stats = await Client.GetSeasonalStatsRecordsAsync(Accounts, new[] { 28, 27, 26, 25 }); + var stats = (await Client.GetSeasonalStatsRecordsAsync(Accounts, new[] { 28, 27, 26, 25 })).ToList(); Assert.Greater(stats.Count, 48); var casualMMR = stats.Single(x => x.ProfileId == "14c01250-ef26-4a32-92ba-e04aa557d619" && x.Board == BoardType.Casual && x.SeasonId == 26).MMR; @@ -40,5 +41,12 @@ public async Task TestMultiPlatformMultiVersionRanked() var rankedWins = stats.Where(x => x.ProfileId == "45c0cccb-a1a8-4433-b3d8-52aaa40d16d2" && x.Board == BoardType.Ranked).OrderByDescending(x => x.SeasonId).First().Wins; Assert.Greater(rankedWins, 5); } + + [Test] + public async Task TestRanked2SeasonStats() + { + var stats = (await Client.GetSeasonalStatsAsync(Accounts, PlatformGroup.PC)).ToList(); + Assert.GreaterOrEqual(Accounts.Count() * 4, stats.Count); + } } } diff --git a/DragonFruit.Six.Api/Seasonal/SeasonStatsExtensions.cs b/DragonFruit.Six.Api/Seasonal/SeasonStatsExtensions.cs index c83794f3..1795eb0f 100644 --- a/DragonFruit.Six.Api/Seasonal/SeasonStatsExtensions.cs +++ b/DragonFruit.Six.Api/Seasonal/SeasonStatsExtensions.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using DragonFruit.Six.Api.Accounts.Entities; +using DragonFruit.Six.Api.Enums; using DragonFruit.Six.Api.Seasonal.Entities; using DragonFruit.Six.Api.Seasonal.Enums; using DragonFruit.Six.Api.Seasonal.Requests; @@ -16,6 +17,54 @@ namespace DragonFruit.Six.Api.Seasonal { public static class SeasonStatsExtensions { + /// + /// Get current season stats for the provided + /// + /// The to use + /// The to get stats for + /// The s to return stats for + /// Optional cancellation token + /// The current season stats for each account. Ranked, Casual, Deathmatch and Event stats will be returned as a separate object + /// + /// This extension uses a protected endpoint to access data, and as such a supported token is needed. + /// Ensure that your implementation requests a token for the provided when called. + /// + public static Task> GetSeasonalStatsAsync(this Dragon6Client client, UbisoftAccount account, PlatformGroup platforms = PlatformGroup.PC | PlatformGroup.Console, CancellationToken cancellation = default) + { + return GetSeasonalStatsAsync(client, account.Yield(), platforms, cancellation); + } + + /// + /// Get current season stats for the provided s (crossplay compatible) + /// + /// The to use + /// The s to get stats for + /// The s to return stats for + /// Optional cancellation token + /// The current season stats for each account. Ranked, Casual, Deathmatch and Event stats will be returned as a separate object + /// + /// This extension uses a protected endpoint to access data, and as such a supported token is needed. + /// Ensure that your implementation requests a token for the provided when called. + /// + public static async Task> GetSeasonalStatsAsync(this Dragon6Client client, IEnumerable accounts, PlatformGroup platforms = PlatformGroup.PC | PlatformGroup.Console, CancellationToken cancellation = default) + { + var request = new Ranked2StatsRequest(accounts, platforms); + var response = await client.PerformAsync(request, cancellation).ConfigureAwait(false); + + return response.SelectTokens("$..full_profiles[*]").Select(x => + { + var children = x.Values(); + var root = (JObject)children.First(); + + foreach (var other in children.Skip(1).Cast()) + { + root.Merge(other); + } + + return root.ToObject(); + }); + } + /// /// Get seasonal stats "records" for the provided s /// @@ -29,7 +78,7 @@ public static class SeasonStatsExtensions /// This call is able to return results for multiple accounts spanning large numbers of seasons, ranking boards and regions. /// Elements are returned ungrouped and unordered - it is recommended to sort then convert the returned to an array or list afterwards. /// - public static Task> GetSeasonalStatsRecordsAsync(this Dragon6Client client, UbisoftAccount account, IEnumerable seasonIds, BoardType? boards = null, Region? regions = null, CancellationToken token = default) + public static Task> GetSeasonalStatsRecordsAsync(this Dragon6Client client, UbisoftAccount account, IEnumerable seasonIds, BoardType? boards = null, Region? regions = null, CancellationToken token = default) { return GetSeasonalStatsRecordsAsync(client, account.Yield(), seasonIds, boards, regions, token); } @@ -47,7 +96,7 @@ public static Task> GetSeasonalStatsRecordsAs /// This call is able to return results for multiple accounts spanning large numbers of seasons, ranking boards and regions. /// Elements are returned ungrouped and unordered - It is recommended to sort then convert the returned to an array or list afterwards. /// - public static async Task> GetSeasonalStatsRecordsAsync(this Dragon6Client client, IEnumerable accounts, IEnumerable seasonIds, BoardType? boards = null, Region? regions = null, CancellationToken token = default) + public static async Task> GetSeasonalStatsRecordsAsync(this Dragon6Client client, IEnumerable accounts, IEnumerable seasonIds, BoardType? boards = null, Region? regions = null, CancellationToken token = default) { boards ??= BoardType.All; seasonIds ??= (-1).Yield(); @@ -84,7 +133,7 @@ public static async Task> GetSeasonalStatsRec var seasonalStatsRequests = requests.Select(x => client.PerformAsync(x, token)); var seasonalStatsResponses = await Task.WhenAll(seasonalStatsRequests).ConfigureAwait(false); - return seasonalStatsResponses.SelectMany(x => x.SelectTokens("$..players_skill_records[*]")).Select(x => x.ToObject()).ToList(); + return seasonalStatsResponses.SelectMany(x => x.SelectTokens("$..players_skill_records[*]")).Select(x => x.ToObject()); } } } From 6489eae1c09b357902d7ec3df2d1eb8169de1525 Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 28 Dec 2022 12:10:25 +0000 Subject: [PATCH 08/10] add tostring() for seasonal record --- DragonFruit.Six.Api/Seasonal/Entities/SeasonalStats.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DragonFruit.Six.Api/Seasonal/Entities/SeasonalStats.cs b/DragonFruit.Six.Api/Seasonal/Entities/SeasonalStats.cs index 42456d3c..2468289a 100644 --- a/DragonFruit.Six.Api/Seasonal/Entities/SeasonalStats.cs +++ b/DragonFruit.Six.Api/Seasonal/Entities/SeasonalStats.cs @@ -101,5 +101,7 @@ public class SeasonalStats public RankInfo RankInfo => _rankInfo ??= Ranks.GetRank(Rank, SeasonId); public RankInfo MaxRankInfo => _maxRankInfo ??= Ranks.GetRank(MaxRank, SeasonId); public RankInfo MMRRankInfo => _mmrRankInfo ??= Ranks.GetRank((int)MMR, SeasonId, true); + + public override string ToString() => $"S{SeasonId}/{Board}: {ProfileId}"; } } From ecd1c8ea14c9e1522fa33dac9630bf63aa056a47 Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 28 Dec 2022 12:10:39 +0000 Subject: [PATCH 09/10] remove unused container class --- .../Entities/SeasonalStatsResponse.cs | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 DragonFruit.Six.Api/Seasonal/Entities/SeasonalStatsResponse.cs diff --git a/DragonFruit.Six.Api/Seasonal/Entities/SeasonalStatsResponse.cs b/DragonFruit.Six.Api/Seasonal/Entities/SeasonalStatsResponse.cs deleted file mode 100644 index 95b57b92..00000000 --- a/DragonFruit.Six.Api/Seasonal/Entities/SeasonalStatsResponse.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Dragon6 API Copyright DragonFruit Network -// Licensed under Apache-2. Refer to the LICENSE file for more info - -using System; -using System.Collections.Generic; -using DragonFruit.Six.Api.Accounts.Entities; -using Newtonsoft.Json; - -namespace DragonFruit.Six.Api.Seasonal.Entities -{ - [Serializable] - [JsonObject(MemberSerialization.OptIn)] - public class SeasonalStatsResponse - { - [JsonProperty("players")] - private Dictionary Data { get; set; } - - public IReadOnlyDictionary Stats => Data; - public SeasonalStats For(UbisoftAccount account) => For(account.ProfileId); - public SeasonalStats For(string profileId) => Data.TryGetValue(profileId, out var data) ? data : null; - } -} From aaf327c1a48e90300de3011cb92c6bfb2639672d Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 28 Dec 2022 13:07:45 +0000 Subject: [PATCH 10/10] improve xmldocs --- DragonFruit.Six.Api/Seasonal/SeasonStatsExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Six.Api/Seasonal/SeasonStatsExtensions.cs b/DragonFruit.Six.Api/Seasonal/SeasonStatsExtensions.cs index 1795eb0f..cd4c31fe 100644 --- a/DragonFruit.Six.Api/Seasonal/SeasonStatsExtensions.cs +++ b/DragonFruit.Six.Api/Seasonal/SeasonStatsExtensions.cs @@ -24,7 +24,7 @@ public static class SeasonStatsExtensions /// The to get stats for /// The s to return stats for /// Optional cancellation token - /// The current season stats for each account. Ranked, Casual, Deathmatch and Event stats will be returned as a separate object + /// The current season stats for the account returning Ranked, Casual, Deathmatch and Event stats as separate objects /// /// This extension uses a protected endpoint to access data, and as such a supported token is needed. /// Ensure that your implementation requests a token for the provided when called.