From 7ccd4dc396cde3e141c177ed513303c7a1940d75 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Mon, 8 Jul 2024 17:34:38 -0700 Subject: [PATCH 01/18] Add fallback logic for exchange image lookup --- .../Core/UserContextManager.cs | 17 +++++++++++++++++ .../OpenSpartan.Workshop.csproj | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/OpenSpartan.Workshop/Core/UserContextManager.cs b/src/OpenSpartan.Workshop/Core/UserContextManager.cs index 5da83a9..62ae5b6 100644 --- a/src/OpenSpartan.Workshop/Core/UserContextManager.cs +++ b/src/OpenSpartan.Workshop/Core/UserContextManager.cs @@ -1930,6 +1930,23 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => }, }; + // There is a chance that the image lookup is going to fail. In that case, we want to + // fallback to the "dumb" logic, and that is - get the offering and all the related metadata. + if (string.IsNullOrWhiteSpace(metadataContainer.ImagePath)) + { + if (offering.OfferingDisplayPath != null) + { + var offeringData = await SafeAPICall(async () => await HaloClient.GameCmsGetStoreOffering(offering.OfferingDisplayPath)); + if (offeringData != null && offeringData.Result != null) + { + if (!string.IsNullOrWhiteSpace(offeringData.Result.ObjectImagePath)) + { + metadataContainer.ImagePath = offeringData.Result.ObjectImagePath.Replace("\\", "/"); + } + } + } + } + if (Path.IsPathRooted(metadataContainer.ImagePath)) { metadataContainer.ImagePath = metadataContainer.ImagePath.TrimStart(Path.DirectorySeparatorChar); diff --git a/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj b/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj index d9e31a6..daa7a0c 100644 --- a/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj +++ b/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj @@ -67,7 +67,7 @@ - + From b96f2c4e93a3ef43b20dced8e6b04d6673f36e89 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Mon, 8 Jul 2024 17:56:02 -0700 Subject: [PATCH 02/18] QOL updates, including calendar and ranked match data --- CURRENTRELEASE.md | 13 ++++---- README.md | 2 +- .../Controls/SeasonCalendarControl.xaml | 7 +++-- .../Converters/CsrToProgressConverter.cs | 2 +- .../Converters/CsrToTooltipValueConverter.cs | 2 +- .../OpenSpartan.Workshop.csproj | 30 ++++++++----------- 6 files changed, 26 insertions(+), 30 deletions(-) diff --git a/CURRENTRELEASE.md b/CURRENTRELEASE.md index 229f371..f4b8034 100644 --- a/CURRENTRELEASE.md +++ b/CURRENTRELEASE.md @@ -1,10 +1,9 @@ -# OpenSpartan Workshop 1.0.6 (`CRUCIBLE-06072024`) +# OpenSpartan Workshop 1.0.7 (`CYLIX-06082924`) -- Fixes an issue with matches not being populated in full search mode. -- Fixes an issue where extra events are not loaded on first (cold) boot. -- Fixes an issue where matches based on a medal are not loaded due to a malformed SQLite query. - ->[!NOTE] ->This is a point release with a hotfix. Original changelog with new functionality captured in [1.0.4](https://github.com/OpenSpartan/openspartan-workshop/releases/tag/1.0.4). +- Improved image fallback for The Exchange, so that missing items now render properly. +- Season calendar now includes background images for each event, operation, and battle pass season. +- [#39] Removes the odd cross-out line in the calendar view. +- Calendar colors are now easier to read. +- Fixes how ranked match percentage is calculated, now showing proper values for next level. Refer to [**getting started guide**](https://openspartan.com/docs/workshop/guides/get-started/) to start using OpenSpartan Workshop. diff --git a/README.md b/README.md index 2f8b648..75cc501 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The benefit of using OpenSpartan Workshop is that for any more complex analysis You can download the application from this repository, in the **Releases** section. -Download for Windows button +Download for Windows button >[!IMPORTANT] >Requires Windows 10 (20H1 - `10.0.19041.0`) or later, [.NET Desktop Runtime 8.0+](https://dotnet.microsoft.com/download/dotnet/8.0), and the [latest Windows App SDK](https://learn.microsoft.com/windows/apps/windows-app-sdk/downloads). Both are bundled with the installer. diff --git a/src/OpenSpartan.Workshop/Controls/SeasonCalendarControl.xaml b/src/OpenSpartan.Workshop/Controls/SeasonCalendarControl.xaml index e9b6e49..78606f7 100644 --- a/src/OpenSpartan.Workshop/Controls/SeasonCalendarControl.xaml +++ b/src/OpenSpartan.Workshop/Controls/SeasonCalendarControl.xaml @@ -12,6 +12,7 @@ x:Name="CalendarViewControl" IsTodayHighlighted="true" TodayForeground="Transparent" + TodayBlackoutForeground="#FFFFFF" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" CalendarItemBorderBrush="DimGray" @@ -19,11 +20,11 @@ CalendarItemCornerRadius="0" BlackoutStrikethroughBrush="Transparent" BlackoutBackground="#5B6163" - BlackoutForeground="#2F2F2F" - DayItemFontSize="{StaticResource BodyTextBlockFontSize}" + BlackoutForeground="#B3A8AA" + DayItemFontSize="{StaticResource SubtitleTextBlockFontSize}" DayItemMargin="4,0,0,0" IsGroupLabelVisible="True" - FirstOfMonthLabelFontSize="{StaticResource BodyTextBlockFontSize}" + FirstOfMonthLabelFontSize="{StaticResource SubtitleTextBlockFontSize}" FirstOfMonthLabelFontWeight="Bold" HorizontalFirstOfMonthLabelAlignment="Right" FirstOfMonthLabelMargin="0,0,4,0" diff --git a/src/OpenSpartan.Workshop/Converters/CsrToProgressConverter.cs b/src/OpenSpartan.Workshop/Converters/CsrToProgressConverter.cs index 540785f..1f3455d 100644 --- a/src/OpenSpartan.Workshop/Converters/CsrToProgressConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/CsrToProgressConverter.cs @@ -15,7 +15,7 @@ public object Convert(object value, Type targetType, object parameter, string la // progress to report on. if (currentCsr.Value > -1) { - return (double)currentCsr.Value / (double)currentCsr.NextTierStart; + return ((double)currentCsr.Value - (double)currentCsr.TierStart) / ((double)currentCsr.NextTierStart - (double)currentCsr.TierStart); } else { diff --git a/src/OpenSpartan.Workshop/Converters/CsrToTooltipValueConverter.cs b/src/OpenSpartan.Workshop/Converters/CsrToTooltipValueConverter.cs index abcd4b6..beab184 100644 --- a/src/OpenSpartan.Workshop/Converters/CsrToTooltipValueConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/CsrToTooltipValueConverter.cs @@ -15,7 +15,7 @@ public object Convert(object value, Type targetType, object parameter, string la // progress to report on. if (currentCsr.Value > -1) { - return $"{currentCsr.Value}/{currentCsr.NextTierStart} ({((double)currentCsr.Value/(double)currentCsr.NextTierStart)*100:0.00}%)"; + return $"{currentCsr.Value}/{currentCsr.NextTierStart} ({(((double)currentCsr.Value - (double)currentCsr.TierStart) / ((double)currentCsr.NextTierStart - (double)currentCsr.TierStart)) * 100.0:0.00}%)"; } else { diff --git a/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj b/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj index daa7a0c..addf0e6 100644 --- a/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj +++ b/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj @@ -12,8 +12,8 @@ None true All - 1.0.6.0 - 1.0.6.0 + 1.0.7.0 + 1.0.7.0 enable @@ -44,7 +44,7 @@ Always - Always + Always @@ -56,10 +56,10 @@ - Always + Always - Always + Always @@ -93,7 +93,7 @@ MSBuild:Compile - MSBuild:Compile + MSBuild:Compile Always @@ -126,7 +126,7 @@ Always - Always + Always Always @@ -165,7 +165,7 @@ Always - Always + Always Always @@ -180,7 +180,7 @@ Always - Always + Always Always @@ -204,16 +204,16 @@ Always - MSBuild:Compile + MSBuild:Compile - MSBuild:Compile + MSBuild:Compile - MSBuild:Compile + MSBuild:Compile - MSBuild:Compile + MSBuild:Compile MSBuild:Compile @@ -230,13 +230,9 @@ MSBuild:Compile - - MSBuild:Compile - - MSBuild:Compile From d27f398f7fd2447ca110264f6d9a142ce427f39a Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Mon, 8 Jul 2024 18:06:10 -0700 Subject: [PATCH 03/18] Fixes #41, making average life a more readable string --- .../Controls/MatchesGridControl.xaml | 2 +- .../ComplexTimeToSimpleTimeConverter.cs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml index 75c3c98..6152ec2 100644 --- a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml +++ b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml @@ -188,7 +188,7 @@ - + diff --git a/src/OpenSpartan.Workshop/Converters/ComplexTimeToSimpleTimeConverter.cs b/src/OpenSpartan.Workshop/Converters/ComplexTimeToSimpleTimeConverter.cs index 24e04b7..7f614bf 100644 --- a/src/OpenSpartan.Workshop/Converters/ComplexTimeToSimpleTimeConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/ComplexTimeToSimpleTimeConverter.cs @@ -1,6 +1,6 @@ using Microsoft.UI.Xaml.Data; using System; -using System.Globalization; +using System.Linq; namespace OpenSpartan.Workshop.Converters { @@ -8,8 +8,20 @@ internal sealed class ComplexTimeToSimpleTimeConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - TimeSpan interval = (TimeSpan)value; - return string.Format(CultureInfo.InvariantCulture, "{0:D2}d {1:D2}hr {2:D2}min {3:D2}sec", interval.Days, interval.Hours, interval.Minutes, interval.Seconds); + if (value is TimeSpan interval) + { + var parts = new[] + { + interval.Days > 0 ? $"{interval.Days}d" : null, + interval.Hours > 0 ? $"{interval.Hours}hr" : null, + interval.Minutes > 0 ? $"{interval.Minutes}min" : null, + interval.Seconds > 0 || interval.TotalSeconds < 60 ? $"{interval.Seconds}sec" : null + }; + + return string.Join(" ", parts.Where(part => part != null)); + } + + return value; } public object ConvertBack(object value, Type targetType, object parameter, string language) From ac12962c85e39c62d5bbf73d50b913aeba09d05d Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Mon, 8 Jul 2024 18:44:58 -0700 Subject: [PATCH 04/18] Reshuffle some things in terms of layout --- CURRENTRELEASE.md | 1 + .../Controls/MatchesGridControl.xaml | 451 +++--- src/OpenSpartan.Workshop/Views/HomeView.xaml | 1212 +++++++++-------- 3 files changed, 846 insertions(+), 818 deletions(-) diff --git a/CURRENTRELEASE.md b/CURRENTRELEASE.md index f4b8034..1597969 100644 --- a/CURRENTRELEASE.md +++ b/CURRENTRELEASE.md @@ -3,6 +3,7 @@ - Improved image fallback for The Exchange, so that missing items now render properly. - Season calendar now includes background images for each event, operation, and battle pass season. - [#39] Removes the odd cross-out line in the calendar view. +- [#41] Fixes average life positioning, ensuring that it can't cause overflow. - Calendar colors are now easier to read. - Fixes how ranked match percentage is calculated, now showing proper values for next level. diff --git a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml index 6152ec2..8e766bd 100644 --- a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml +++ b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml @@ -13,9 +13,9 @@ x:Name="MatchesGridControlEntity" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> - + - + - - + + + + + + + + + @@ -146,7 +153,8 @@ - + + @@ -219,245 +227,253 @@ - - - - - - - - - - - - - - - 20 - 0 - - - - - - 20 - 0 - - - - - - / - - + + + + + + + + + + + + + + + + + 20 + 0 + + + + + + 20 + 0 + + + + + + / + + + + - - - - + + + + + - - - - - - + - - - - + + - - + + - - - - - - + + + - - - - + + + + + + + + + - - - - - + + - - + + + - + + - - + + - - - - - - - - - - - + + + + + + + + + + + + - - - - - + + - - + + + - + + - - + + - - - - - - - - - - - + + + + + + + + + + + + - - - - - + + - - + + + - + + - - + + - - - - - - - - - - - + + + + + + + + + + + + - - - - - + + - - + + + - + + - - + + - - - - - - - - - - - + + + + + + + + + + + + - - - - - + + - - + + + - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - + + + + + + - - - - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + - + diff --git a/src/OpenSpartan.Workshop/Views/HomeView.xaml b/src/OpenSpartan.Workshop/Views/HomeView.xaml index 7070765..b010a38 100644 --- a/src/OpenSpartan.Workshop/Views/HomeView.xaml +++ b/src/OpenSpartan.Workshop/Views/HomeView.xaml @@ -12,8 +12,13 @@ Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> - - + + + + + + + @@ -40,640 +45,645 @@ - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + From a395f9359d860b195442948afaebf684002c4002 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Mon, 8 Jul 2024 20:10:47 -0700 Subject: [PATCH 05/18] Fix rank slider --- CURRENTRELEASE.md | 3 +++ .../Controls/MatchesGridControl.xaml | 14 ++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CURRENTRELEASE.md b/CURRENTRELEASE.md index 1597969..f3eb7d2 100644 --- a/CURRENTRELEASE.md +++ b/CURRENTRELEASE.md @@ -6,5 +6,8 @@ - [#41] Fixes average life positioning, ensuring that it can't cause overflow. - Calendar colors are now easier to read. - Fixes how ranked match percentage is calculated, now showing proper values for next level. +- Home page now can be scrolled on smaller screens. +- Inside match metadata, medals and ranked counterfactuals correctly flow when screen is resized. +- The app now correctly reacts at startup to an error with authentication token acquisition. A message is shown if that is not possible. Refer to [**getting started guide**](https://openspartan.com/docs/workshop/guides/get-started/) to start using OpenSpartan Workshop. diff --git a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml index 8e766bd..6d711a0 100644 --- a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml +++ b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml @@ -16,6 +16,8 @@ + + - - - + + + - + 20 0 - + 20 0 @@ -263,7 +265,7 @@ - + From 338c3893619d4162c44018894ecb8699c2283163 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Tue, 9 Jul 2024 12:19:33 -0700 Subject: [PATCH 06/18] Fix more layout issues --- .../Controls/MatchesGridControl.xaml | 2 +- .../Views/BattlePassView.xaml | 92 ++++++++++--------- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml index 6d711a0..b022e55 100644 --- a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml +++ b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml @@ -239,7 +239,7 @@ - + diff --git a/src/OpenSpartan.Workshop/Views/BattlePassView.xaml b/src/OpenSpartan.Workshop/Views/BattlePassView.xaml index ba842c6..cb5794c 100644 --- a/src/OpenSpartan.Workshop/Views/BattlePassView.xaml +++ b/src/OpenSpartan.Workshop/Views/BattlePassView.xaml @@ -11,60 +11,62 @@ DataContext="{x:Bind viewmodels:BattlePassViewModel.Instance}" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> - - - - - - - - - - - + + - - + + + + + + - - + + + + + - - - - - - + + - + + + + + + - - + - - - - - - - - + + - - + - - + + + + + + - - - - - - + + - - - + + + + + + + + + + + + + + From 16909d6662b0a0074415c4bf2a778f8cb67064d5 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Tue, 9 Jul 2024 12:26:29 -0700 Subject: [PATCH 07/18] Fixes spacing and version labels --- src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml | 4 ++-- src/OpenSpartan.Workshop/Core/Configuration.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml index b022e55..51b8f55 100644 --- a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml +++ b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml @@ -156,7 +156,7 @@ - + @@ -230,7 +230,7 @@ - + diff --git a/src/OpenSpartan.Workshop/Core/Configuration.cs b/src/OpenSpartan.Workshop/Core/Configuration.cs index dd88c31..2574d72 100644 --- a/src/OpenSpartan.Workshop/Core/Configuration.cs +++ b/src/OpenSpartan.Workshop/Core/Configuration.cs @@ -11,8 +11,8 @@ internal sealed class Configuration internal const string HaloWaypointCsrImageEndpoint = "https://www.halowaypoint.com/images/halo-infinite/csr/"; // Build-related metadata. - internal const string Version = "1.0.6"; - internal const string BuildId = "CRUCIBLE-06072024"; + internal const string Version = "1.0.7"; + internal const string BuildId = "CYLIX-06082924"; internal const string PackageName = "OpenSpartan.Workshop"; // Authentication and setting-related metadata. @@ -25,9 +25,9 @@ internal sealed class Configuration // API-related default metadata. internal const string DefaultRelease = "1.7"; internal const string DefaultAPIVersion = "1"; - internal const string DefaultHeaderImage = "progression/Switcher/Season_Switcher_S7_TENRAIIV.png"; + internal const string DefaultHeaderImage = "progression/Switcher/Season_Switcher_S7_AVL.png"; internal const string DefaultSandbox = "UNUSED"; - internal const string DefaultBuild = "257697.24.05.16.1801-0"; + internal const string DefaultBuild = "258337.24.06.12.2052-2"; // Rank markers used to download the rank images. internal static readonly string[] HaloInfiniteRanks = From 25d71552b8a9cf9db16115b93833d901cf8635ac Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Tue, 9 Jul 2024 12:40:46 -0700 Subject: [PATCH 08/18] Cleanup some data code --- src/OpenSpartan.Workshop/Data/DataHandler.cs | 31 ++++------- src/OpenSpartan.Workshop/Data/Extensions.cs | 51 +++++++++++++------ .../Data/MatchesSource.cs | 4 +- 3 files changed, 47 insertions(+), 39 deletions(-) diff --git a/src/OpenSpartan.Workshop/Data/DataHandler.cs b/src/OpenSpartan.Workshop/Data/DataHandler.cs index b571c63..6d3c9aa 100644 --- a/src/OpenSpartan.Workshop/Data/DataHandler.cs +++ b/src/OpenSpartan.Workshop/Data/DataHandler.cs @@ -40,21 +40,17 @@ internal static string SetWALJournalingMode() { using var connection = new SqliteConnection($"Data Source={DatabasePath}"); connection.Open(); - using var command = connection.CreateCommand(); + using var command = connection.CreateCommand(); command.CommandText = GetQuery("Bootstrap", "SetWALJournalingMode"); + using var reader = command.ExecuteReader(); - if (reader.HasRows) + if (reader.Read()) { - while (reader.Read()) - { - return reader.GetString(0).Trim(); - } - } - else - { - LogEngine.Log($"WAL journaling mode not set.", LogSeverity.Error); + return reader.GetString(0).Trim(); } + + LogEngine.Log($"WAL journaling mode not set.", LogSeverity.Error); } catch (Exception ex) { @@ -135,20 +131,13 @@ internal static bool InsertServiceRecordEntry(string serviceRecordJson) using var connection = new SqliteConnection($"Data Source={DatabasePath}"); connection.Open(); - var command = connection.CreateCommand(); - command.CommandText = GetQuery("Insert", "ServiceRecord"); ; + using var command = connection.CreateCommand(); + command.CommandText = GetQuery("Insert", "ServiceRecord"); command.Parameters.AddWithValue("$ResponseBody", serviceRecordJson); command.Parameters.AddWithValue("$SnapshotTimestamp", DateTimeOffset.Now.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture)); - using var reader = command.ExecuteReader(); - if (reader.RecordsAffected > 1) - { - return true; - } - else - { - return false; - } + int recordsAffected = command.ExecuteNonQuery(); + return recordsAffected > 1; } catch (Exception ex) { diff --git a/src/OpenSpartan.Workshop/Data/Extensions.cs b/src/OpenSpartan.Workshop/Data/Extensions.cs index 29101d1..a1b08fc 100644 --- a/src/OpenSpartan.Workshop/Data/Extensions.cs +++ b/src/OpenSpartan.Workshop/Data/Extensions.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; -using System.Linq; using System.Reflection; using System.Text; @@ -14,16 +13,20 @@ internal static class Extensions { public static bool IsTableAvailable(this SqliteConnection connection, string tableName) { - var verifyTableAvailabilityQuery = File.ReadAllText(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Queries", "VerifyTableAvailability.sql"), Encoding.UTF8); - var command = connection.CreateCommand(); - command.CommandText = verifyTableAvailabilityQuery; - command.Parameters.AddWithValue("$id", tableName); - - // Service record table - using var reader = command.ExecuteReader(); - if (reader.HasRows) + string assemblyLocation = Assembly.GetExecutingAssembly().Location; + if (!string.IsNullOrEmpty(assemblyLocation)) { - return true; + string queriesPath = Path.Combine(Path.GetDirectoryName(assemblyLocation)!, "Queries"); + string filePath = Path.Combine(queriesPath, "VerifyTableAvailability.sql"); + + string verifyTableAvailabilityQuery = File.ReadAllText(filePath, Encoding.UTF8); + + using var command = connection.CreateCommand(); + command.CommandText = verifyTableAvailabilityQuery; + command.Parameters.AddWithValue("$id", tableName); + + using var reader = command.ExecuteReader(); + return reader.HasRows; } else { @@ -35,20 +38,36 @@ public static bool BootstrapTable(this SqliteConnection connection, string table { try { - var tableBootstrapQuery = File.ReadAllText(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Queries", "Bootstrap", $"{tableName}.sql"), Encoding.UTF8); + string assemblyLocation = Assembly.GetExecutingAssembly().Location; + + if (!string.IsNullOrEmpty(assemblyLocation)) + { + string queriesPath = Path.Combine(Path.GetDirectoryName(assemblyLocation)!, "Queries", "Bootstrap"); + string filePath = Path.Combine(queriesPath, $"{tableName}.sql"); - var command = connection.CreateCommand(); - command.CommandText = tableBootstrapQuery; + string tableBootstrapQuery = File.ReadAllText(filePath, Encoding.UTF8); - command.ExecuteReader(); + using var command = connection.CreateCommand(); + command.CommandText = tableBootstrapQuery; + _ = command.ExecuteReader(); - return true; + return true; + } + } + catch (IOException ex) + { + LogEngine.Log($"File operation failed for table {tableName}. {ex.Message}", Models.LogSeverity.Error); + } + catch (SqliteException ex) + { + LogEngine.Log($"Database operation failed for table {tableName}. {ex.Message}", Models.LogSeverity.Error); } catch (Exception ex) { LogEngine.Log($"Could not bootstrap table {tableName}. {ex.Message}", Models.LogSeverity.Error); - return false; } + + return false; } public static void AddRange(this ObservableCollection collection, IEnumerable items) diff --git a/src/OpenSpartan.Workshop/Data/MatchesSource.cs b/src/OpenSpartan.Workshop/Data/MatchesSource.cs index 0f4b7da..ce0bf8d 100644 --- a/src/OpenSpartan.Workshop/Data/MatchesSource.cs +++ b/src/OpenSpartan.Workshop/Data/MatchesSource.cs @@ -24,14 +24,14 @@ Task> IIncrementalSource.GetPage { if (MatchesViewModel.Instance.MatchList != null && MatchesViewModel.Instance.MatchList.Count > 0) { - var date = MatchesViewModel.Instance.MatchList.Min(a => a.StartTime).ToString("o", CultureInfo.InvariantCulture); + var date = MatchesViewModel.Instance.MatchList.Min(a => a.EndTime).ToString("o", CultureInfo.InvariantCulture); var matches = Task.Run(() => (IEnumerable)DataHandler.GetMatches($"xuid({HomeViewModel.Instance.Xuid})", date, pageSize)); return matches; } else { - return null; + return Task.FromResult(Enumerable.Empty()); } } } From 6a40dfa63ec84f1e86d11057fc5b50842d7fd8af Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Tue, 9 Jul 2024 13:12:03 -0700 Subject: [PATCH 09/18] Cleanup, and now not having a clearance errors out Too much depends on having an active flight, so without it we can't move forward. --- .../Core/UserContextManager.cs | 184 ++++---- src/OpenSpartan.Workshop/Data/DataHandler.cs | 393 ++++++++---------- 2 files changed, 276 insertions(+), 301 deletions(-) diff --git a/src/OpenSpartan.Workshop/Core/UserContextManager.cs b/src/OpenSpartan.Workshop/Core/UserContextManager.cs index 62ae5b6..855c349 100644 --- a/src/OpenSpartan.Workshop/Core/UserContextManager.cs +++ b/src/OpenSpartan.Workshop/Core/UserContextManager.cs @@ -187,79 +187,73 @@ public static async Task> SafeAP } } - internal static bool InitializeHaloClient(AuthenticationResult authResult) + internal static async Task InitializeHaloClient(AuthenticationResult authResult) { - HaloAuthenticationClient haloAuthClient = new(); - XboxAuthenticationClient manager = new(); + try + { + HaloAuthenticationClient haloAuthClient = new(); + XboxAuthenticationClient manager = new(); - var ticket = new XboxTicket(); - var haloTicket = new XboxTicket(); - var extendedTicket = new XboxTicket(); - var haloToken = new SpartanToken(); + var ticket = await manager.RequestUserToken(authResult.AccessToken) ?? await manager.RequestUserToken(authResult.AccessToken); - Task.Run(async () => - { - ticket = await manager.RequestUserToken(authResult.AccessToken); - ticket ??= await manager.RequestUserToken(authResult.AccessToken); - }).GetAwaiter().GetResult(); + if (ticket == null) + { + LogEngine.Log("Failed to obtain Xbox user token.", LogSeverity.Error); + return false; + } - Task.Run(async () => - { - haloTicket = await manager.RequestXstsToken(ticket.Token); - }).GetAwaiter().GetResult(); + var haloTicketTask = manager.RequestXstsToken(ticket.Token); + var extendedTicketTask = manager.RequestXstsToken(ticket.Token, false); - Task.Run(async () => - { - extendedTicket = await manager.RequestXstsToken(ticket.Token, false); - }).GetAwaiter().GetResult(); + var haloTicket = await haloTicketTask; + var extendedTicket = await extendedTicketTask; - if (haloTicket != null) - { - Task.Run(async () => + if (haloTicket == null) { - haloToken = await haloAuthClient.GetSpartanToken(haloTicket.Token, 4); - }).GetAwaiter().GetResult(); + LogEngine.Log("Failed to obtain Halo XSTS token.", LogSeverity.Error); + return false; + } + + var haloToken = await haloAuthClient.GetSpartanToken(haloTicket.Token, 4); if (extendedTicket != null) { XboxUserContext = extendedTicket; - HaloClient = new(haloToken.Token, extendedTicket.DisplayClaims.Xui[0].XUID, userAgent: $"{Configuration.PackageName}/{Configuration.Version}-{Configuration.BuildId}"); - - Task.Run(async () => - { - PlayerClearance? clearance = null; + HaloClient = new HaloInfiniteClient(haloToken.Token, extendedTicket.DisplayClaims.Xui[0].XUID, userAgent: $"{Configuration.PackageName}/{Configuration.Version}-{Configuration.BuildId}"); - if ((bool)SettingsViewModel.Instance.Settings.UseObanClearance) - { - clearance = (await SafeAPICall(async () => { return await HaloClient.SettingsActiveFlight(SettingsViewModel.Instance.Settings.Sandbox, SettingsViewModel.Instance.Settings.Build, SettingsViewModel.Instance.Settings.Release); })).Result; - } - else - { - clearance = (await SafeAPICall(async () => { return await HaloClient.SettingsActiveClearance(SettingsViewModel.Instance.Settings.Release); })).Result; - } + PlayerClearance? clearance = null; - if (clearance != null && !string.IsNullOrWhiteSpace(clearance.FlightConfigurationId)) - { - HaloClient.ClearanceToken = clearance.FlightConfigurationId; - LogEngine.Log($"Your clearance is {clearance.FlightConfigurationId} and it's set in the client."); - } - else - { - LogEngine.Log("Could not obtain the clearance.", LogSeverity.Error); - } - }).GetAwaiter().GetResult(); + if (SettingsViewModel.Instance.Settings.UseObanClearance) + { + clearance = (await SafeAPICall(async () => await HaloClient.SettingsActiveFlight(SettingsViewModel.Instance.Settings.Sandbox, SettingsViewModel.Instance.Settings.Build, SettingsViewModel.Instance.Settings.Release)))?.Result; + } + else + { + clearance = (await SafeAPICall(async () => await HaloClient.SettingsActiveClearance(SettingsViewModel.Instance.Settings.Release)))?.Result; + } - return true; + if (clearance != null && !string.IsNullOrWhiteSpace(clearance.FlightConfigurationId)) + { + HaloClient.ClearanceToken = clearance.FlightConfigurationId; + LogEngine.Log($"Your clearance is {clearance.FlightConfigurationId} and it's set in the client."); + return true; + } + else + { + LogEngine.Log("Could not obtain the clearance.", LogSeverity.Error); + return false; + } } else { + LogEngine.Log("Extended ticket is null. Cannot authenticate.", LogSeverity.Error); return false; } } - else + catch (Exception ex) { - LogEngine.Log("Halo ticket is null. Cannot authenticate.", LogSeverity.Error); + LogEngine.Log($"Error initializing Halo client: {ex.Message}", LogSeverity.Error); return false; } } @@ -914,7 +908,7 @@ internal static async Task ReAcquireTokens() var authResult = await InitializePublicClientApplication(); if (authResult != null) { - var result = InitializeHaloClient(authResult); + var result = await InitializeHaloClient(authResult); return result; } @@ -995,14 +989,17 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => // returned, we can abort. var operations = await GetOperations(); - if (settings.ExcludedOperations != null) + if (operations != null) { - foreach (var excludedOperation in settings.ExcludedOperations.ToList()) + if (settings.ExcludedOperations != null) { - var operationToRemove = operations.OperationRewardTracks.FirstOrDefault(x => x.RewardTrackPath == excludedOperation); - if (operationToRemove != null) + foreach (var excludedOperation in settings.ExcludedOperations.ToList()) { - operations.OperationRewardTracks.Remove(operationToRemove); + var operationToRemove = operations.OperationRewardTracks.FirstOrDefault(x => x.RewardTrackPath == excludedOperation); + if (operationToRemove != null) + { + operations.OperationRewardTracks.Remove(operationToRemove); + } } } } @@ -1061,53 +1058,56 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => await ProcessRegularSeasonRanges(rewardTrack.Value.DateRange.Value, rewardTrack.Value.Name.Value, i, targetBackgroundPath); } - // Then, we process operations - foreach (var operation in operations.OperationRewardTracks) + if (operations != null) { - var compoundOperation = new OperationCompoundModel { RewardTrack = operation, SeasonRewardTrack = seasonRewardTracks.GetValueOrDefault(operation.RewardTrackPath) }; + // Then, we process operations + foreach (var operation in operations.OperationRewardTracks) + { + var compoundOperation = new OperationCompoundModel { RewardTrack = operation, SeasonRewardTrack = seasonRewardTracks.GetValueOrDefault(operation.RewardTrackPath) }; - var isRewardTrackAvailable = DataHandler.IsOperationRewardTrackAvailable(operation.RewardTrackPath); + var isRewardTrackAvailable = DataHandler.IsOperationRewardTrackAvailable(operation.RewardTrackPath); - if (isRewardTrackAvailable) - { - var operationDetails = DataHandler.GetOperationResponseBody(operation.RewardTrackPath); - if (operationDetails != null) - compoundOperation.RewardTrackMetadata = operationDetails; + if (isRewardTrackAvailable) + { + var operationDetails = DataHandler.GetOperationResponseBody(operation.RewardTrackPath); + if (operationDetails != null) + compoundOperation.RewardTrackMetadata = operationDetails; - LogEngine.Log($"{operation.RewardTrackPath} (Local) - calendar prep completed"); - } - else - { - var apiResult = await SafeAPICall(async () => await HaloClient.GameCmsGetEvent(operation.RewardTrackPath, HaloClient.ClearanceToken)); - if (apiResult?.Result != null) - DataHandler.UpdateOperationRewardTracks(apiResult.Response.Message, operation.RewardTrackPath); + LogEngine.Log($"{operation.RewardTrackPath} (Local) - calendar prep completed"); + } + else + { + var apiResult = await SafeAPICall(async () => await HaloClient.GameCmsGetEvent(operation.RewardTrackPath, HaloClient.ClearanceToken)); + if (apiResult?.Result != null) + DataHandler.UpdateOperationRewardTracks(apiResult.Response.Message, operation.RewardTrackPath); - compoundOperation.RewardTrackMetadata = apiResult.Result; + compoundOperation.RewardTrackMetadata = apiResult.Result; - LogEngine.Log($"{operation.RewardTrackPath} - calendar prep completed"); - } + LogEngine.Log($"{operation.RewardTrackPath} - calendar prep completed"); + } - // If there is a background image, let's make sure that we attempt to download it. - // The same image may be downloaded when the Operations view is populated, but we - // don't know if that happened yet or not. + // If there is a background image, let's make sure that we attempt to download it. + // The same image may be downloaded when the Operations view is populated, but we + // don't know if that happened yet or not. - string? targetBackgroundPath = compoundOperation.RewardTrackMetadata?.SummaryImagePath ?? - compoundOperation.RewardTrackMetadata?.BackgroundImagePath ?? - compoundOperation.SeasonRewardTrack?.Logo; + string? targetBackgroundPath = compoundOperation.RewardTrackMetadata?.SummaryImagePath ?? + compoundOperation.RewardTrackMetadata?.BackgroundImagePath ?? + compoundOperation.SeasonRewardTrack?.Logo; - if (!string.IsNullOrEmpty(targetBackgroundPath)) - { - if (Path.IsPathRooted(targetBackgroundPath)) + if (!string.IsNullOrEmpty(targetBackgroundPath)) { - targetBackgroundPath = targetBackgroundPath.TrimStart(Path.DirectorySeparatorChar); - targetBackgroundPath = targetBackgroundPath.TrimStart(Path.AltDirectorySeparatorChar); + if (Path.IsPathRooted(targetBackgroundPath)) + { + targetBackgroundPath = targetBackgroundPath.TrimStart(Path.DirectorySeparatorChar); + targetBackgroundPath = targetBackgroundPath.TrimStart(Path.AltDirectorySeparatorChar); + } + + string qualifiedBackgroundImagePath = Path.Combine(Configuration.AppDataDirectory, "imagecache", targetBackgroundPath); + await DownloadAndSetImage(targetBackgroundPath, qualifiedBackgroundImagePath); } - string qualifiedBackgroundImagePath = Path.Combine(Configuration.AppDataDirectory, "imagecache", targetBackgroundPath); - await DownloadAndSetImage(targetBackgroundPath, qualifiedBackgroundImagePath); + await ProcessRegularSeasonRanges(compoundOperation.RewardTrackMetadata.DateRange.Value, compoundOperation.RewardTrackMetadata.Name.Value, operations.OperationRewardTracks.IndexOf(operation), targetBackgroundPath); } - - await ProcessRegularSeasonRanges(compoundOperation.RewardTrackMetadata.DateRange.Value, compoundOperation.RewardTrackMetadata.Name.Value, operations.OperationRewardTracks.IndexOf(operation), targetBackgroundPath); } // And now we check the event data. @@ -1986,7 +1986,7 @@ internal static async Task InitializeAllDataOnLaunch() var authResult = await InitializePublicClientApplication(); if (authResult != null) { - var instantiationResult = InitializeHaloClient(authResult); + var instantiationResult = await InitializeHaloClient(authResult); if (instantiationResult) { diff --git a/src/OpenSpartan.Workshop/Data/DataHandler.cs b/src/OpenSpartan.Workshop/Data/DataHandler.cs index 6d3c9aa..39899ca 100644 --- a/src/OpenSpartan.Workshop/Data/DataHandler.cs +++ b/src/OpenSpartan.Workshop/Data/DataHandler.cs @@ -153,22 +153,15 @@ internal static bool InsertPlaylistCSRSnapshot(string playlistId, string playlis using var connection = new SqliteConnection($"Data Source={DatabasePath}"); connection.Open(); - var command = connection.CreateCommand(); - command.CommandText = GetQuery("Insert", "PlaylistCSR"); ; + using var command = connection.CreateCommand(); + command.CommandText = GetQuery("Insert", "PlaylistCSR"); command.Parameters.AddWithValue("$ResponseBody", playlistCsrJson); command.Parameters.AddWithValue("$PlaylistId", playlistId); command.Parameters.AddWithValue("$PlaylistVersion", playlistVersion); command.Parameters.AddWithValue("$SnapshotTimestamp", DateTimeOffset.Now.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture)); - using var reader = command.ExecuteReader(); - if (reader.RecordsAffected > 1) - { - return true; - } - else - { - return false; - } + int recordsAffected = command.ExecuteNonQuery(); + return recordsAffected > 1; } catch (Exception ex) { @@ -188,56 +181,45 @@ internal static List GetMatchIds() command.CommandText = GetQuery("Select", "DistinctMatchIds"); using var reader = command.ExecuteReader(); - if (reader.HasRows) + List matchIds = []; + while (reader.Read()) { - List matchIds = new List(); - while (reader.Read()) - { - matchIds.Add(Guid.Parse(reader.GetString(0).Trim())); - } - - return matchIds; + matchIds.Add(reader.GetGuid(0)); } - else + + if (matchIds.Count == 0) { - LogEngine.Log($"No rows returned for distinct match IDs.", LogSeverity.Warning); + LogEngine.Log("No rows returned for distinct match IDs.", LogSeverity.Warning); } + + return matchIds; } catch (Exception ex) { LogEngine.Log($"An error occurred obtaining unique match IDs. {ex.Message}", LogSeverity.Error); + return []; } - - return null; } internal static RewardTrackMetadata GetOperationResponseBody(string operationPath) { try { - using SqliteConnection connection = new($"Data Source={DatabasePath}"); + using var connection = new SqliteConnection($"Data Source={DatabasePath}"); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = GetQuery("Select", "OperationResponseBody"); command.Parameters.AddWithValue("$OperationPath", operationPath); - using SqliteDataReader reader = command.ExecuteReader(); - if (reader.HasRows) - { - RewardTrackMetadata response = new(); - - while (reader.Read()) - { - response = JsonSerializer.Deserialize(reader.GetString(0).Trim(), serializerOptions); - } - - return response; - } - else + using var reader = command.ExecuteReader(); + if (reader.Read()) { - LogEngine.Log($"No rows returned for operations.", LogSeverity.Warning); + string jsonString = reader.GetString(0).Trim(); + return JsonSerializer.Deserialize(jsonString, serializerOptions); } + + LogEngine.Log("No rows returned for operations.", LogSeverity.Warning); } catch (Exception ex) { @@ -251,25 +233,25 @@ internal static int GetExistingMatchCount(IEnumerable matchIds) { try { - using SqliteConnection connection = new($"Data Source={DatabasePath}"); + using var connection = new SqliteConnection($"Data Source={DatabasePath}"); connection.Open(); - // In this context, we want the command to be literal rather than parameterized. + // Build the command text with the matchIds directly embedded (literal query) + string matchIdsString = string.Join(", ", matchIds.Select(g => $"'{g}'")); + string query = GetQuery("Select", "ExistingMatchCount").Replace("$MatchGUIDList", matchIdsString, StringComparison.InvariantCultureIgnoreCase); + using var command = connection.CreateCommand(); - command.CommandText = GetQuery("Select", "ExistingMatchCount").Replace("$MatchGUIDList", string.Join(", ", matchIds.Select(g => $"'{g}'")), StringComparison.InvariantCultureIgnoreCase); + command.CommandText = query; - using SqliteDataReader reader = command.ExecuteReader(); - if (reader.HasRows) + using var reader = command.ExecuteReader(); + if (reader.Read()) { - while (reader.Read()) - { - var resultOrdinal = reader.GetOrdinal("ExistingMatchCount"); - return reader.IsDBNull(resultOrdinal) ? -1 : reader.GetFieldValue(resultOrdinal); - } + var resultOrdinal = reader.GetOrdinal("ExistingMatchCount"); + return reader.IsDBNull(resultOrdinal) ? -1 : reader.GetFieldValue(resultOrdinal); } else { - LogEngine.Log($"No rows returned for existing match metadata.", LogSeverity.Warning); + LogEngine.Log("No rows returned for existing match metadata.", LogSeverity.Warning); } } catch (Exception ex) @@ -301,46 +283,43 @@ private static List GetMatchesInternal(string playerXuid, long if (medalNameId.HasValue) { command.CommandText = GetQuery("Select", "PlayerMatchesBasedOnMedal"); - command.Parameters.AddWithValue("MedalNameId", medalNameId.Value); + command.Parameters.AddWithValue("$MedalNameId", medalNameId.Value); } else { command.CommandText = GetQuery("Select", "PlayerMatches"); } - command.Parameters.AddWithValue("PlayerXuid", playerXuid); - command.Parameters.AddWithValue("BoundaryTime", boundaryTime); - command.Parameters.AddWithValue("BoundaryLimit", boundaryLimit); + command.Parameters.AddWithValue("$PlayerXuid", playerXuid); + command.Parameters.AddWithValue("$BoundaryTime", boundaryTime); + command.Parameters.AddWithValue("$BoundaryLimit", boundaryLimit); using var reader = command.ExecuteReader(); - if (reader.HasRows) + List matches = []; + while (reader.Read()) { - List matches = []; - while (reader.Read()) - { - var matchEntry = ReadMatchTableEntity(reader); - - if (matchEntry.PlayerTeamStats[0].Stats.CoreStats.Medals != null && matchEntry.PlayerTeamStats[0].Stats.CoreStats.Medals.Count > 0) - { - matchEntry.PlayerTeamStats[0].Stats.CoreStats.Medals = UserContextManager.EnrichMedalMetadata(matchEntry.PlayerTeamStats[0].Stats.CoreStats.Medals); - } + var matchEntry = ReadMatchTableEntity(reader); - matches.Add(matchEntry); + if (matchEntry.PlayerTeamStats[0].Stats.CoreStats.Medals != null && matchEntry.PlayerTeamStats[0].Stats.CoreStats.Medals.Count > 0) + { + matchEntry.PlayerTeamStats[0].Stats.CoreStats.Medals = UserContextManager.EnrichMedalMetadata(matchEntry.PlayerTeamStats[0].Stats.CoreStats.Medals); } - return matches; + matches.Add(matchEntry); } - else + + if (matches.Count == 0) { - LogEngine.Log($"No rows returned for player match IDs.", LogSeverity.Warning); + LogEngine.Log("No rows returned for player match IDs.", LogSeverity.Warning); } + + return matches; } catch (Exception ex) { LogEngine.Log($"An error occurred obtaining matches. {ex.Message}", LogSeverity.Error); + return new List(); } - - return null; } private static MatchTableEntity ReadMatchTableEntity(SqliteDataReader reader) @@ -437,16 +416,14 @@ internal static (bool MatchAvailable, bool StatsAvailable) GetMatchStatsAvailabi using var connection = new SqliteConnection($"Data Source={DatabasePath}"); connection.Open(); - using (SqliteCommand command = connection.CreateCommand()) - { - command.CommandText = GetQuery("Select", "MatchStatsAvailability"); - command.Parameters.AddWithValue("$MatchId", matchId); + using var command = connection.CreateCommand(); + command.CommandText = GetQuery("Select", "MatchStatsAvailability"); + command.Parameters.AddWithValue("$MatchId", matchId); - using var reader = command.ExecuteReader(); - if (reader.HasRows && reader.Read()) - { - return (Convert.ToBoolean(reader.GetFieldValue(0)), Convert.ToBoolean(reader.GetFieldValue(1))); - } + using var reader = command.ExecuteReader(); + if (reader.Read()) + { + return (reader.GetBoolean(0), reader.GetBoolean(1)); } } catch (Exception ex) @@ -464,50 +441,42 @@ internal static bool InsertPlayerMatchStats(string matchId, string statsBody) using var connection = new SqliteConnection($"Data Source={DatabasePath}"); connection.Open(); - using var insertionCommand = connection.CreateCommand(); - insertionCommand.CommandText = GetQuery("Insert", "PlayerMatchStats"); - insertionCommand.Parameters.AddWithValue("$MatchId", matchId); - insertionCommand.Parameters.AddWithValue("$ResponseBody", statsBody); + using var command = connection.CreateCommand(); + command.CommandText = GetQuery("Insert", "PlayerMatchStats"); + command.Parameters.AddWithValue("$MatchId", matchId); + command.Parameters.AddWithValue("$ResponseBody", statsBody); - var insertionResult = insertionCommand.ExecuteNonQuery(); + var rowsAffected = command.ExecuteNonQuery(); - if (insertionResult > 0) - { - return true; - } + return rowsAffected > 0; } catch (Exception ex) { LogEngine.Log($"An error occurred inserting player match and stats. {ex.Message}", LogSeverity.Error); + return false; } - - return false; } - internal static bool InsertMatchStats (string matchBody) + internal static bool InsertMatchStats(string matchBody) { try { using var connection = new SqliteConnection($"Data Source={DatabasePath}"); connection.Open(); - using var insertionCommand = connection.CreateCommand(); - insertionCommand.CommandText = GetQuery("Insert", "MatchStats"); - insertionCommand.Parameters.AddWithValue("$ResponseBody", matchBody); + using var command = connection.CreateCommand(); + command.CommandText = GetQuery("Insert", "MatchStats"); + command.Parameters.AddWithValue("$ResponseBody", matchBody); - var insertionResult = insertionCommand.ExecuteNonQuery(); + var rowsAffected = command.ExecuteNonQuery(); - if (insertionResult > 0) - { - return true; - } + return rowsAffected > 0; } catch (Exception ex) { LogEngine.Log($"An error occurred inserting match and stats. {ex.Message}", LogSeverity.Error); + return false; } - - return false; } internal static async Task UpdateMatchAssetRecords(MatchStats result) @@ -517,29 +486,32 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result) bool mapAvailable = false; bool gameVariantAvailable = false; bool engineGameVariantAvailable = false; - bool playlistAvailable = true; bool playlistMapModePairAvailable = true; - UGCGameVariant targetGameVariant = null; using var connection = new SqliteConnection($"Data Source={DatabasePath}"); await connection.OpenAsync(); - - string query = "SELECT EXISTS(SELECT 1 FROM Maps WHERE AssetId = $MapAssetId AND VersionId = $MapVersionId) AS MAP_AVAILABLE, " + - "EXISTS(SELECT 1 FROM GameVariants WHERE AssetId = $GameVariantAssetId AND VersionId = $GameVariantVersionId) AS GAMEVARIANT_AVAILABLE"; + // Construct the initial query + var queryBuilder = new StringBuilder(); + queryBuilder.Append("SELECT "); + queryBuilder.Append("EXISTS(SELECT 1 FROM Maps WHERE AssetId = $MapAssetId AND VersionId = $MapVersionId) AS MAP_AVAILABLE, "); + queryBuilder.Append("EXISTS(SELECT 1 FROM GameVariants WHERE AssetId = $GameVariantAssetId AND VersionId = $GameVariantVersionId) AS GAMEVARIANT_AVAILABLE"); + + // Conditionally add more parts to the query based on available parameters if (result.MatchInfo.Playlist != null) { - query += ", EXISTS(SELECT 1 FROM Playlists WHERE AssetId = $PlaylistAssetId AND VersionId = $PlaylistVersionId) AS PLAYLIST_AVAILABLE"; + queryBuilder.Append(", EXISTS(SELECT 1 FROM Playlists WHERE AssetId = $PlaylistAssetId AND VersionId = $PlaylistVersionId) AS PLAYLIST_AVAILABLE"); } if (result.MatchInfo.PlaylistMapModePair != null) { - query += ", EXISTS(SELECT 1 FROM PlaylistMapModePairs WHERE AssetId = $PlaylistMapModePairAssetId AND VersionId = $PlaylistMapModePairVersionId) AS PLAYLISTMAPMODEPAIR_AVAILABLE"; + queryBuilder.Append(", EXISTS(SELECT 1 FROM PlaylistMapModePairs WHERE AssetId = $PlaylistMapModePairAssetId AND VersionId = $PlaylistMapModePairVersionId) AS PLAYLISTMAPMODEPAIR_AVAILABLE"); } - using (SqliteCommand command = new(query, connection)) + // Execute the constructed query + using (var command = new SqliteCommand(queryBuilder.ToString(), connection)) { command.Parameters.AddWithValue("$MapAssetId", result.MatchInfo.MapVariant.AssetId.ToString()); command.Parameters.AddWithValue("$MapVersionId", result.MatchInfo.MapVariant.VersionId.ToString()); @@ -568,6 +540,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result) } } + // Update assets if they are not available if (!mapAvailable) { var map = await UserContextManager.SafeAPICall(async () => await UserContextManager.HaloClient.HIUGCDiscoveryGetMap(result.MatchInfo.MapVariant.AssetId.ToString(), result.MatchInfo.MapVariant.VersionId.ToString())); @@ -635,17 +608,15 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result) { targetGameVariant = gameVariant.Result; - using (var insertionCommand = connection.CreateCommand()) - { - insertionCommand.CommandText = GetQuery("Insert", "GameVariants"); - insertionCommand.Parameters.AddWithValue("$ResponseBody", gameVariant.Response.Message); + using var insertionCommand = connection.CreateCommand(); + insertionCommand.CommandText = GetQuery("Insert", "GameVariants"); + insertionCommand.Parameters.AddWithValue("$ResponseBody", gameVariant.Response.Message); - var insertionResult = await insertionCommand.ExecuteNonQueryAsync(); + var insertionResult = await insertionCommand.ExecuteNonQueryAsync(); - if (insertionResult > 0) - { - LogEngine.Log($"Stored game variant: {result.MatchInfo.UgcGameVariant.AssetId}/{result.MatchInfo.UgcGameVariant.VersionId}"); - } + if (insertionResult > 0) + { + LogEngine.Log($"Stored game variant: {result.MatchInfo.UgcGameVariant.AssetId}/{result.MatchInfo.UgcGameVariant.VersionId}"); } using var egvQueryCommand = connection.CreateCommand(); @@ -682,7 +653,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result) } catch (Exception ex) { - LogEngine.Log($"Error updating match stats. {ex.Message}", LogSeverity.Error); + LogEngine.Log($"Error updating match asset records. {ex.Message}", LogSeverity.Error); return false; } } @@ -703,134 +674,142 @@ internal static List GetMedals() using var command = connection.CreateCommand(); command.CommandText = GetQuery("Select", "LatestMedalsSnapshot"); + List medals = []; + using var reader = command.ExecuteReader(); - if (reader.HasRows) + while (reader.Read()) { - List matchIds = []; - while (reader.Read()) - { - matchIds.AddRange(JsonSerializer.Deserialize>(reader.GetString(0))); - } - - return matchIds; + medals.AddRange(JsonSerializer.Deserialize>(reader.GetString(0))); } - else + + if (medals.Count == 0) { LogEngine.Log($"No rows returned for medals.", LogSeverity.Warning); } + + return medals; } catch (Exception ex) { LogEngine.Log($"An error occurred obtaining medals from the database. {ex.Message}", LogSeverity.Error); + return null; } - - return null; } internal static bool UpdateOperationRewardTracks(string response, string path) { - using var connection = new SqliteConnection($"Data Source={DatabasePath}"); - using var insertionCommand = connection.CreateCommand(); - - insertionCommand.CommandText = GetQuery("Insert", "OperationRewardTracks"); - insertionCommand.Parameters.AddWithValue("$ResponseBody", response); - insertionCommand.Parameters.AddWithValue("$Path", path); - insertionCommand.Parameters.AddWithValue("$LastUpdated", DateTimeOffset.Now.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture)); + try + { + using var connection = new SqliteConnection($"Data Source={DatabasePath}"); + connection.Open(); - connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = GetQuery("Insert", "OperationRewardTracks"); + command.Parameters.AddWithValue("$ResponseBody", response); + command.Parameters.AddWithValue("$Path", path); + command.Parameters.AddWithValue("$LastUpdated", DateTimeOffset.Now.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture)); - var insertionResult = insertionCommand.ExecuteNonQuery(); + var insertionResult = command.ExecuteNonQuery(); - if (insertionResult > 0) - { - LogEngine.Log($"Stored reward track {path}."); - return true; + if (insertionResult > 0) + { + LogEngine.Log($"Stored reward track {path}."); + return true; + } + else + { + LogEngine.Log($"Could not store reward track {path}.", LogSeverity.Error); + } } - else + catch (Exception ex) { - return false; + LogEngine.Log($"An error occurred updating operation reward tracks. {ex.Message}", LogSeverity.Error); } + + return false; } internal static bool UpdateInventoryItems(string response, string path) { - using var connection = new SqliteConnection($"Data Source={DatabasePath}"); - using var insertionCommand = connection.CreateCommand(); - - insertionCommand.CommandText = GetQuery("Insert", "InventoryItems"); - insertionCommand.Parameters.AddWithValue("$ResponseBody", response); - insertionCommand.Parameters.AddWithValue("$Path", path); - insertionCommand.Parameters.AddWithValue("$LastUpdated", DateTimeOffset.Now.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture)); + try + { + using var connection = new SqliteConnection($"Data Source={DatabasePath}"); + connection.Open(); - connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = GetQuery("Insert", "InventoryItems"); + command.Parameters.AddWithValue("$ResponseBody", response); + command.Parameters.AddWithValue("$Path", path); + command.Parameters.AddWithValue("$LastUpdated", DateTimeOffset.Now.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture)); - var insertionResult = insertionCommand.ExecuteNonQuery(); + var insertionResult = command.ExecuteNonQuery(); - if (insertionResult > 0) - { - LogEngine.Log($"Stored inventory item {path}."); - return true; + if (insertionResult > 0) + { + LogEngine.Log($"Stored inventory item {path}."); + return true; + } + else + { + LogEngine.Log($"Could not store inventory item {path}.", LogSeverity.Error); + } } - else + catch (Exception ex) { - return false; + LogEngine.Log($"An error occurred updating inventory items. {ex.Message}", LogSeverity.Error); } + + return false; } internal static bool IsOperationRewardTrackAvailable(string path) { - using var connection = new SqliteConnection($"Data Source={DatabasePath}"); - using var command = connection.CreateCommand(); + try + { + using var connection = new SqliteConnection($"Data Source={DatabasePath}"); + connection.Open(); - command.CommandText = $"SELECT EXISTS(SELECT 1 FROM OperationRewardTracks WHERE Path='{path}') AS OPERATION_AVAILABLE"; + using var command = connection.CreateCommand(); + command.CommandText = "SELECT EXISTS(SELECT 1 FROM OperationRewardTracks WHERE Path = @Path) AS OPERATION_AVAILABLE"; + command.Parameters.AddWithValue("@Path", path); - connection.Open(); + var result = command.ExecuteScalar(); - using var reader = command.ExecuteReader(); - if (reader.HasRows) - { - while (reader.Read()) + if (result != null && result != DBNull.Value) { - var operationAvailable = reader.GetFieldValue(0); - if (operationAvailable > 0) - { - return true; - } - else - { - return false; - } + return Convert.ToInt32(result) > 0; } } + catch (Exception ex) + { + LogEngine.Log($"An error occurred checking operation reward track availability. {ex.Message}", LogSeverity.Error); + } return false; } internal static bool IsInventoryItemAvailable(string path) { - using var connection = new SqliteConnection($"Data Source={DatabasePath}"); - using var command = connection.CreateCommand(); + try + { + using var connection = new SqliteConnection($"Data Source={DatabasePath}"); + connection.Open(); - command.CommandText = $"SELECT EXISTS(SELECT 1 FROM InventoryItems WHERE Path='{path}') AS INVENTORY_ITEM_AVAILABLE"; + using var command = connection.CreateCommand(); + command.CommandText = "SELECT EXISTS(SELECT 1 FROM InventoryItems WHERE Path = @Path) AS INVENTORY_ITEM_AVAILABLE"; + command.Parameters.AddWithValue("@Path", path); - connection.Open(); + var result = command.ExecuteScalar(); - using var reader = command.ExecuteReader(); - if (reader.HasRows) - { - while (reader.Read()) + if (result != null && result != DBNull.Value) { - var operationAvailable = reader.GetFieldValue(0); - if (operationAvailable > 0) - { - return true; - } - else - { - return false; - } + return Convert.ToInt32(result) > 0; } } + catch (Exception ex) + { + LogEngine.Log($"An error occurred checking inventory item availability. {ex.Message}", LogSeverity.Error); + } return false; } @@ -847,12 +826,9 @@ internal static InGameItem GetInventoryItem(string path) command.Parameters.AddWithValue("$Path", path); using var reader = command.ExecuteReader(); - if (reader.HasRows) + if (reader.Read()) { - while (reader.Read()) - { - return JsonSerializer.Deserialize(reader.GetString(0), serializerOptions); - } + return JsonSerializer.Deserialize(reader.GetString(0), serializerOptions); } else { @@ -874,21 +850,21 @@ internal static async Task InsertOwnedInventoryItems(PlayerInventory resul using var connection = new SqliteConnection($"Data Source={DatabasePath}"); await connection.OpenAsync(); - var command = GetQuery("Insert", "OwnedInventoryItems"); + var commandText = GetQuery("Insert", "OwnedInventoryItems"); foreach (var item in result.Items) { - using var insertionCommand = connection.CreateCommand(); - insertionCommand.CommandText = command; - insertionCommand.Parameters.AddWithValue("$Amount", item.Amount); - insertionCommand.Parameters.AddWithValue("$ItemId", item.ItemId); - insertionCommand.Parameters.AddWithValue("$ItemPath", item.ItemPath); - insertionCommand.Parameters.AddWithValue("$ItemType", item.ItemType); - insertionCommand.Parameters.AddWithValue("$FirstAcquiredDate", item.FirstAcquiredDate.ISO8601Date); + using var command = connection.CreateCommand(); + command.CommandText = commandText; + command.Parameters.AddWithValue("$Amount", item.Amount); + command.Parameters.AddWithValue("$ItemId", item.ItemId); + command.Parameters.AddWithValue("$ItemPath", item.ItemPath); + command.Parameters.AddWithValue("$ItemType", item.ItemType); + command.Parameters.AddWithValue("$FirstAcquiredDate", item.FirstAcquiredDate.ISO8601Date); - var insertionResult = await insertionCommand.ExecuteNonQueryAsync(); + var rowsAffected = await command.ExecuteNonQueryAsync(); - if (insertionResult > 0) + if (rowsAffected > 0) { LogEngine.Log($"Stored owned inventory item {item.ItemId}."); } @@ -906,6 +882,5 @@ internal static async Task InsertOwnedInventoryItems(PlayerInventory resul return false; } } - } } From d0f0368b13ec7ea3b670d52f2166cb8ba1a3aee8 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Tue, 9 Jul 2024 16:26:42 -0700 Subject: [PATCH 10/18] More code cleanup --- .../Controls/SeasonCalendarControl.xaml | 2 +- .../Core/UserContextManager.cs | 878 ++++++++---------- 2 files changed, 411 insertions(+), 469 deletions(-) diff --git a/src/OpenSpartan.Workshop/Controls/SeasonCalendarControl.xaml b/src/OpenSpartan.Workshop/Controls/SeasonCalendarControl.xaml index 78606f7..3a090f9 100644 --- a/src/OpenSpartan.Workshop/Controls/SeasonCalendarControl.xaml +++ b/src/OpenSpartan.Workshop/Controls/SeasonCalendarControl.xaml @@ -40,7 +40,7 @@ - + diff --git a/src/OpenSpartan.Workshop/Core/UserContextManager.cs b/src/OpenSpartan.Workshop/Core/UserContextManager.cs index 855c349..9bd10f0 100644 --- a/src/OpenSpartan.Workshop/Core/UserContextManager.cs +++ b/src/OpenSpartan.Workshop/Core/UserContextManager.cs @@ -17,7 +17,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -25,6 +24,7 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -70,20 +70,24 @@ internal static nint GetMainWindowHandle() { try { - LogEngine.Log($"Attempting to populate medata metadata..."); + LogEngine.Log($"Attempting to populate medal metadata..."); - var metadata = await SafeAPICall(async () => await HaloClient.GameCmsGetMedalMetadata()); - if (metadata != null && metadata.Result != null) + var metadata = await SafeAPICall(() => HaloClient.GameCmsGetMedalMetadata()); + + if (metadata?.Result != null) { + LogEngine.Log($"Medal metadata populated successfully."); return metadata.Result; } - - LogEngine.Log($"Medal metadata populated."); - return null; + else + { + LogEngine.Log($"Failed to populate medal metadata: No valid result received.", LogSeverity.Error); + return null; + } } catch (Exception ex) { - LogEngine.Log($"Could not populate medal metadata. {ex.Message}", LogSeverity.Error); + LogEngine.Log($"Could not populate medal metadata: {ex.Message}", LogSeverity.Error); return null; } } @@ -158,24 +162,21 @@ public static async Task GetWorkshopSettings() public static async Task> SafeAPICall(Func>> orionAPICall) { - HaloApiResultContainer result = null; - try { - result = await orionAPICall(); + HaloApiResultContainer result = await orionAPICall(); - if (result.Response.Code == 401) + if (result != null && result.Response != null && result.Response.Code == 401) { - var tokenResult = await ReAcquireTokens(); - - if (!tokenResult) + if (await ReAcquireTokens()) + { + result = await orionAPICall(); + } + else { LogEngine.Log("Could not reacquire tokens.", LogSeverity.Error); - return default; } - - return await orionAPICall(); } return result; @@ -183,7 +184,7 @@ public static async Task> SafeAP catch (Exception ex) { LogEngine.Log($"Failed to make Halo Infinite API call. {ex.Message}", LogSeverity.Error); - return result; + return default; } } @@ -263,20 +264,26 @@ internal static async Task PopulateCareerData() try { var xuid = XboxUserContext.DisplayClaims.Xui[0].XUID; - var economyTask = SafeAPICall(() => HaloClient.EconomyGetPlayerCareerRank(new List { $"xuid({xuid})" }, "careerRank1")); - var ranksTask = SafeAPICall(() => HaloClient.GameCmsGetCareerRanks("careerRank1")); - await Task.WhenAll(economyTask, ranksTask); + // Fetch career data asynchronously + var careerRankTask = SafeAPICall(() => HaloClient.EconomyGetPlayerCareerRank([$"xuid({xuid})"], "careerRank1")); + var rankCollectionTask = SafeAPICall(() => HaloClient.GameCmsGetCareerRanks("careerRank1")); + + // Await both tasks concurrently + await Task.WhenAll(careerRankTask, rankCollectionTask); - var careerTrackResult = economyTask.GetResultOrDefault() as HaloApiResultContainer; - var careerTrackContainerResult = ranksTask.GetResultOrDefault() as HaloApiResultContainer; + // Extract results from tasks + var careerTrackResult = careerRankTask.Result; + var careerTrackContainerResult = rankCollectionTask.Result; - if (careerTrackResult?.Result != null && careerTrackResult.Response.Code == 200) + // Process career track result + if (careerTrackResult != null && careerTrackResult.Response.Code == 200) { await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => HomeViewModel.Instance.CareerSnapshot = careerTrackResult.Result); } - if (careerTrackContainerResult?.Result != null && (careerTrackContainerResult.Response.Code == 200 || careerTrackContainerResult.Response.Code == 304)) + // Process career track container result + if (careerTrackContainerResult != null && (careerTrackContainerResult.Response.Code == 200 || careerTrackContainerResult.Response.Code == 304)) { await DispatcherWindow.DispatcherQueue.EnqueueAsync(async () => { @@ -298,8 +305,7 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(async () => var relevantRanks = careerTrackContainerResult.Result.Ranks.TakeWhile(c => c.Rank < currentRank); HomeViewModel.Instance.ExperienceEarnedToDate = relevantRanks.Sum(rank => rank.XpRequiredForRank) + careerTrackResult.Result.RewardTracks[0].Result.CurrentProgress.PartialProgress; - // Currently a bug in the Halo Infinite CMS where the Onyx Cadet 3 large icon is set incorrectly. - // Hopefully at some point this will be fixed. + // Handle known bug in the Halo Infinite CMS for rank images if (currentCareerStage.RankLargeIcon == "career_rank/CelebrationMoment/219_Cadet_Onyx_III.png") { currentCareerStage.RankLargeIcon = "career_rank/CelebrationMoment/19_Cadet_Onyx_III.png"; @@ -331,7 +337,6 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(async () => } } - internal static void EnsureDirectoryExists(string path) { var file = new FileInfo(path); @@ -340,28 +345,40 @@ internal static void EnsureDirectoryExists(string path) private static async Task DownloadAndSetImage(string serviceImagePath, string localImagePath, Action setImageAction = null, bool isOnWaypoint = false) { - if (!System.IO.File.Exists(localImagePath)) + try { - HaloApiResultContainer image = null; - - if (isOnWaypoint) + // Check if local image file exists + if (System.IO.File.Exists(localImagePath)) { - image = await SafeAPICall(async () => await HaloClient.GameCmsGetGenericWaypointFile(serviceImagePath)); - } - else - { - image = await SafeAPICall(async () => await HaloClient.GameCmsGetImage(serviceImagePath)); + if (setImageAction != null) + { + await DispatcherWindow.DispatcherQueue.EnqueueAsync(setImageAction); + } + return; } + HaloApiResultContainer image = null; + + Func>> apiCall = isOnWaypoint ? + async () => await HaloClient.GameCmsGetGenericWaypointFile(serviceImagePath) : + async () => await HaloClient.GameCmsGetImage(serviceImagePath); + + image = await SafeAPICall(apiCall); + + // Check if the image retrieval was successful if (image != null && image.Result != null && image.Response.Code == 200) { await System.IO.File.WriteAllBytesAsync(localImagePath, image.Result); + + if (setImageAction != null) + { + await DispatcherWindow.DispatcherQueue.EnqueueAsync(setImageAction); + } } } - - if (setImageAction != null) + catch (Exception ex) { - await DispatcherWindow.DispatcherQueue.EnqueueAsync(setImageAction); + LogEngine.Log($"Failed to download and set image '{serviceImagePath}' to '{localImagePath}'. Error: {ex.Message}", LogSeverity.Error); } } @@ -373,102 +390,87 @@ internal static async Task PopulateServiceRecordData() try { // Get initial service record details - var serviceRecordResult = await SafeAPICall(async () => - { - return await HaloClient.StatsGetPlayerServiceRecord($"xuid({XboxUserContext.DisplayClaims.Xui[0].XUID})", LifecycleMode.Matchmade); - }); + var serviceRecordResult = await SafeAPICall(() => + HaloClient.StatsGetPlayerServiceRecord($"xuid({XboxUserContext.DisplayClaims.Xui[0].XUID})", LifecycleMode.Matchmade)); - if (serviceRecordResult != null && serviceRecordResult.Result != null && serviceRecordResult.Response.Code == 200) + if (serviceRecordResult != null && serviceRecordResult.Response.Code == 200) { - // First, we want to insert the service record entry in the database. + // Update UI with service record details await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { HomeViewModel.Instance.ServiceRecord = serviceRecordResult.Result; + RankedViewModel.Instance.RankedLoadingState = MetadataLoadingState.Loading; + RankedViewModel.Instance.Playlists.Clear(); }); + // Insert service record entry into the database DataHandler.InsertServiceRecordEntry(serviceRecordResult.Response.Message); - await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => - { - RankedViewModel.Instance.RankedLoadingState = MetadataLoadingState.Loading; - RankedViewModel.Instance.Playlists = []; - }); - - // For ranked progression, we can run that on the thread pool to make sure it doesn't block - // all other calls. + // Process ranked playlists asynchronously on the thread pool _ = Task.Run(async () => { await ProcessRankedPlaylists(serviceRecordResult.Result, ServiceRecordCancellationTracker.Token); }, ServiceRecordCancellationTracker.Token); - } - return true; + return true; + } + else + { + LogEngine.Log($"Failed to retrieve service record for xuid({XboxUserContext.DisplayClaims.Xui[0].XUID}). Response code: {serviceRecordResult?.Response.Code}", LogSeverity.Error); + return false; + } } - catch + catch (Exception ex) { + LogEngine.Log($"An error occurred while populating service record data: {ex.Message}", LogSeverity.Error); return false; } } private static async Task ProcessRankedPlaylists(PlayerServiceRecord serviceRecord, CancellationToken token) { - // Next, we want to also capture the ranked progression. To do that, we will iterate through each - // playlist the player has ever played in, according to the service record. - foreach (var playlist in serviceRecord.Subqueries.PlaylistAssetIds) + try { - token.ThrowIfCancellationRequested(); - - // We look inside each playlist configuration to see if the playlist has CSR associated - // with it, since we only care about CSR-enabled playlists to get ranked progression. - var playlistConfigurationResult = await SafeAPICall(async () => + foreach (var playlist in serviceRecord.Subqueries.PlaylistAssetIds) { - return await HaloClient.GameCmsGetMultiplayerPlaylistConfiguration($"{playlist.ToString()}.json"); - }); + token.ThrowIfCancellationRequested(); - if (playlistConfigurationResult != null && playlistConfigurationResult.Result != null) - { - // Let's check if the playlist has CSR - if (playlistConfigurationResult.Result.HasCsr == true) + var playlistConfigurationResult = await SafeAPICall(() => + HaloClient.GameCmsGetMultiplayerPlaylistConfiguration($"{playlist}.json")); + + if (playlistConfigurationResult?.Result?.HasCsr == true) { - var playlistCsr = await SafeAPICall(async () => - { - return await HaloClient.SkillGetPlaylistCsr(playlist.ToString(), new List { $"xuid({XboxUserContext.DisplayClaims.Xui[0].XUID})" }); - }); + var playlistCsr = await SafeAPICall(() => + HaloClient.SkillGetPlaylistCsr(playlist.ToString(), new List { $"xuid({XboxUserContext.DisplayClaims.Xui[0].XUID})" })); - // If we successfully got a playlist CSR, let's record that data locally in the database - // and also get additional playlist data. We also check that the Value array has more than - // zero elements, because if there is nothing that means that there is no CSR snapshot - // to capture and store. - if (playlistCsr != null && playlistCsr.Result != null && playlistCsr.Response != null && playlistCsr.Result.Value.Count > 0) + if (playlistCsr?.Result?.Value?.Count > 0) { DataHandler.InsertPlaylistCSRSnapshot(playlist.ToString(), playlistConfigurationResult.Result.UgcPlaylistVersion.ToString(), playlistCsr.Response.Message); - var playlistMetadata = await SafeAPICall(async () => - { - return await HaloClient.HIUGCDiscoveryGetPlaylist(playlist.ToString(), playlistConfigurationResult.Result.UgcPlaylistVersion.ToString(), HaloClient.ClearanceToken); - }); + var playlistMetadata = await SafeAPICall(() => + HaloClient.HIUGCDiscoveryGetPlaylist(playlist.ToString(), playlistConfigurationResult.Result.UgcPlaylistVersion.ToString(), HaloClient.ClearanceToken)); - // Now, let's get the data into the local viewmodel if the metadata acquisition was successful. - if (playlistMetadata != null && playlistMetadata.Result != null) + if (playlistMetadata?.Result != null && !token.IsCancellationRequested) { - if (!token.IsCancellationRequested) + await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { - await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => + RankedViewModel.Instance.Playlists.Add(new PlaylistCSRSnapshot { - RankedViewModel.Instance.Playlists.Add(new PlaylistCSRSnapshot() - { - Name = playlistMetadata.Result.PublicName, - Id = playlist, - Version = playlistConfigurationResult.Result.UgcPlaylistVersion, - Snapshot = playlistCsr.Result.Value[0], // We only got data for one player. - }); + Name = playlistMetadata.Result.PublicName, + Id = playlist, + Version = playlistConfigurationResult.Result.UgcPlaylistVersion, + Snapshot = playlistCsr.Result.Value[0] // Assuming we only get data for one player }); - } + }); } } } } } + catch (Exception ex) + { + LogEngine.Log($"Error processing ranked playlists: {ex.Message}", LogSeverity.Error); + } await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { @@ -968,16 +970,10 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => // First, we get the raw season calendar to get the list of all available events that // were registered in the Halo Infinite API. var seasonCalendar = await GetSeasonCalendar(); - if (seasonCalendar != null) + settings.ExtraRitualEvents?.ForEach(extraRitualEvent => { - if (settings.ExtraRitualEvents != null) - { - foreach (var extraRitualEvent in settings.ExtraRitualEvents) - { - seasonCalendar.Events.Add(new SeasonCalendarEntry { RewardTrackPath = extraRitualEvent }); - } - } - } + seasonCalendar?.Events.Add(new SeasonCalendarEntry { RewardTrackPath = extraRitualEvent }); + }); // Once the season calendar is obtained, we want to capture the metadata for every // single season entry. Events can be populated directly as part of the battle pass query @@ -989,48 +985,45 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => // returned, we can abort. var operations = await GetOperations(); - if (operations != null) + if (operations == null) return false; + + if (settings.ExcludedOperations != null) { - if (settings.ExcludedOperations != null) - { - foreach (var excludedOperation in settings.ExcludedOperations.ToList()) - { - var operationToRemove = operations.OperationRewardTracks.FirstOrDefault(x => x.RewardTrackPath == excludedOperation); - if (operationToRemove != null) - { - operations.OperationRewardTracks.Remove(operationToRemove); - } - } - } + operations.OperationRewardTracks.RemoveAll(operation => + settings.ExcludedOperations.Contains(operation.RewardTrackPath)); } - if (csrCalendar != null && csrCalendar.Result != null) + if (csrCalendar == null || csrCalendar.Result == null) { - for (int i = 0; i < csrCalendar.Result.Seasons.Count; i++) + await HandleCalendarLoadingStateCompleted(); + return false; + } + + foreach (var season in csrCalendar.Result.Seasons) + { + var days = GenerateDateList(season.StartDate.ISO8601Date, season.EndDate.ISO8601Date); + foreach (var day in days) { - var days = GenerateDateList(csrCalendar.Result.Seasons[i].StartDate.ISO8601Date, csrCalendar.Result.Seasons[i].EndDate.ISO8601Date); - foreach (var day in days) + await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { - await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => + SeasonCalendarViewDayItem calendarItem = new() { - SeasonCalendarViewDayItem calendarItem = new(); - calendarItem.DateTime = day; - calendarItem.CSRSeasonText = csrCalendar.Result.Seasons[i].CsrSeasonFilePath.Replace(".json", string.Empty); - calendarItem.CSRSeasonMarkerColor = ColorConverter.FromHex(Configuration.SeasonColors[i]); + DateTime = day, + CSRSeasonText = season.CsrSeasonFilePath.Replace(".json", string.Empty), + CSRSeasonMarkerColor = ColorConverter.FromHex(Configuration.SeasonColors[csrCalendar.Result.Seasons.IndexOf(season)]) + }; - SeasonCalendarViewModel.Instance.SeasonDays.Add(calendarItem); - }); - } + SeasonCalendarViewModel.Instance.SeasonDays.Add(calendarItem); + }); } } - else + + async Task HandleCalendarLoadingStateCompleted() { await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { SeasonCalendarViewModel.Instance.CalendarLoadingState = MetadataLoadingState.Completed; }); - - return false; } // Complete the parsing of individual seasons @@ -1060,7 +1053,6 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => if (operations != null) { - // Then, we process operations foreach (var operation in operations.OperationRewardTracks) { var compoundOperation = new OperationCompoundModel { RewardTrack = operation, SeasonRewardTrack = seasonRewardTracks.GetValueOrDefault(operation.RewardTrackPath) }; @@ -1069,10 +1061,7 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => if (isRewardTrackAvailable) { - var operationDetails = DataHandler.GetOperationResponseBody(operation.RewardTrackPath); - if (operationDetails != null) - compoundOperation.RewardTrackMetadata = operationDetails; - + compoundOperation.RewardTrackMetadata = DataHandler.GetOperationResponseBody(operation.RewardTrackPath); LogEngine.Log($"{operation.RewardTrackPath} (Local) - calendar prep completed"); } else @@ -1082,17 +1071,12 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => DataHandler.UpdateOperationRewardTracks(apiResult.Response.Message, operation.RewardTrackPath); compoundOperation.RewardTrackMetadata = apiResult.Result; - LogEngine.Log($"{operation.RewardTrackPath} - calendar prep completed"); } - // If there is a background image, let's make sure that we attempt to download it. - // The same image may be downloaded when the Operations view is populated, but we - // don't know if that happened yet or not. - string? targetBackgroundPath = compoundOperation.RewardTrackMetadata?.SummaryImagePath ?? - compoundOperation.RewardTrackMetadata?.BackgroundImagePath ?? - compoundOperation.SeasonRewardTrack?.Logo; + compoundOperation.RewardTrackMetadata?.BackgroundImagePath ?? + compoundOperation.SeasonRewardTrack?.Logo; if (!string.IsNullOrEmpty(targetBackgroundPath)) { @@ -1106,61 +1090,70 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => await DownloadAndSetImage(targetBackgroundPath, qualifiedBackgroundImagePath); } - await ProcessRegularSeasonRanges(compoundOperation.RewardTrackMetadata.DateRange.Value, compoundOperation.RewardTrackMetadata.Name.Value, operations.OperationRewardTracks.IndexOf(operation), targetBackgroundPath); + await ProcessRegularSeasonRanges(compoundOperation.RewardTrackMetadata.DateRange.Value, + compoundOperation.RewardTrackMetadata.Name.Value, + operations.OperationRewardTracks.IndexOf(operation), + targetBackgroundPath); } } - // And now we check the event data. - var distinctEvents = seasonCalendar.Events.DistinctBy(x => x.RewardTrackPath).ToList(); - foreach (var eventEntry in distinctEvents) + // Extract distinct events based on RewardTrackPath + var distinctEvents = seasonCalendar.Events + .Select(x => x.RewardTrackPath) + .Distinct() + .ToList(); + + foreach (var rewardTrackPath in distinctEvents) { var compoundEvent = new OperationCompoundModel { - RewardTrack = new RewardTrack { RewardTrackPath = eventEntry.RewardTrackPath } + RewardTrack = new RewardTrack { RewardTrackPath = rewardTrackPath } }; - var isRewardTrackAvailable = DataHandler.IsOperationRewardTrackAvailable(eventEntry.RewardTrackPath); + var isRewardTrackAvailable = DataHandler.IsOperationRewardTrackAvailable(rewardTrackPath); if (isRewardTrackAvailable) { - compoundEvent.RewardTrackMetadata = DataHandler.GetOperationResponseBody(eventEntry.RewardTrackPath); - + compoundEvent.RewardTrackMetadata = DataHandler.GetOperationResponseBody(rewardTrackPath); var rewardTrack = await GetRewardTrackMetadata("event", compoundEvent.RewardTrackMetadata.TrackId); - LogEngine.Log($"{eventEntry.RewardTrackPath} (Local) - calendar prep completed"); + LogEngine.Log($"{rewardTrackPath} (Local) - calendar prep completed"); } else { - var apiResult = await SafeAPICall(async () => await HaloClient.GameCmsGetEvent(eventEntry.RewardTrackPath, HaloClient.ClearanceToken)); + var apiResult = await SafeAPICall(async () => await HaloClient.GameCmsGetEvent(rewardTrackPath, HaloClient.ClearanceToken)); + if (apiResult?.Result != null) { - DataHandler.UpdateOperationRewardTracks(apiResult.Response.Message, eventEntry.RewardTrackPath); + DataHandler.UpdateOperationRewardTracks(apiResult.Response.Message, rewardTrackPath); } - compoundEvent.RewardTrackMetadata = apiResult.Result; - LogEngine.Log($"{eventEntry.RewardTrackPath} - calendar prep completed"); + compoundEvent.RewardTrackMetadata = apiResult?.Result; + + LogEngine.Log($"{rewardTrackPath} - calendar prep completed"); } // If there is a background image, let's make sure that we attempt to download it. - // The same image may be downloaded when the Operations view is populated, but we - // don't know if that happened yet or not. string? targetBackgroundPath = compoundEvent?.RewardTrackMetadata?.SummaryImagePath ?? - compoundEvent?.RewardTrackMetadata?.BackgroundImagePath ?? - compoundEvent?.SeasonRewardTrack?.Logo; + compoundEvent?.RewardTrackMetadata?.BackgroundImagePath ?? + compoundEvent?.SeasonRewardTrack?.Logo; if (!string.IsNullOrEmpty(targetBackgroundPath)) { - if (Path.IsPathRooted(targetBackgroundPath)) - { - targetBackgroundPath = targetBackgroundPath.TrimStart(Path.DirectorySeparatorChar); - targetBackgroundPath = targetBackgroundPath.TrimStart(Path.AltDirectorySeparatorChar); - } + // Normalize the path by trimming separators + targetBackgroundPath = targetBackgroundPath.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + // Construct the qualified path for image caching string qualifiedBackgroundImagePath = Path.Combine(Configuration.AppDataDirectory, "imagecache", targetBackgroundPath); + await DownloadAndSetImage(targetBackgroundPath, qualifiedBackgroundImagePath); } - await ProcessRegularSeasonRanges(compoundEvent.RewardTrackMetadata.DateRange.Value, compoundEvent.RewardTrackMetadata.Name.Value, distinctEvents.IndexOf(eventEntry), targetBackgroundPath); + // Process regular season ranges + await ProcessRegularSeasonRanges(compoundEvent.RewardTrackMetadata.DateRange.Value, + compoundEvent.RewardTrackMetadata.Name.Value, + distinctEvents.IndexOf(rewardTrackPath), + targetBackgroundPath); } await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => @@ -1292,77 +1285,115 @@ internal static async Task PopulateMedalData() { try { + // Log start of operation LogEngine.Log("Getting medal metadata..."); + // Retrieve medal metadata asynchronously + MedalMetadata = await PrepopulateMedalMetadata(); + + // Check if metadata or medals list is null or empty if (MedalMetadata == null || MedalMetadata.Medals == null || MedalMetadata.Medals.Count == 0) + { + LogEngine.Log("Medal metadata or medals list is empty.", LogSeverity.Warning); return false; + } - // This gets the medals that are locally stored. + // Retrieve locally stored medals var medals = DataHandler.GetMedals(); if (medals == null) + { + LogEngine.Log("Locally stored medals not found.", LogSeverity.Warning); return false; + } - var compoundMedals = medals.Join(MedalMetadata.Medals, earned => earned.NameId, references => references.NameId, (earned, references) => new Medal() - { - Count = earned.Count, - Description = references.Description, - DifficultyIndex = references.DifficultyIndex, - Name = references.Name, - NameId = references.NameId, - PersonalScore = references.PersonalScore, - SortingWeight = references.SortingWeight, - SpriteIndex = references.SpriteIndex, - TotalPersonalScoreAwarded = earned.TotalPersonalScoreAwarded, - TypeIndex = references.TypeIndex, - }).ToList(); + // Join locally stored medals with metadata to create compound medals + var compoundMedals = medals.Join( + MedalMetadata.Medals, + earned => earned.NameId, + references => references.NameId, + (earned, references) => new Medal + { + Count = earned.Count, + Description = references.Description, + DifficultyIndex = references.DifficultyIndex, + Name = references.Name, + NameId = references.NameId, + PersonalScore = references.PersonalScore, + SortingWeight = references.SortingWeight, + SpriteIndex = references.SpriteIndex, + TotalPersonalScoreAwarded = earned.TotalPersonalScoreAwarded, + TypeIndex = references.TypeIndex, + }) + .ToList(); - var group = compoundMedals.OrderByDescending(x => x.Count).GroupBy(x => x.TypeIndex); + // Group compound medals by TypeIndex and order by Count + var groupedMedals = compoundMedals + .OrderByDescending(x => x.Count) + .GroupBy(x => x.TypeIndex) + .ToList(); + // Update MedalsViewModel on UI thread await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { - MedalsViewModel.Instance.Medals = new System.Collections.ObjectModel.ObservableCollection>(group); + MedalsViewModel.Instance.Medals = new ObservableCollection>(groupedMedals); }); + // Ensure directory for medal images exists string qualifiedMedalPath = Path.Combine(Configuration.AppDataDirectory, "imagecache", "medals"); - var spriteRequestResult = await SafeAPICall(async () => await HaloClient.GameCmsGetGenericWaypointFile(MedalMetadata.Sprites.ExtraLarge.Path)); + // Retrieve sprite content for medals + var spriteRequestResult = await SafeAPICall(async () => + await HaloClient.GameCmsGetGenericWaypointFile(MedalMetadata.Sprites.ExtraLarge.Path)); var spriteContent = spriteRequestResult?.Result; if (spriteContent != null) { - using MemoryStream ms = new(spriteContent); - SkiaSharp.SKBitmap bmp = SkiaSharp.SKBitmap.Decode(ms); - using var pixmap = bmp.PeekPixels(); - - // We want to download all medals that are available - // in the stack. That way, we don't have to fiddle with - // individual missing medals later on. - foreach (var medal in MedalMetadata.Medals) + // Decode sprite content into SKBitmap + using (MemoryStream ms = new MemoryStream(spriteContent)) { - string medalImagePath = Path.Combine(qualifiedMedalPath, $"{medal.NameId}.png"); - EnsureDirectoryExists(medalImagePath); - - if (!System.IO.File.Exists(medalImagePath)) + SkiaSharp.SKBitmap bmp = SkiaSharp.SKBitmap.Decode(ms); + using (var pixmap = bmp.PeekPixels()) { - var row = (int)Math.Floor(medal.SpriteIndex / 16.0); - var column = (int)(medal.SpriteIndex % 16.0); - SkiaSharp.SKRectI rectI = SkiaSharp.SKRectI.Create(column * 256, row * 256, 256, 256); - - var subset = pixmap.ExtractSubset(rectI); - using var data = subset.Encode(SkiaSharp.SKPngEncoderOptions.Default); - await System.IO.File.WriteAllBytesAsync(medalImagePath, data.ToArray()); - LogEngine.Log($"Wrote medal to file: {medalImagePath}"); + // Download and save medal images + foreach (var medal in MedalMetadata.Medals) + { + string medalImagePath = Path.Combine(qualifiedMedalPath, $"{medal.NameId}.png"); + EnsureDirectoryExists(medalImagePath); + + // Skip writing if file already exists + if (!System.IO.File.Exists(medalImagePath)) + { + // Calculate position and size of medal sprite + var row = (int)Math.Floor(medal.SpriteIndex / 16.0); + var column = (int)(medal.SpriteIndex % 16.0); + SkiaSharp.SKRectI rectI = SkiaSharp.SKRectI.Create(column * 256, row * 256, 256, 256); + + // Extract subset of pixmap and encode as PNG + var subset = pixmap.ExtractSubset(rectI); + using (var data = subset.Encode(SkiaSharp.SKPngEncoderOptions.Default)) + { + await System.IO.File.WriteAllBytesAsync(medalImagePath, data.ToArray()); + } + + // Log successful write + LogEngine.Log($"Wrote medal to file: {medalImagePath}"); + } + } } } + // Log completion of medal retrieval LogEngine.Log("Got medals."); } } catch (Exception ex) { + // Log error if any exception occurs LogEngine.Log($"Could not obtain medal metadata. Error: {ex.Message}", LogSeverity.Error); return false; } + + // Return true indicating successful operation return true; } @@ -1408,16 +1439,10 @@ public static async Task PopulateBattlePassData(CancellationToken cancella // First, we get the raw season calendar to get the list of all available events that // were registered in the Halo Infinite API. var seasonCalendar = await GetSeasonCalendar(); - if (seasonCalendar != null) + settings.ExtraRitualEvents?.ForEach(extraRitualEvent => { - if (settings.ExtraRitualEvents != null) - { - foreach (var extraRitualEvent in settings.ExtraRitualEvents) - { - seasonCalendar.Events.Add(new SeasonCalendarEntry { RewardTrackPath = extraRitualEvent }); - } - } - } + seasonCalendar?.Events.Add(new SeasonCalendarEntry { RewardTrackPath = extraRitualEvent }); + }); // Once the season calendar is obtained, we want to capture the metadata for every // single season entry. Events can be populated directly as part of the battle pass query @@ -1432,52 +1457,17 @@ public static async Task PopulateBattlePassData(CancellationToken cancella if (settings.ExcludedOperations != null) { - foreach (var excludedOperation in settings.ExcludedOperations.ToList()) - { - var operationToRemove = operations.OperationRewardTracks.FirstOrDefault(x => x.RewardTrackPath == excludedOperation); - if (operationToRemove != null) - { - operations.OperationRewardTracks.Remove(operationToRemove); - } - } + operations.OperationRewardTracks.RemoveAll(operation => + settings.ExcludedOperations.Contains(operation.RewardTrackPath)); } // Let's get the data for each of the operations. foreach (var operation in operations.OperationRewardTracks) { - // Tell the user that the operations are currently being loaded by changing the - // loading parameter to the reward track path. cancellationToken.ThrowIfCancellationRequested(); await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => BattlePassViewModel.Instance.BattlePassLoadingParameter = operation.RewardTrackPath); - // We can now also pull the metadata from the previously declared - // calendar container. - var compoundOperation = new OperationCompoundModel { RewardTrack = operation, SeasonRewardTrack = seasonRewardTracks.GetValueOrDefault(operation.RewardTrackPath) }; - - var isRewardTrackAvailable = DataHandler.IsOperationRewardTrackAvailable(operation.RewardTrackPath); - - if (isRewardTrackAvailable) - { - var operationDetails = DataHandler.GetOperationResponseBody(operation.RewardTrackPath); - if (operationDetails != null) - { - compoundOperation.RewardTrackMetadata = operationDetails; - } - compoundOperation.Rewards = new(await GetFlattenedRewards(operationDetails.Ranks, operation.CurrentProgress.Rank)); - LogEngine.Log($"{operation.RewardTrackPath} (Local) - Completed"); - } - else - { - var apiResult = await SafeAPICall(async () => await HaloClient.GameCmsGetEvent(operation.RewardTrackPath, HaloClient.ClearanceToken)); - if (apiResult?.Result != null) - { - DataHandler.UpdateOperationRewardTracks(apiResult.Response.Message, operation.RewardTrackPath); - } - compoundOperation.RewardTrackMetadata = apiResult.Result; - compoundOperation.Rewards = new(await GetFlattenedRewards(apiResult.Result.Ranks, operation.CurrentProgress.Rank)); - LogEngine.Log($"{operation.RewardTrackPath} - Completed"); - } - + var compoundOperation = await ProcessOperation(operation, seasonRewardTracks); await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => BattlePassViewModel.Instance.BattlePasses.Add(compoundOperation)); } @@ -1507,12 +1497,11 @@ public static async Task PopulateBattlePassData(CancellationToken cancella compoundEvent.RewardTrackMetadata = eventDetails; } - // For events, there is no "Current Progress" indicator the same way we have it for operations, so - // we're using a dummy value of -1. - // We want to get the current progress for the evnet. var rewardTrack = await GetRewardTrackMetadata("event", compoundEvent.RewardTrackMetadata.TrackId); + // For events, there is no "Current Progress" indicator the same way we have it for operations, so + // we're using a dummy value of -1. compoundEvent.Rewards = new(await GetFlattenedRewards(eventDetails.Ranks, (rewardTrack != null ? rewardTrack.CurrentProgress.Rank : -1))); LogEngine.Log($"{eventEntry.RewardTrackPath} (Local) - Completed"); } @@ -1541,7 +1530,7 @@ public static async Task PopulateBattlePassData(CancellationToken cancella compoundEvent.RewardTrackMetadata.SummaryImagePath += ".png"; } - await UpdateLocalImage("imagecache", compoundEvent.RewardTrackMetadata.SummaryImagePath); + await DownloadAndSetImage(compoundEvent.RewardTrackMetadata.SummaryImagePath, Path.Combine(Configuration.AppDataDirectory, "imagecache", compoundEvent.RewardTrackMetadata.SummaryImagePath)).ConfigureAwait(false); } await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => BattlePassViewModel.Instance.Events.Add(compoundEvent)); @@ -1550,6 +1539,38 @@ public static async Task PopulateBattlePassData(CancellationToken cancella return true; } + private static async Task ProcessOperation(RewardTrack operation, Dictionary? seasonRewardTracks) + { + var compoundOperation = new OperationCompoundModel { RewardTrack = operation, SeasonRewardTrack = seasonRewardTracks?.GetValueOrDefault(operation.RewardTrackPath) }; + + var isRewardTrackAvailable = DataHandler.IsOperationRewardTrackAvailable(operation.RewardTrackPath); + + if (isRewardTrackAvailable) + { + var operationDetails = DataHandler.GetOperationResponseBody(operation.RewardTrackPath); + if (operationDetails != null) + { + compoundOperation.RewardTrackMetadata = operationDetails; + } + compoundOperation.Rewards = new (await GetFlattenedRewards(operationDetails?.Ranks, operation.CurrentProgress.Rank)); + LogEngine.Log($"{operation.RewardTrackPath} (Local) - Completed"); + } + else + { + var apiResult = await SafeAPICall(async () => + await HaloClient.GameCmsGetEvent(operation.RewardTrackPath, HaloClient.ClearanceToken)); + if (apiResult?.Result != null) + { + DataHandler.UpdateOperationRewardTracks(apiResult.Response.Message, operation.RewardTrackPath); + } + compoundOperation.RewardTrackMetadata = apiResult?.Result; + compoundOperation.Rewards =new(await GetFlattenedRewards(apiResult?.Result?.Ranks, operation.CurrentProgress.Rank)); + LogEngine.Log($"{operation.RewardTrackPath} - Completed"); + } + + return compoundOperation; + } + private static async Task?> GetSeasonRewardTrackMetadata(SeasonCalendar? seasonCalendar) { if (seasonCalendar == null || seasonCalendar.Seasons == null || seasonCalendar.Seasons.Count == 0) @@ -1557,32 +1578,38 @@ public static async Task PopulateBattlePassData(CancellationToken cancella var seasonRewardTracks = new Dictionary(); + var downloadTasks = new List(); + foreach (var season in seasonCalendar.Seasons) { if (string.IsNullOrWhiteSpace(season.SeasonMetadata) || string.IsNullOrWhiteSpace(season.OperationTrackPath)) continue; - var result = await SafeAPICall(async () => - await HaloClient.GameCmsGetSeasonRewardTrack(season.SeasonMetadata, HaloClient.ClearanceToken) - ); + var result = await SafeAPICall(() => HaloClient.GameCmsGetSeasonRewardTrack(season.SeasonMetadata, HaloClient.ClearanceToken)); if (result?.Result != null) { seasonRewardTracks.Add(season.OperationTrackPath, result.Result); - // If we have the metadata, let's also make sure that we download the relevant images. - await UpdateLocalImage("imagecache", result.Result.SummaryBackgroundPath); - await UpdateLocalImage("imagecache", result.Result.BattlePassSeasonUpsellBackgroundImage); - await UpdateLocalImage("imagecache", result.Result.ChallengesBackgroundPath); - await UpdateLocalImage("imagecache", result.Result.BattlePassLogoImage); - await UpdateLocalImage("imagecache", result.Result.SeasonLogoImage); - await UpdateLocalImage("imagecache", result.Result.RitualLogoImage); - await UpdateLocalImage("imagecache", result.Result.StorefrontBackgroundImage); - await UpdateLocalImage("imagecache", result.Result.CardBackgroundImage); - await UpdateLocalImage("imagecache", result.Result.ProgressionBackgroundImage); + // Queue up image download tasks + downloadTasks.Add(Task.Run(async () => + { + await DownloadAndSetImage(result.Result.SummaryBackgroundPath, Path.Combine(Configuration.AppDataDirectory, "imagecache", result.Result.SummaryBackgroundPath)).ConfigureAwait(false); + await DownloadAndSetImage(result.Result.BattlePassSeasonUpsellBackgroundImage, Path.Combine(Configuration.AppDataDirectory, "imagecache", result.Result.BattlePassSeasonUpsellBackgroundImage)).ConfigureAwait(false); + await DownloadAndSetImage(result.Result.ChallengesBackgroundPath, Path.Combine(Configuration.AppDataDirectory, "imagecache", result.Result.ChallengesBackgroundPath)).ConfigureAwait(false); + await DownloadAndSetImage(result.Result.BattlePassLogoImage, Path.Combine(Configuration.AppDataDirectory, "imagecache", result.Result.BattlePassLogoImage)).ConfigureAwait(false); + await DownloadAndSetImage(result.Result.SeasonLogoImage, Path.Combine(Configuration.AppDataDirectory, "imagecache", result.Result.SeasonLogoImage)).ConfigureAwait(false); + await DownloadAndSetImage(result.Result.RitualLogoImage, Path.Combine(Configuration.AppDataDirectory, "imagecache", result.Result.RitualLogoImage)).ConfigureAwait(false); + await DownloadAndSetImage(result.Result.StorefrontBackgroundImage, Path.Combine(Configuration.AppDataDirectory, "imagecache", result.Result.StorefrontBackgroundImage)).ConfigureAwait(false); + await DownloadAndSetImage(result.Result.CardBackgroundImage, Path.Combine(Configuration.AppDataDirectory, "imagecache", result.Result.CardBackgroundImage)).ConfigureAwait(false); + await DownloadAndSetImage(result.Result.ProgressionBackgroundImage, Path.Combine(Configuration.AppDataDirectory, "imagecache", result.Result.ProgressionBackgroundImage)).ConfigureAwait(false); + })); } } + // Wait for all image download tasks to complete + await Task.WhenAll(downloadTasks).ConfigureAwait(false); + return seasonRewardTracks.Count > 0 ? seasonRewardTracks : null; } @@ -1702,122 +1729,70 @@ private static async Task WriteImageToFileAsync(string path, byte[] imageData) internal static async Task> ExtractInventoryRewards(int rank, int playerRank, IEnumerable inventoryItems, bool isFree) { - List rewardContainers = new(inventoryItems.Count()); - SemaphoreSlim semaphore = new(Environment.ProcessorCount); + List> processingTasks = []; - async Task ProcessInventoryItem(InventoryAmount inventoryReward) + foreach (var inventoryReward in inventoryItems) { - await semaphore.WaitAsync().ConfigureAwait(false); - - try - { - bool inventoryItemLocallyAvailable = DataHandler.IsInventoryItemAvailable(inventoryReward.InventoryItemPath); + processingTasks.Add(ProcessInventoryItem(rank, playerRank, isFree, inventoryReward)); + } - var container = new ItemMetadataContainer - { - Ranks = Tuple.Create(rank, playerRank), - IsFree = isFree, - ItemValue = inventoryReward.Amount, - Type = ItemClass.StandardReward, - }; + ItemMetadataContainer[] containers = await Task.WhenAll(processingTasks).ConfigureAwait(false); - if (inventoryItemLocallyAvailable) - { - container.ItemDetails = DataHandler.GetInventoryItem(inventoryReward.InventoryItemPath); + return [.. containers]; + } - if (container.ItemDetails != null) - { - LogEngine.Log($"Trying to get local image for {container.ItemDetails.CommonData.Id} (entity: {inventoryReward.InventoryItemPath})"); + private static async Task ProcessInventoryItem(int rank, int playerRank, bool isFree, InventoryAmount inventoryReward) + { + try + { + bool inventoryItemLocallyAvailable = DataHandler.IsInventoryItemAvailable(inventoryReward.InventoryItemPath); - if (await UpdateLocalImage("imagecache", container.ItemDetails.CommonData.DisplayPath.Media.MediaUrl.Path).ConfigureAwait(false)) - { - LogEngine.Log($"Stored local image: {container.ItemDetails.CommonData.DisplayPath.Media.MediaUrl.Path}"); - } - else - { - LogEngine.Log(container.ItemDetails.CommonData.DisplayPath.Media.MediaUrl.Path, LogSeverity.Error); - } - } - else - { - LogEngine.Log("Inventory item is null.", LogSeverity.Error); - } - } - else - { - var item = await SafeAPICall(async () => await HaloClient.GameCmsGetItem(inventoryReward.InventoryItemPath, HaloClient.ClearanceToken).ConfigureAwait(false)).ConfigureAwait(false); + var container = new ItemMetadataContainer + { + Ranks = Tuple.Create(rank, playerRank), + IsFree = isFree, + ItemValue = inventoryReward.Amount, + Type = ItemClass.StandardReward, + }; - if (item != null && item.Result != null) - { - LogEngine.Log($"Trying to get local image for {item.Result.CommonData.Id} (entity: {inventoryReward.InventoryItemPath})"); + if (inventoryItemLocallyAvailable) + { + container.ItemDetails = DataHandler.GetInventoryItem(inventoryReward.InventoryItemPath); - if (await UpdateLocalImage("imagecache", item.Result.CommonData.DisplayPath.Media.MediaUrl.Path).ConfigureAwait(false)) - { - LogEngine.Log($"Stored local image: {item.Result.CommonData.DisplayPath.Media.MediaUrl.Path}"); - } - else - { - LogEngine.Log(item.Result.CommonData.DisplayPath.Media.MediaUrl.Path, LogSeverity.Error); - } + if (container.ItemDetails != null) + { + LogEngine.Log($"Trying to get local image for {container.ItemDetails.CommonData.Id} (entity: {inventoryReward.InventoryItemPath})"); - DataHandler.UpdateInventoryItems(item.Response.Message, inventoryReward.InventoryItemPath); - container.ItemDetails = item.Result; - } + await DownloadAndSetImage(container.ItemDetails.CommonData.DisplayPath.Media.MediaUrl.Path, Path.Combine(Configuration.AppDataDirectory, "imagecache", container.ItemDetails.CommonData.DisplayPath.Media.MediaUrl.Path)).ConfigureAwait(false); } - - container.ImagePath = container.ItemDetails?.CommonData.DisplayPath.Media.MediaUrl.Path; - - lock (rewardContainers) + else { - rewardContainers.Add(container); + LogEngine.Log("Inventory item is null.", LogSeverity.Error); } } - catch (Exception ex) - { - LogEngine.Log($"Could not set container item details for {inventoryReward.InventoryItemPath}. {ex.Message}", LogSeverity.Error); - } - finally + else { - semaphore.Release(); - } - } - - await Task.WhenAll(inventoryItems.Select(ProcessInventoryItem)).ConfigureAwait(false); + var item = await SafeAPICall(async () => await HaloClient.GameCmsGetItem(inventoryReward.InventoryItemPath, HaloClient.ClearanceToken).ConfigureAwait(false)).ConfigureAwait(false); - return rewardContainers; - } - - internal static async Task UpdateLocalImage(string subDirectoryName, string imagePath) - { - if (string.IsNullOrWhiteSpace(imagePath)) - return false; + if (item?.Result != null) + { + LogEngine.Log($"Trying to get local image for {item.Result.CommonData.Id} (entity: {inventoryReward.InventoryItemPath})"); - string qualifiedImagePath = Path.Join(Configuration.AppDataDirectory, subDirectoryName, imagePath); + await DownloadAndSetImage(item.Result.CommonData.DisplayPath.Media.MediaUrl.Path, Path.Combine(Configuration.AppDataDirectory, "imagecache", item.Result.CommonData.DisplayPath.Media.MediaUrl.Path)).ConfigureAwait(false); - // Let's make sure that we create the directory if it does not exist. - EnsureDirectoryExists(qualifiedImagePath); + DataHandler.UpdateInventoryItems(item.Response.Message, inventoryReward.InventoryItemPath); + container.ItemDetails = item.Result; + } + } - if (!System.IO.File.Exists(qualifiedImagePath)) - { - var rankImage = await SafeAPICall(async () => - { - return await HaloClient.GameCmsGetImage(imagePath); - }); + container.ImagePath = container.ItemDetails?.CommonData.DisplayPath.Media.MediaUrl.Path; - if (rankImage.Result != null && rankImage.Response.Code == 200) - { - await System.IO.File.WriteAllBytesAsync(qualifiedImagePath, rankImage.Result); - return true; - } - else - { - return false; - } + return container; } - else + catch (Exception ex) { - // File already exists, so we can safely return true. - return true; + LogEngine.Log($"Could not set container item details for {inventoryReward.InventoryItemPath}. {ex.Message}", LogSeverity.Error); + return null; // or handle error as needed } } @@ -1983,130 +1958,97 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => internal static async Task InitializeAllDataOnLaunch() { - var authResult = await InitializePublicClientApplication(); - if (authResult != null) + try { - var instantiationResult = await InitializeHaloClient(authResult); + var authResult = await InitializePublicClientApplication(); + if (authResult == null) + throw new Exception("Authentication with Halo services failed."); - if (instantiationResult) + var haloClientInitialized = await InitializeHaloClient(authResult); + if (!haloClientInitialized) + throw new Exception("Could not initialize Halo client."); + + // Update UI state + await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { - await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => - { - SplashScreenViewModel.Instance.IsBlocking = false; - }); + SplashScreenViewModel.Instance.IsBlocking = false; + }); - if (instantiationResult) - { - if (string.IsNullOrWhiteSpace(HaloClient.ClearanceToken)) - { - LogEngine.Log($"The clearance is empty, so many API calls that depend on it may fail."); - } + // Set HomeViewModel properties + HomeViewModel.Instance.Gamertag = XboxUserContext.DisplayClaims.Xui[0].Gamertag; + HomeViewModel.Instance.Xuid = XboxUserContext.DisplayClaims.Xui[0].XUID; - HomeViewModel.Instance.Gamertag = XboxUserContext.DisplayClaims.Xui[0].Gamertag; - HomeViewModel.Instance.Xuid = XboxUserContext.DisplayClaims.Xui[0].XUID; + // Bootstrap database and set journaling mode + var databaseBootstrapResult = DataHandler.BootstrapDatabase(); + var journalingMode = DataHandler.SetWALJournalingMode(); - var databaseBootstrapResult = DataHandler.BootstrapDatabase(); - var journalingMode = DataHandler.SetWALJournalingMode(); + if (journalingMode.Equals("wal", StringComparison.Ordinal)) + { + LogEngine.Log("Successfully set WAL journaling mode."); + } + else + { + LogEngine.Log("Could not set WAL journaling mode.", LogSeverity.Warning); + } - if (journalingMode.Equals("wal", StringComparison.Ordinal)) - { - LogEngine.Log("Successfully set WAL journaling mode."); - } - else - { - LogEngine.Log("Could not set WAL journaling mode.", LogSeverity.Warning); - } + // Reset collections + await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => + { + BattlePassViewModel.Instance.BattlePasses = BattlePassViewModel.Instance.BattlePasses ?? []; + MatchesViewModel.Instance.MatchList = MatchesViewModel.Instance.MatchList ?? []; + MedalsViewModel.Instance.Medals = MedalsViewModel.Instance.Medals ?? []; + }); - await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => + // Concurrently populate MatchRecordsData and BattlePassData with other tasks + await Task.WhenAll( + PopulateMatchRecordsData().ContinueWith(async t => + { + if (t.Result) { - // Reset all collections to make sure that left-over data is not displayed. - BattlePassViewModel.Instance.BattlePasses = BattlePassViewModel.Instance.BattlePasses ?? []; - MatchesViewModel.Instance.MatchList = MatchesViewModel.Instance.MatchList ?? []; - MedalsViewModel.Instance.Medals = MedalsViewModel.Instance.Medals ?? []; - }); - - // We want to populate the medal metadata before we do anything else. - MedalMetadata = await PrepopulateMedalMetadata(); - - // Let's get career data first to make sure that it's quickly populated. - _ = await PopulateCareerData(); - - // Service Record data should be pulled early to make sure that we - // get the latest medals quickly before everything else is populated. - _ = await PopulateServiceRecordData(); - - Parallel.Invoke( - async () => await PopulateMedalData(), - async () => await PopulateExchangeData(), - async () => await PopulateCsrImages(), - async () => - { - try - { - await PopulateSeasonCalendar(); - } - catch (Exception ex) - { - LogEngine.Log($"Could not populate the calendar. {ex.Message}", LogSeverity.Error); - } - }, - async () => await PopulateUserInventory(), - async () => await PopulateCustomizationData(), - async () => await PopulateDecorationData(), - async () => - { - var matchRecordsOutcome = await PopulateMatchRecordsData(); - - if (matchRecordsOutcome) - { - await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => - { - MatchesViewModel.Instance.MatchLoadingState = MetadataLoadingState.Completed; - MatchesViewModel.Instance.MatchLoadingParameter = string.Empty; - }); - } - }, - async () => + await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { - try - { - await PopulateBattlePassData(BattlePassLoadingCancellationTracker.Token); - - await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => - { - BattlePassViewModel.Instance.BattlePassLoadingState = MetadataLoadingState.Completed; - }); - } - catch - { - BattlePassLoadingCancellationTracker = new CancellationTokenSource(); - } + MatchesViewModel.Instance.MatchLoadingState = MetadataLoadingState.Completed; + MatchesViewModel.Instance.MatchLoadingParameter = string.Empty; }); - - return true; - } - - return false; - } - else - { - await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => + } + }, TaskScheduler.FromCurrentSynchronizationContext()), + PopulateBattlePassData(BattlePassLoadingCancellationTracker.Token).ContinueWith(async t => { - SplashScreenViewModel.Instance.IsErrorMessageDisplayed = true; - }); + if (t.IsCompletedSuccessfully) + { + await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => + { + BattlePassViewModel.Instance.BattlePassLoadingState = MetadataLoadingState.Completed; + }); + } + else if (t.IsFaulted) + { + BattlePassLoadingCancellationTracker = new CancellationTokenSource(); + } + }, TaskScheduler.FromCurrentSynchronizationContext()), + PopulateCareerData(), + PopulateServiceRecordData(), + PopulateMedalData(), + PopulateExchangeData(), + PopulateCsrImages(), + PopulateSeasonCalendar(), + PopulateUserInventory(), + PopulateCustomizationData(), + PopulateDecorationData() + ); - LogEngine.Log("Could not authenticate with Halo services.", LogSeverity.Error); - return false; - } + return true; } - else + catch (Exception ex) { + // Handle exceptions + LogEngine.Log($"Initialization failed: {ex.Message}", LogSeverity.Error); + await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { SplashScreenViewModel.Instance.IsErrorMessageDisplayed = true; }); - LogEngine.Log("Could not authenticate with Halo services.", LogSeverity.Error); return false; } } From 3afd989b4049dafdf7388284f7d6e085f362592c Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Tue, 9 Jul 2024 16:30:08 -0700 Subject: [PATCH 11/18] Update list of changes --- CURRENTRELEASE.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CURRENTRELEASE.md b/CURRENTRELEASE.md index f3eb7d2..20d2ace 100644 --- a/CURRENTRELEASE.md +++ b/CURRENTRELEASE.md @@ -1,13 +1,16 @@ # OpenSpartan Workshop 1.0.7 (`CYLIX-06082924`) -- Improved image fallback for The Exchange, so that missing items now render properly. -- Season calendar now includes background images for each event, operation, and battle pass season. +- [#30] Prevent auth loops if incorrect release number is used. +- [#38] Battlepass data now correctly renders on smaller screens, allowing scrolling. - [#39] Removes the odd cross-out line in the calendar view. - [#41] Fixes average life positioning, ensuring that it can't cause overflow. +- Improved image fallback for The Exchange, so that missing items now render properly. +- Season calendar now includes background images for each event, operation, and battle pass season. - Calendar colors are now easier to read. - Fixes how ranked match percentage is calculated, now showing proper values for next level. - Home page now can be scrolled on smaller screens. - Inside match metadata, medals and ranked counterfactuals correctly flow when screen is resized. - The app now correctly reacts at startup to an error with authentication token acquisition. A message is shown if that is not possible. +- General performance optimizations and maintainability cleanup. Refer to [**getting started guide**](https://openspartan.com/docs/workshop/guides/get-started/) to start using OpenSpartan Workshop. From e8889268d393b59d2fc8a32272044d9047dcab16 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Tue, 9 Jul 2024 19:28:08 -0700 Subject: [PATCH 12/18] Cleanup converters --- CURRENTRELEASE.md | 4 +- .../Converters/BoolNegativeConverter.cs | 22 ++---- .../BoolToDisabledBrushConverter.cs | 13 +--- .../Converters/BoolToVisibilityConverter.cs | 17 +---- .../CommaAfterThousandsConverter.cs | 10 +-- .../ComplexTimeToSimpleTimeConverter.cs | 21 ++---- .../Converters/CsrProgressStateConverter.cs | 11 +-- .../Converters/CsrToPathConverter.cs | 25 +++---- .../Converters/CsrToProgressConverter.cs | 29 ++------ .../Converters/CsrToTextRankConverter.cs | 23 ++---- .../DirectValueToPercentageStringConverter.cs | 13 +++- .../DoubleToPercentageStringConverter.cs | 13 +++- .../ISO8601ToLocalDateStringConverter.cs | 2 +- .../ListCountToVisibilityConverter.cs | 5 +- .../MedalDifficultyToBrushConverter.cs | 72 ++++++++----------- .../MedalTypeIndexToStringConverter.cs | 24 ++++--- ...tadataLoadingStateToVisibilityConverter.cs | 4 +- .../OutcomeToBackgroundConverter.cs | 34 +++------ .../OutcomeToForegroundConverter.cs | 34 +++------ .../Converters/PerformanceToColorConverter.cs | 22 +++--- .../Converters/PerformanceToGlyphConverter.cs | 14 ++-- .../RankIdentifierToPathConverter.cs | 10 ++- .../Converters/RankToVisibilityConverter.cs | 12 ++-- .../Converters/RewardTypeToStringConverter.cs | 30 +++++--- .../RewardTypeToVisibilityConverter.cs | 22 +++--- .../ServicePathToLocalPathConverter.cs | 21 +++--- ...tValueAvailabilityToVisibilityConverter.cs | 25 ++++--- ...StringAvailabilityToVisibilityConverter.cs | 3 +- .../Core/Configuration.cs | 36 +++------- .../Core/UserContextManager.cs | 24 ++++--- .../Views/SettingsView.xaml.cs | 3 +- 31 files changed, 247 insertions(+), 351 deletions(-) diff --git a/CURRENTRELEASE.md b/CURRENTRELEASE.md index 20d2ace..0d22d42 100644 --- a/CURRENTRELEASE.md +++ b/CURRENTRELEASE.md @@ -1,4 +1,4 @@ -# OpenSpartan Workshop 1.0.7 (`CYLIX-06082924`) +# OpenSpartan Workshop 1.0.7 (`CYLIX-06082024`) - [#30] Prevent auth loops if incorrect release number is used. - [#38] Battlepass data now correctly renders on smaller screens, allowing scrolling. @@ -12,5 +12,7 @@ - Inside match metadata, medals and ranked counterfactuals correctly flow when screen is resized. - The app now correctly reacts at startup to an error with authentication token acquisition. A message is shown if that is not possible. - General performance optimizations and maintainability cleanup. +- Fixed an issue where duplicate ranked playlist may render in the **Ranked Progression** view. +- Fixed an issue where duplicate items may render in the **Exchange** view. Refer to [**getting started guide**](https://openspartan.com/docs/workshop/guides/get-started/) to start using OpenSpartan Workshop. diff --git a/src/OpenSpartan.Workshop/Converters/BoolNegativeConverter.cs b/src/OpenSpartan.Workshop/Converters/BoolNegativeConverter.cs index 643fffe..e2c9cbb 100644 --- a/src/OpenSpartan.Workshop/Converters/BoolNegativeConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/BoolNegativeConverter.cs @@ -5,24 +5,10 @@ namespace OpenSpartan.Workshop.Converters { public class BoolNegativeConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, string language) - { - if (value is bool boolValue) - { - return !boolValue; - } + public object Convert(object value, Type targetType, object parameter, string language) => + value is bool boolValue ? !boolValue : (object)null; - return null; - } - - public object ConvertBack(object value, Type targetType, object parameter, string language) - { - if (value is bool boolValue) - { - return !boolValue; - } - - return false; - } + public object ConvertBack(object value, Type targetType, object parameter, string language) => + value is bool boolValue ? !boolValue : (object)false; } } diff --git a/src/OpenSpartan.Workshop/Converters/BoolToDisabledBrushConverter.cs b/src/OpenSpartan.Workshop/Converters/BoolToDisabledBrushConverter.cs index bd9af48..ab44d8f 100644 --- a/src/OpenSpartan.Workshop/Converters/BoolToDisabledBrushConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/BoolToDisabledBrushConverter.cs @@ -9,16 +9,9 @@ public class BoolToDisabledBrushConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - if (value is bool isActive && !isActive) - { - // Return a brush for an active state - return new SolidColorBrush((Color)Microsoft.UI.Xaml.Application.Current.Resources["TextFillColorPrimary"]); - } - else - { - // Return the system-defined disabled brush - return (SolidColorBrush)Microsoft.UI.Xaml.Application.Current.Resources["SystemControlDisabledBaseMediumLowBrush"]; - } + return value is bool isActive && !isActive + ? new SolidColorBrush((Color)Microsoft.UI.Xaml.Application.Current.Resources["TextFillColorPrimary"]) + : (SolidColorBrush)Microsoft.UI.Xaml.Application.Current.Resources["SystemControlDisabledBaseMediumLowBrush"]; } public object ConvertBack(object value, Type targetType, object parameter, string language) diff --git a/src/OpenSpartan.Workshop/Converters/BoolToVisibilityConverter.cs b/src/OpenSpartan.Workshop/Converters/BoolToVisibilityConverter.cs index 624fe03..e3412aa 100644 --- a/src/OpenSpartan.Workshop/Converters/BoolToVisibilityConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/BoolToVisibilityConverter.cs @@ -6,21 +6,10 @@ namespace OpenSpartan.Workshop.Converters { internal sealed class BoolToVisibilityConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, string language) - { - if ((bool)value) - { - return Visibility.Visible; - } - else - { - return Visibility.Collapsed; - } - } + public object Convert(object value, Type targetType, object parameter, string language) => + (bool)value ? Visibility.Visible : Visibility.Collapsed; - public object ConvertBack(object value, Type targetType, object parameter, string language) - { + public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException(); - } } } diff --git a/src/OpenSpartan.Workshop/Converters/CommaAfterThousandsConverter.cs b/src/OpenSpartan.Workshop/Converters/CommaAfterThousandsConverter.cs index 878be26..18cce7a 100644 --- a/src/OpenSpartan.Workshop/Converters/CommaAfterThousandsConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/CommaAfterThousandsConverter.cs @@ -6,14 +6,10 @@ namespace OpenSpartan.Workshop.Converters { internal sealed class CommaAfterThousandsConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, string language) - { - return string.Format(CultureInfo.InvariantCulture, "{0:n0}", value); - } + public object Convert(object value, Type targetType, object parameter, string language) => + string.Format(CultureInfo.InvariantCulture, "{0:n0}", value); - public object ConvertBack(object value, Type targetType, object parameter, string language) - { + public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException(); - } } } diff --git a/src/OpenSpartan.Workshop/Converters/ComplexTimeToSimpleTimeConverter.cs b/src/OpenSpartan.Workshop/Converters/ComplexTimeToSimpleTimeConverter.cs index 7f614bf..6b59936 100644 --- a/src/OpenSpartan.Workshop/Converters/ComplexTimeToSimpleTimeConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/ComplexTimeToSimpleTimeConverter.cs @@ -6,27 +6,18 @@ namespace OpenSpartan.Workshop.Converters { internal sealed class ComplexTimeToSimpleTimeConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, string language) - { - if (value is TimeSpan interval) - { - var parts = new[] + public object Convert(object value, Type targetType, object parameter, string language) => + value is TimeSpan interval + ? string.Join(" ", new[] { interval.Days > 0 ? $"{interval.Days}d" : null, interval.Hours > 0 ? $"{interval.Hours}hr" : null, interval.Minutes > 0 ? $"{interval.Minutes}min" : null, interval.Seconds > 0 || interval.TotalSeconds < 60 ? $"{interval.Seconds}sec" : null - }; + }.Where(part => part != null)) + : value; - return string.Join(" ", parts.Where(part => part != null)); - } - - return value; - } - - public object ConvertBack(object value, Type targetType, object parameter, string language) - { + public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException(); - } } } diff --git a/src/OpenSpartan.Workshop/Converters/CsrProgressStateConverter.cs b/src/OpenSpartan.Workshop/Converters/CsrProgressStateConverter.cs index 4acfb21..8c5c34a 100644 --- a/src/OpenSpartan.Workshop/Converters/CsrProgressStateConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/CsrProgressStateConverter.cs @@ -8,21 +8,16 @@ internal sealed class CsrProgressStateConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - MatchTableEntity entity = value as MatchTableEntity; - - if (entity != null) + if (value is MatchTableEntity entity) { bool inverse = parameter?.ToString() == "inverse"; - - return (entity.PostMatchCsr > entity.PreMatchCsr ^ inverse) ? entity.PostMatchCsr : entity.PreMatchCsr; + return (entity.PostMatchCsr > entity.PreMatchCsr) ^ inverse ? entity.PostMatchCsr : entity.PreMatchCsr; } return 0; } - public object ConvertBack(object value, Type targetType, object parameter, string language) - { + public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException(); - } } } diff --git a/src/OpenSpartan.Workshop/Converters/CsrToPathConverter.cs b/src/OpenSpartan.Workshop/Converters/CsrToPathConverter.cs index 4d336b2..1163fd9 100644 --- a/src/OpenSpartan.Workshop/Converters/CsrToPathConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/CsrToPathConverter.cs @@ -9,24 +9,17 @@ internal class CsrToPathConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - Csr csr = (Csr)value; - string tierPath = string.Empty; - // Tier is zero-indexed, but the images are starting from 1, so we need to ensure - // that we increment the tier to get the right image. - if (!string.IsNullOrEmpty(csr.Tier)) - { - tierPath = Path.Combine(Core.Configuration.AppDataDirectory, "imagecache", "csr", $"{csr.Tier.ToLowerInvariant()}_{csr.SubTier + 1}.png"); - } - else - { - tierPath = Path.Combine(Core.Configuration.AppDataDirectory, "imagecache", "csr", $"unranked_{csr.InitialMeasurementMatches - csr.MeasurementMatchesRemaining}.png"); - } - return tierPath; + if (value is not Csr csr) + return string.Empty; + + string fileName = !string.IsNullOrEmpty(csr.Tier) + ? $"{csr.Tier.ToLowerInvariant()}_{csr.SubTier + 1}.png" + : $"unranked_{csr.InitialMeasurementMatches - csr.MeasurementMatchesRemaining}.png"; + + return Path.Combine(Core.Configuration.AppDataDirectory, "imagecache", "csr", fileName); } - public object ConvertBack(object value, Type targetType, object parameter, string language) - { + public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException(); - } } } diff --git a/src/OpenSpartan.Workshop/Converters/CsrToProgressConverter.cs b/src/OpenSpartan.Workshop/Converters/CsrToProgressConverter.cs index 1f3455d..6c3b497 100644 --- a/src/OpenSpartan.Workshop/Converters/CsrToProgressConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/CsrToProgressConverter.cs @@ -6,31 +6,12 @@ namespace OpenSpartan.Workshop.Converters { internal class CsrToProgressConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, string language) - { - Csr currentCsr = (Csr)value; - if (currentCsr != null) - { - // When the Value is -1 that means the user is not ranked - there is no - // progress to report on. - if (currentCsr.Value > -1) - { - return ((double)currentCsr.Value - (double)currentCsr.TierStart) / ((double)currentCsr.NextTierStart - (double)currentCsr.TierStart); - } - else - { - return (double)0; - } - } - else - { - return (double)0; - } - } + public object Convert(object value, Type targetType, object parameter, string language) => + value is Csr currentCsr && currentCsr.Value > -1 + ? (double)(currentCsr.Value - currentCsr.TierStart) / (currentCsr.NextTierStart - currentCsr.TierStart) + : (double)0; - public object ConvertBack(object value, Type targetType, object parameter, string language) - { + public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException(); - } } } diff --git a/src/OpenSpartan.Workshop/Converters/CsrToTextRankConverter.cs b/src/OpenSpartan.Workshop/Converters/CsrToTextRankConverter.cs index cebfc5b..740abb2 100644 --- a/src/OpenSpartan.Workshop/Converters/CsrToTextRankConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/CsrToTextRankConverter.cs @@ -6,25 +6,12 @@ namespace OpenSpartan.Workshop.Converters { internal class CsrToTextRankConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, string language) - { - Csr csr = (Csr)value; + public object Convert(object value, Type targetType, object parameter, string language) => + value is Csr csr && !string.IsNullOrEmpty(csr.Tier) + ? $"{csr.Tier} {csr.SubTier + 1}" + : "Unranked"; - // Tier is zero-indexed, but the images are starting from 1, so we need to ensure - // that we increment the tier to get the right image. - if (!string.IsNullOrEmpty(csr.Tier)) - { - return $"{csr.Tier} {csr.SubTier + 1}"; - } - else - { - return "Unranked"; - } - } - - public object ConvertBack(object value, Type targetType, object parameter, string language) - { + public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException(); - } } } diff --git a/src/OpenSpartan.Workshop/Converters/DirectValueToPercentageStringConverter.cs b/src/OpenSpartan.Workshop/Converters/DirectValueToPercentageStringConverter.cs index 990e045..d772d3b 100644 --- a/src/OpenSpartan.Workshop/Converters/DirectValueToPercentageStringConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/DirectValueToPercentageStringConverter.cs @@ -8,7 +8,18 @@ internal class DirectValueToPercentageStringConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - return $"{System.Convert.ToDouble(value, CultureInfo.InvariantCulture) / 100.0:P02}"; + if (value == null) + return "0.00%"; + + if (double.TryParse(value.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out double doubleValue)) + { + return $"{doubleValue / 100.0:P02}"; + } + else + { + // Handle cases where value cannot be converted to double + return "0.00%"; + } } public object ConvertBack(object value, Type targetType, object parameter, string language) diff --git a/src/OpenSpartan.Workshop/Converters/DoubleToPercentageStringConverter.cs b/src/OpenSpartan.Workshop/Converters/DoubleToPercentageStringConverter.cs index 79b3fbd..770c5b4 100644 --- a/src/OpenSpartan.Workshop/Converters/DoubleToPercentageStringConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/DoubleToPercentageStringConverter.cs @@ -8,7 +8,18 @@ internal sealed class DoubleToPercentageStringConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - return $"{System.Convert.ToDouble(value, CultureInfo.InvariantCulture):P2}"; + if (value == null) + return "0.00%"; + + if (double.TryParse(value.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out double doubleValue)) + { + return $"{doubleValue:P2}"; + } + else + { + // Handle cases where value cannot be converted to double + return "0.00%"; + } } public object ConvertBack(object value, Type targetType, object parameter, string language) diff --git a/src/OpenSpartan.Workshop/Converters/ISO8601ToLocalDateStringConverter.cs b/src/OpenSpartan.Workshop/Converters/ISO8601ToLocalDateStringConverter.cs index d65f8b0..3766249 100644 --- a/src/OpenSpartan.Workshop/Converters/ISO8601ToLocalDateStringConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/ISO8601ToLocalDateStringConverter.cs @@ -13,7 +13,7 @@ public object Convert(object value, Type targetType, object parameter, string la return dateTime.ToString("MMMM d, yyyy h:mm tt", CultureInfo.CurrentCulture); } - return value; + return string.Empty; // Handle null value gracefully or return an appropriate default } public object ConvertBack(object value, Type targetType, object parameter, string language) diff --git a/src/OpenSpartan.Workshop/Converters/ListCountToVisibilityConverter.cs b/src/OpenSpartan.Workshop/Converters/ListCountToVisibilityConverter.cs index e495f11..e42b873 100644 --- a/src/OpenSpartan.Workshop/Converters/ListCountToVisibilityConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/ListCountToVisibilityConverter.cs @@ -2,7 +2,6 @@ using Microsoft.UI.Xaml.Data; using System; using System.Collections; -using System.Linq; namespace OpenSpartan.Workshop.Converters { @@ -10,9 +9,9 @@ internal class ListCountToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - if (value is IEnumerable enumerable) + if (value is IEnumerable enumerable && enumerable.GetEnumerator().MoveNext()) { - return enumerable.Cast().Any() ? Visibility.Visible : Visibility.Collapsed; + return Visibility.Visible; } return Visibility.Collapsed; diff --git a/src/OpenSpartan.Workshop/Converters/MedalDifficultyToBrushConverter.cs b/src/OpenSpartan.Workshop/Converters/MedalDifficultyToBrushConverter.cs index 0951151..b9fa1a1 100644 --- a/src/OpenSpartan.Workshop/Converters/MedalDifficultyToBrushConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/MedalDifficultyToBrushConverter.cs @@ -1,7 +1,7 @@ using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Media; using System; -using System.Globalization; +using Windows.UI; namespace OpenSpartan.Workshop.Converters { @@ -9,50 +9,40 @@ internal sealed class MedalDifficultyToBrushConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - int typeIndex = System.Convert.ToInt32(value, CultureInfo.InvariantCulture); - var gCollection = new GradientStopCollection(); - - switch (typeIndex) + if (value is int typeIndex) { - // Normal - case 0: - gCollection = new GradientStopCollection - { - new GradientStop() { Color = Windows.UI.Color.FromArgb(255, 64, 64, 64), Offset = 0.3 }, - new GradientStop() { Color = Windows.UI.Color.FromArgb(255, 32, 91, 34), Offset = 0.7 }, - }; - - return new LinearGradientBrush(gCollection, 90); - // Heroic - case 1: - gCollection = new GradientStopCollection - { - new GradientStop() { Color = Windows.UI.Color.FromArgb(255, 64, 64, 64), Offset = 0.3 }, - new GradientStop() { Color = Windows.UI.Color.FromArgb(255, 32, 50, 79), Offset = 0.7 }, - }; + GradientStopCollection gCollection = new GradientStopCollection(); - return new LinearGradientBrush(gCollection, 90); - // Legendary - case 2: - gCollection = new GradientStopCollection - { - new GradientStop() { Color = Windows.UI.Color.FromArgb(255, 64, 64, 64), Offset = 0.3 }, - new GradientStop() { Color = Windows.UI.Color.FromArgb(255, 71, 36, 116), Offset = 0.7 }, - }; + switch (typeIndex) + { + // Normal + case 0: + gCollection.Add(new GradientStop() { Color = Color.FromArgb(255, 64, 64, 64), Offset = 0.3 }); + gCollection.Add(new GradientStop() { Color = Color.FromArgb(255, 32, 91, 34), Offset = 0.7 }); + break; + // Heroic + case 1: + gCollection.Add(new GradientStop() { Color = Color.FromArgb(255, 64, 64, 64), Offset = 0.3 }); + gCollection.Add(new GradientStop() { Color = Color.FromArgb(255, 32, 50, 79), Offset = 0.7 }); + break; + // Legendary + case 2: + gCollection.Add(new GradientStop() { Color = Color.FromArgb(255, 64, 64, 64), Offset = 0.3 }); + gCollection.Add(new GradientStop() { Color = Color.FromArgb(255, 71, 36, 116), Offset = 0.7 }); + break; + // Mythic + case 3: + gCollection.Add(new GradientStop() { Color = Color.FromArgb(255, 64, 64, 64), Offset = 0.3 }); + gCollection.Add(new GradientStop() { Color = Color.FromArgb(255, 92, 31, 40), Offset = 0.7 }); + break; + default: + return "N/A"; + } - return new LinearGradientBrush(gCollection, 90); - // Mythic - case 3: - gCollection = new GradientStopCollection - { - new GradientStop() { Color = Windows.UI.Color.FromArgb(255, 64, 64, 64), Offset = 0.3 }, - new GradientStop() { Color = Windows.UI.Color.FromArgb(255, 92, 31, 40), Offset = 0.7 }, - }; - - return new LinearGradientBrush(gCollection, 90); - default: - return "N/A"; + return new LinearGradientBrush(gCollection, 90); } + + return "N/A"; } public object ConvertBack(object value, Type targetType, object parameter, string language) diff --git a/src/OpenSpartan.Workshop/Converters/MedalTypeIndexToStringConverter.cs b/src/OpenSpartan.Workshop/Converters/MedalTypeIndexToStringConverter.cs index f335146..43f7a05 100644 --- a/src/OpenSpartan.Workshop/Converters/MedalTypeIndexToStringConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/MedalTypeIndexToStringConverter.cs @@ -8,17 +8,21 @@ internal sealed class MedalTypeIndexToStringConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - int typeIndex = System.Convert.ToInt32(value, CultureInfo.InvariantCulture); - return typeIndex switch + if (value is int typeIndex) { - 0 => "Spree", - 1 => "Mode", - 2 => "Multikill", - 3 => "Proficiency", - 4 => "Skill", - 5 => "Style", - _ => "N/A", - }; + return typeIndex switch + { + 0 => "Spree", + 1 => "Mode", + 2 => "Multikill", + 3 => "Proficiency", + 4 => "Skill", + 5 => "Style", + _ => "N/A", + }; + } + + return "N/A"; // Handle non-integer or null values gracefully } public object ConvertBack(object value, Type targetType, object parameter, string language) diff --git a/src/OpenSpartan.Workshop/Converters/MetadataLoadingStateToVisibilityConverter.cs b/src/OpenSpartan.Workshop/Converters/MetadataLoadingStateToVisibilityConverter.cs index 435995a..795e93b 100644 --- a/src/OpenSpartan.Workshop/Converters/MetadataLoadingStateToVisibilityConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/MetadataLoadingStateToVisibilityConverter.cs @@ -9,9 +9,7 @@ internal sealed class MetadataLoadingStateToVisibilityConverter : IValueConverte { public object Convert(object value, Type targetType, object parameter, string language) { - var state = (MetadataLoadingState)value; - - if (state != MetadataLoadingState.Completed) + if (value is MetadataLoadingState state && state != MetadataLoadingState.Completed) { return Visibility.Visible; } diff --git a/src/OpenSpartan.Workshop/Converters/OutcomeToBackgroundConverter.cs b/src/OpenSpartan.Workshop/Converters/OutcomeToBackgroundConverter.cs index d3b2c68..1d03958 100644 --- a/src/OpenSpartan.Workshop/Converters/OutcomeToBackgroundConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/OutcomeToBackgroundConverter.cs @@ -9,31 +9,19 @@ internal sealed class OutcomeToBackgroundConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - Outcome outcome = (Outcome)value; - - switch (outcome) + if (value is Outcome outcome) { - case Outcome.DidNotFinish: - { - return new SolidColorBrush(Windows.UI.Color.FromArgb(50, 115, 103, 240)); - } - case Outcome.Loss: - { - return new SolidColorBrush(Windows.UI.Color.FromArgb(50, 234, 84, 85)); - } - case Outcome.Tie: - { - return new SolidColorBrush(Windows.UI.Color.FromArgb(50, 115, 103, 240)); - } - case Outcome.Win: - { - return new SolidColorBrush(Windows.UI.Color.FromArgb(50, 40, 199, 111)); - } - default: - { - return new SolidColorBrush(Windows.UI.Color.FromArgb(50, 0, 0, 0)); - } + return outcome switch + { + Outcome.DidNotFinish => new SolidColorBrush(Windows.UI.Color.FromArgb(50, 115, 103, 240)), + Outcome.Loss => new SolidColorBrush(Windows.UI.Color.FromArgb(50, 234, 84, 85)), + Outcome.Tie => new SolidColorBrush(Windows.UI.Color.FromArgb(50, 115, 103, 240)), + Outcome.Win => new SolidColorBrush(Windows.UI.Color.FromArgb(50, 40, 199, 111)), + _ => new SolidColorBrush(Windows.UI.Color.FromArgb(50, 0, 0, 0)), + }; } + + return new SolidColorBrush(Windows.UI.Color.FromArgb(50, 0, 0, 0)); } public object ConvertBack(object value, Type targetType, object parameter, string language) diff --git a/src/OpenSpartan.Workshop/Converters/OutcomeToForegroundConverter.cs b/src/OpenSpartan.Workshop/Converters/OutcomeToForegroundConverter.cs index 86a22fe..9272e03 100644 --- a/src/OpenSpartan.Workshop/Converters/OutcomeToForegroundConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/OutcomeToForegroundConverter.cs @@ -9,31 +9,19 @@ internal sealed class OutcomeToForegroundConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - Outcome outcome = (Outcome)value; - - switch (outcome) + if (value is Outcome outcome) { - case Outcome.DidNotFinish: - { - return new SolidColorBrush(Windows.UI.Color.FromArgb(255, 115, 103, 240)); - } - case Outcome.Loss: - { - return new SolidColorBrush(Windows.UI.Color.FromArgb(255, 234, 84, 85)); - } - case Outcome.Tie: - { - return new SolidColorBrush(Windows.UI.Color.FromArgb(255, 115, 103, 240)); - } - case Outcome.Win: - { - return new SolidColorBrush(Windows.UI.Color.FromArgb(255, 40, 199, 111)); - } - default: - { - return new SolidColorBrush(Windows.UI.Color.FromArgb(255, 0, 0, 0)); - } + return outcome switch + { + Outcome.DidNotFinish => new SolidColorBrush(Windows.UI.Color.FromArgb(255, 115, 103, 240)), + Outcome.Loss => new SolidColorBrush(Windows.UI.Color.FromArgb(255, 234, 84, 85)), + Outcome.Tie => new SolidColorBrush(Windows.UI.Color.FromArgb(255, 115, 103, 240)), + Outcome.Win => new SolidColorBrush(Windows.UI.Color.FromArgb(255, 40, 199, 111)), + _ => new SolidColorBrush(Windows.UI.Color.FromArgb(255, 0, 0, 0)), + }; } + + return new SolidColorBrush(Windows.UI.Color.FromArgb(255, 0, 0, 0)); } public object ConvertBack(object value, Type targetType, object parameter, string language) diff --git a/src/OpenSpartan.Workshop/Converters/PerformanceToColorConverter.cs b/src/OpenSpartan.Workshop/Converters/PerformanceToColorConverter.cs index 0d53b34..94645aa 100644 --- a/src/OpenSpartan.Workshop/Converters/PerformanceToColorConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/PerformanceToColorConverter.cs @@ -1,29 +1,27 @@ using Microsoft.UI.Xaml.Data; using OpenSpartan.Workshop.Models; -using System; -using System.Collections.Generic; using Microsoft.UI.Xaml.Media; using Microsoft.UI; +using System; namespace OpenSpartan.Workshop.Converters { internal sealed class PerformanceToColorConverter : IValueConverter { - private readonly Dictionary performanceBrushMap = new Dictionary - { - { PerformanceMeasure.Outperformed, new SolidColorBrush(Windows.UI.Color.FromArgb(255, 40, 199, 111)) }, - { PerformanceMeasure.Underperformed, new SolidColorBrush(Windows.UI.Color.FromArgb(255, 234, 84, 85)) }, - { PerformanceMeasure.MetExpectations, new SolidColorBrush(Windows.UI.Color.FromArgb(255, 115, 103, 240)) }, - }; - public object Convert(object value, Type targetType, object parameter, string language) { - if (value is PerformanceMeasure performance && performanceBrushMap.TryGetValue(performance, out SolidColorBrush brush)) + if (value is PerformanceMeasure performance) { - return brush; + return performance switch + { + PerformanceMeasure.Outperformed => new SolidColorBrush(Windows.UI.Color.FromArgb(255, 40, 199, 111)), + PerformanceMeasure.Underperformed => new SolidColorBrush(Windows.UI.Color.FromArgb(255, 234, 84, 85)), + PerformanceMeasure.MetExpectations => new SolidColorBrush(Windows.UI.Color.FromArgb(255, 115, 103, 240)), + _ => new SolidColorBrush(Colors.Black), + }; } - // Return a default SolidColorBrush (e.g., Black) if the conversion fails + // Return a default SolidColorBrush (e.g., Black) if the input value is null or not of expected type return new SolidColorBrush(Colors.Black); } diff --git a/src/OpenSpartan.Workshop/Converters/PerformanceToGlyphConverter.cs b/src/OpenSpartan.Workshop/Converters/PerformanceToGlyphConverter.cs index fd69ff6..33f8aa2 100644 --- a/src/OpenSpartan.Workshop/Converters/PerformanceToGlyphConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/PerformanceToGlyphConverter.cs @@ -9,20 +9,16 @@ internal sealed class PerformanceToGlyphConverter : IValueConverter { private readonly Dictionary performanceGlyphMap = new() { - { PerformanceMeasure.Outperformed, "e742" }, - { PerformanceMeasure.Underperformed, "e741" }, - { PerformanceMeasure.MetExpectations, "e73f" }, + { PerformanceMeasure.Outperformed, "\xE742" }, + { PerformanceMeasure.Underperformed, "\xE741" }, + { PerformanceMeasure.MetExpectations, "\xE73F" }, }; public object Convert(object value, Type targetType, object parameter, string language) { - if (value is PerformanceMeasure performance && performanceGlyphMap.TryGetValue(performance, out string unicodeString)) + if (value is PerformanceMeasure performance && performanceGlyphMap.TryGetValue(performance, out string glyph)) { - if (int.TryParse(unicodeString, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out int unicodeValue)) - { - char glyphChar = (char)unicodeValue; - return glyphChar.ToString(); - } + return glyph; } return string.Empty; diff --git a/src/OpenSpartan.Workshop/Converters/RankIdentifierToPathConverter.cs b/src/OpenSpartan.Workshop/Converters/RankIdentifierToPathConverter.cs index 21fc2c3..8f90d7a 100644 --- a/src/OpenSpartan.Workshop/Converters/RankIdentifierToPathConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/RankIdentifierToPathConverter.cs @@ -8,8 +8,12 @@ internal sealed class RankIdentifierToPathConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - var tierPath = Path.Combine(Core.Configuration.AppDataDirectory, "imagecache", "csr", $"{value}.png"); - return tierPath; + if (value is string rankIdentifier) + { + return Path.Combine(Core.Configuration.AppDataDirectory, "imagecache", "csr", $"{rankIdentifier}.png"); + } + + return null; } public object ConvertBack(object value, Type targetType, object parameter, string language) @@ -17,4 +21,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/src/OpenSpartan.Workshop/Converters/RankToVisibilityConverter.cs b/src/OpenSpartan.Workshop/Converters/RankToVisibilityConverter.cs index e16e266..6e32066 100644 --- a/src/OpenSpartan.Workshop/Converters/RankToVisibilityConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/RankToVisibilityConverter.cs @@ -8,16 +8,12 @@ internal sealed class RankToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - Tuple ranks = (Tuple)value; - - if (ranks.Item1 <= ranks.Item2) - { - return Visibility.Visible; - } - else + if (value is Tuple ranks) { - return Visibility.Collapsed; + return ranks.Item1 <= ranks.Item2 ? Visibility.Visible : Visibility.Collapsed; } + + return Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, string language) diff --git a/src/OpenSpartan.Workshop/Converters/RewardTypeToStringConverter.cs b/src/OpenSpartan.Workshop/Converters/RewardTypeToStringConverter.cs index b535056..c10ede9 100644 --- a/src/OpenSpartan.Workshop/Converters/RewardTypeToStringConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/RewardTypeToStringConverter.cs @@ -8,16 +8,28 @@ internal class RewardTypeToStringConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - ItemMetadataContainer type = (ItemMetadataContainer)value; - return type.Type switch + if (value is ItemMetadataContainer type) { - ItemClass.XPGrant => "XP Grant", - ItemClass.SpartanPoints => "Spartan Points", - ItemClass.Credits => "Credits", - ItemClass.XPBoost => "XP Boost", - ItemClass.ChallengeReroll => "Challenge Swap", - _ => type.ItemDetails.CommonData.Title.Value, - }; + switch (type.Type) + { + case ItemClass.XPGrant: + return "XP Grant"; + case ItemClass.SpartanPoints: + return "Spartan Points"; + case ItemClass.Credits: + return "Credits"; + case ItemClass.XPBoost: + return "XP Boost"; + case ItemClass.ChallengeReroll: + return "Challenge Swap"; + case ItemClass.StandardReward: + break; + default: + return type.ItemDetails.CommonData.Title.Value; + } + } + + return string.Empty; } public object ConvertBack(object value, Type targetType, object parameter, string language) diff --git a/src/OpenSpartan.Workshop/Converters/RewardTypeToVisibilityConverter.cs b/src/OpenSpartan.Workshop/Converters/RewardTypeToVisibilityConverter.cs index e0c031e..55d79b7 100644 --- a/src/OpenSpartan.Workshop/Converters/RewardTypeToVisibilityConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/RewardTypeToVisibilityConverter.cs @@ -9,16 +9,20 @@ internal class RewardTypeToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - ItemClass type = (ItemClass)value; - return type switch + if (value is ItemClass type) { - ItemClass.XPGrant or - ItemClass.SpartanPoints or - ItemClass.Credits or - ItemClass.XPBoost or - ItemClass.ChallengeReroll => Visibility.Visible, - _ => Visibility.Collapsed, - }; + return type switch + { + ItemClass.XPGrant or + ItemClass.SpartanPoints or + ItemClass.Credits or + ItemClass.XPBoost or + ItemClass.ChallengeReroll => Visibility.Visible, + _ => Visibility.Collapsed, + }; + } + + return Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, string language) diff --git a/src/OpenSpartan.Workshop/Converters/ServicePathToLocalPathConverter.cs b/src/OpenSpartan.Workshop/Converters/ServicePathToLocalPathConverter.cs index b759f19..3962f56 100644 --- a/src/OpenSpartan.Workshop/Converters/ServicePathToLocalPathConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/ServicePathToLocalPathConverter.cs @@ -8,22 +8,17 @@ internal sealed class ServicePathToLocalPathConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - var localPath = string.Empty; - - if (value != null) + if (value is string targetPath) { - string? targetPath = value as string; - - if (Path.IsPathRooted(targetPath)) - { - targetPath = targetPath.TrimStart(Path.DirectorySeparatorChar); - targetPath = targetPath.TrimStart(Path.AltDirectorySeparatorChar); - } + // Normalize the targetPath by removing leading directory separators + targetPath = targetPath.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - localPath = Path.Join(Core.Configuration.AppDataDirectory, "imagecache", targetPath); + // Construct the local path + var localPath = Path.Combine(Core.Configuration.AppDataDirectory, "imagecache", targetPath); + return localPath; } - return localPath; + return string.Empty; } public object ConvertBack(object value, Type targetType, object parameter, string language) @@ -31,4 +26,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/src/OpenSpartan.Workshop/Converters/ShortValueAvailabilityToVisibilityConverter.cs b/src/OpenSpartan.Workshop/Converters/ShortValueAvailabilityToVisibilityConverter.cs index be7e9b6..1bf1c35 100644 --- a/src/OpenSpartan.Workshop/Converters/ShortValueAvailabilityToVisibilityConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/ShortValueAvailabilityToVisibilityConverter.cs @@ -4,24 +4,23 @@ namespace OpenSpartan.Workshop.Converters { - internal sealed class SingleValueAvailabilityToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) { - public object Convert(object value, Type targetType, object parameter, string language) + if (value is float floatValue && floatValue > 0) { - if (value != null && (float)value > 0) - { - return Visibility.Visible; - } - else - { - return Visibility.Collapsed; - } + return Visibility.Visible; } - - public object ConvertBack(object value, Type targetType, object parameter, string language) + else { - throw new NotImplementedException(); + return Visibility.Collapsed; } } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } } diff --git a/src/OpenSpartan.Workshop/Converters/StringAvailabilityToVisibilityConverter.cs b/src/OpenSpartan.Workshop/Converters/StringAvailabilityToVisibilityConverter.cs index 33a1401..5d60d96 100644 --- a/src/OpenSpartan.Workshop/Converters/StringAvailabilityToVisibilityConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/StringAvailabilityToVisibilityConverter.cs @@ -8,7 +8,8 @@ internal sealed class StringAvailabilityToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - if (value != null && !string.IsNullOrWhiteSpace((string)value)) + // Check if value is a non-null and non-empty string + if (!string.IsNullOrEmpty(value as string)) { return Visibility.Visible; } diff --git a/src/OpenSpartan.Workshop/Core/Configuration.cs b/src/OpenSpartan.Workshop/Core/Configuration.cs index 2574d72..a104de2 100644 --- a/src/OpenSpartan.Workshop/Core/Configuration.cs +++ b/src/OpenSpartan.Workshop/Core/Configuration.cs @@ -12,7 +12,7 @@ internal sealed class Configuration // Build-related metadata. internal const string Version = "1.0.7"; - internal const string BuildId = "CYLIX-06082924"; + internal const string BuildId = "CYLIX-06082024"; internal const string PackageName = "OpenSpartan.Workshop"; // Authentication and setting-related metadata. @@ -76,29 +76,15 @@ internal sealed class Configuration ]; internal static readonly string[] SeasonColors = - [ - "#08B2E3", - "#EE6352", - "#57A773", - "#AB2346", - "#D5A021", - "#550527", - "#688E26", - "#F44708", - "#A10702", - "#38405F", - "#FF0035", - "#820263", - "#D90368", - "#170F11", - "#36C9C6", - "#D8A7CA", - "#7E3F8F", - "#402039", - "#313715", - "#390099", - "#1E555C", - "#941C2F", - ]; + { + "#08B2E3", "#EE6352", "#57A773", "#AB2346", "#D5A021", "#550527", "#688E26", "#F44708", + "#A10702", "#38405F", "#FF0035", "#820263", "#D90368", "#170F11", "#36C9C6", "#D8A7CA", + "#7E3F8F", "#402039", "#313715", "#390099", "#1E555C", "#941C2F", "#0D98BA", "#FF5733", + "#28A745", "#DA70D6", "#FF4500", "#FFD700", "#FF1493", "#00FA9A", "#7FFFD4", "#4682B4", + "#FF8C00", "#00CED1", "#9400D3", "#FF00FF", "#800000", "#00FF7F", "#7CFC00", "#FF6347", + "#EE82EE", "#BA55D3", "#FA8072", "#B22222", "#008080", "#DAA520", "#000080", "#FF4500", + "#ADFF2F", "#FFDEAD", "#00FF00", "#FFFF54", "#B0E57C", "#DC143C", "#32CD32", "#FFD700", + "#8A2BE2", "#FF69B4", "#4B0082", + }; } } diff --git a/src/OpenSpartan.Workshop/Core/UserContextManager.cs b/src/OpenSpartan.Workshop/Core/UserContextManager.cs index 9bd10f0..c95fa8f 100644 --- a/src/OpenSpartan.Workshop/Core/UserContextManager.cs +++ b/src/OpenSpartan.Workshop/Core/UserContextManager.cs @@ -1,5 +1,4 @@ -using CommunityToolkit.Common; -using CommunityToolkit.WinUI; +using CommunityToolkit.WinUI; using Den.Dev.Orion.Authentication; using Den.Dev.Orion.Core; using Den.Dev.Orion.Models; @@ -24,7 +23,6 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Text.Json; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -454,13 +452,16 @@ private static async Task ProcessRankedPlaylists(PlayerServiceRecord serviceReco { await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { - RankedViewModel.Instance.Playlists.Add(new PlaylistCSRSnapshot + if (!RankedViewModel.Instance.Playlists.Any(x => x.Id == playlist)) { - Name = playlistMetadata.Result.PublicName, - Id = playlist, - Version = playlistConfigurationResult.Result.UgcPlaylistVersion, - Snapshot = playlistCsr.Result.Value[0] // Assuming we only get data for one player - }); + RankedViewModel.Instance.Playlists.Add(new PlaylistCSRSnapshot + { + Name = playlistMetadata.Result.PublicName, + Id = playlist, + Version = playlistConfigurationResult.Result.UgcPlaylistVersion, + Snapshot = playlistCsr.Result.Value[0] // Assuming we only get data for one player + }); + } }); } } @@ -1938,7 +1939,10 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { - ExchangeViewModel.Instance.ExchangeItems.Add(metadataContainer); + if (!ExchangeViewModel.Instance.ExchangeItems.Any(x => x.ItemDetails.CommonData.Id == metadataContainer.ItemDetails.CommonData.Id)) + { + ExchangeViewModel.Instance.ExchangeItems.Add(metadataContainer); + } }); } diff --git a/src/OpenSpartan.Workshop/Views/SettingsView.xaml.cs b/src/OpenSpartan.Workshop/Views/SettingsView.xaml.cs index f811895..a4a3c57 100644 --- a/src/OpenSpartan.Workshop/Views/SettingsView.xaml.cs +++ b/src/OpenSpartan.Workshop/Views/SettingsView.xaml.cs @@ -1,6 +1,5 @@ using CommunityToolkit.WinUI; using Microsoft.UI.Xaml.Controls; -using NLog; using OpenSpartan.Workshop.Core; using OpenSpartan.Workshop.ViewModels; using System; @@ -21,7 +20,7 @@ private async void btnLogOut_Click(object sender, Microsoft.UI.Xaml.RoutedEventA ContentDialog deleteFileDialog = new ContentDialog { Title = "Log out", - Content = "Are you sure you want to log out?", + Content = "Are you sure you want to log out? Your data will not be deleted.", PrimaryButtonText = "Yes", CloseButtonText = "No", DefaultButton = ContentDialogButton.Close, From 6fe0617f8b3e72e7b02292daddaa665e80f8cdf8 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Wed, 10 Jul 2024 18:30:49 -0700 Subject: [PATCH 13/18] Update with currency checks --- CURRENTRELEASE.md | 1 + .../Core/UserContextManager.cs | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/CURRENTRELEASE.md b/CURRENTRELEASE.md index 0d22d42..590c1c8 100644 --- a/CURRENTRELEASE.md +++ b/CURRENTRELEASE.md @@ -14,5 +14,6 @@ - General performance optimizations and maintainability cleanup. - Fixed an issue where duplicate ranked playlist may render in the **Ranked Progression** view. - Fixed an issue where duplicate items may render in the **Exchange** view. +- Massive speed up to the **Operations** view load times - the app no longer issues unnecessary REST API calls to get currency data. Refer to [**getting started guide**](https://openspartan.com/docs/workshop/guides/get-started/) to start using OpenSpartan Workshop. diff --git a/src/OpenSpartan.Workshop/Core/UserContextManager.cs b/src/OpenSpartan.Workshop/Core/UserContextManager.cs index c95fa8f..f58b851 100644 --- a/src/OpenSpartan.Workshop/Core/UserContextManager.cs +++ b/src/OpenSpartan.Workshop/Core/UserContextManager.cs @@ -54,6 +54,8 @@ internal static class UserContextManager internal static XboxTicket XboxUserContext { get; set; } + internal static Dictionary currencyDefinitions = []; + internal static readonly JsonSerializerOptions SerializerOptions = new() { PropertyNameCaseInsensitive = true, @@ -1654,7 +1656,7 @@ internal static async Task>> G internal static async Task> ExtractCurrencyRewards(int rank, int playerRank, IEnumerable currencyItems, bool isFree) { - List rewardContainers = new(); + List rewardContainers = []; foreach (var currencyReward in currencyItems) { @@ -1663,9 +1665,19 @@ internal static async Task> ExtractCurrencyRewards(i Ranks = new Tuple(rank, playerRank), IsFree = isFree, ItemValue = currencyReward.Amount, - CurrencyDetails = await GetInGameCurrency(currencyReward.CurrencyPath) }; + if (!currencyDefinitions.Any(x => x.Key == currencyReward.CurrencyPath)) + { + var currencyDetails = await GetInGameCurrency(currencyReward.CurrencyPath); + container.CurrencyDetails = currencyDetails; + currencyDefinitions.Add(currencyReward.CurrencyPath, currencyDetails); + } + else + { + container.CurrencyDetails = currencyDefinitions[currencyReward.CurrencyPath]; + } + if (container.CurrencyDetails != null) { switch (container.CurrencyDetails.Id.ToLower(CultureInfo.InvariantCulture)) @@ -1688,19 +1700,9 @@ internal static async Task> ExtractCurrencyRewards(i } string currencyImageLocation = GetCurrencyImageLocation(container.Type); - container.ImagePath = currencyImageLocation; - string qualifiedImagePath = Path.Combine(Configuration.AppDataDirectory, "imagecache", currencyImageLocation); - - EnsureDirectoryExists(qualifiedImagePath); - - var rankImage = await SafeAPICall(async () => await HaloClient.GameCmsGetImage(currencyImageLocation)); - - if (rankImage.Result != null && rankImage.Response.Code == 200) - { - await WriteImageToFileAsync(qualifiedImagePath, rankImage.Result); - } + await DownloadAndSetImage(container.ImagePath, Path.Combine(Configuration.AppDataDirectory, "imagecache", currencyImageLocation)); } rewardContainers.Add(container); From 1fcadf0b7ab86cdfab1e271fce25f4822d8ae0af Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Thu, 11 Jul 2024 09:56:06 -0700 Subject: [PATCH 14/18] Simplify call --- .../Core/UserContextManager.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/OpenSpartan.Workshop/Core/UserContextManager.cs b/src/OpenSpartan.Workshop/Core/UserContextManager.cs index f58b851..5af3659 100644 --- a/src/OpenSpartan.Workshop/Core/UserContextManager.cs +++ b/src/OpenSpartan.Workshop/Core/UserContextManager.cs @@ -54,7 +54,7 @@ internal static class UserContextManager internal static XboxTicket XboxUserContext { get; set; } - internal static Dictionary currencyDefinitions = []; + internal static Dictionary CurrencyDefinitions = []; internal static readonly JsonSerializerOptions SerializerOptions = new() { @@ -1667,17 +1667,14 @@ internal static async Task> ExtractCurrencyRewards(i ItemValue = currencyReward.Amount, }; - if (!currencyDefinitions.Any(x => x.Key == currencyReward.CurrencyPath)) + if (!CurrencyDefinitions.TryGetValue(currencyReward.CurrencyPath, out var currencyDetails)) { - var currencyDetails = await GetInGameCurrency(currencyReward.CurrencyPath); - container.CurrencyDetails = currencyDetails; - currencyDefinitions.Add(currencyReward.CurrencyPath, currencyDetails); - } - else - { - container.CurrencyDetails = currencyDefinitions[currencyReward.CurrencyPath]; + currencyDetails = await GetInGameCurrency(currencyReward.CurrencyPath); + CurrencyDefinitions.Add(currencyReward.CurrencyPath, currencyDetails); } + container.CurrencyDetails = currencyDetails; + if (container.CurrencyDetails != null) { switch (container.CurrencyDetails.Id.ToLower(CultureInfo.InvariantCulture)) From f0f88eccec271bd50bd1eabe43d2f95809133a81 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Thu, 11 Jul 2024 11:06:51 -0700 Subject: [PATCH 15/18] Cleanup, and making sure that we're creating the appropriate folders --- .../Core/UserContextManager.cs | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/OpenSpartan.Workshop/Core/UserContextManager.cs b/src/OpenSpartan.Workshop/Core/UserContextManager.cs index 5af3659..805498c 100644 --- a/src/OpenSpartan.Workshop/Core/UserContextManager.cs +++ b/src/OpenSpartan.Workshop/Core/UserContextManager.cs @@ -368,6 +368,10 @@ private static async Task DownloadAndSetImage(string serviceImagePath, string lo // Check if the image retrieval was successful if (image != null && image.Result != null && image.Response.Code == 200) { + // In case the folder does not exist, make sure we create it. + FileInfo file = new(localImagePath); + file.Directory.Create(); + await System.IO.File.WriteAllBytesAsync(localImagePath, image.Result); if (setImageAction != null) @@ -486,30 +490,13 @@ internal static async Task PopulateDecorationData() try { string backgroundPath = SettingsViewModel.Instance.Settings.HeaderImagePath; + string cachedImagePath = Path.Combine(Configuration.AppDataDirectory, "imagecache", backgroundPath); - // Get initial service record details - string qualifiedBackgroundImagePath = Path.Combine(Configuration.AppDataDirectory, "imagecache", backgroundPath); - - if (!System.IO.File.Exists(qualifiedBackgroundImagePath)) - { - var backgroundImageResult = await SafeAPICall(async () => - { - return await HaloClient.GameCmsGetImage(backgroundPath); - }); - - if (backgroundImageResult.Result != null && backgroundImageResult.Response.Code == 200) - { - // Let's make sure that we create the directory if it does not exist. - FileInfo file = new(qualifiedBackgroundImagePath); - file.Directory.Create(); - - await System.IO.File.WriteAllBytesAsync(qualifiedBackgroundImagePath, backgroundImageResult.Result); - } - } + await DownloadAndSetImage(backgroundPath, cachedImagePath); await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { - HomeViewModel.Instance.SeasonalBackground = qualifiedBackgroundImagePath; + HomeViewModel.Instance.SeasonalBackground = cachedImagePath; }); return true; From 14d234aa2f083535e5b6a7f5a806ff0beec29ab9 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Thu, 11 Jul 2024 11:27:00 -0700 Subject: [PATCH 16/18] Fixes to testing bugs --- .../Core/UserContextManager.cs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/OpenSpartan.Workshop/Core/UserContextManager.cs b/src/OpenSpartan.Workshop/Core/UserContextManager.cs index 805498c..aafb72c 100644 --- a/src/OpenSpartan.Workshop/Core/UserContextManager.cs +++ b/src/OpenSpartan.Workshop/Core/UserContextManager.cs @@ -1293,6 +1293,33 @@ internal static async Task PopulateMedalData() if (medals == null) { LogEngine.Log("Locally stored medals not found.", LogSeverity.Warning); + + var coreStats = HomeViewModel.Instance.ServiceRecord?.CoreStats; + var serviceRecordMedals = coreStats?.Medals; + + if (serviceRecordMedals != null && serviceRecordMedals.Count > 0) + { + medals = serviceRecordMedals; + LogEngine.Log("Using medals from the local service record.", LogSeverity.Info); + } + else + { + LogEngine.Log("Re-acquiring service record to get medal data.", LogSeverity.Info); + if (await PopulateServiceRecordData()) + { + serviceRecordMedals = coreStats?.Medals; + if (serviceRecordMedals != null && serviceRecordMedals.Count > 0) + { + medals = serviceRecordMedals; + LogEngine.Log("Using medals from the local service record after re-acquiring.", LogSeverity.Info); + } + else + { + LogEngine.Log("Medals could not be populated because the service record contents are empty.", LogSeverity.Warning); + } + } + } + return false; } @@ -1817,7 +1844,6 @@ internal static async Task PopulateExchangeData() await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { - ExchangeViewModel.Instance.ExchangeItems = new ObservableCollection(); ExchangeViewModel.Instance.ExchangeLoadingState = MetadataLoadingState.Loading; }); @@ -1828,6 +1854,12 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => if (exchangeOfferings != null && exchangeOfferings.Result != null) { + // Only clear out exchange items if the previous call to get them from the store succeeded. + await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => + { + ExchangeViewModel.Instance.ExchangeItems = []; + }); + _ = Task.Run(async () => { await ProcessExchangeItems(exchangeOfferings.Result, ExchangeCancellationTracker.Token); From 15aa42582bde2ec99fbaedb5abba9e0df9f20f71 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Thu, 11 Jul 2024 15:03:36 -0700 Subject: [PATCH 17/18] Cleanup --- .../Core/UserContextManager.cs | 157 ++++++++++-------- .../Models/MetadataLoadingState.cs | 3 +- .../ViewModels/MatchesViewModel.cs | 1 + .../Views/ExchangeView.xaml.cs | 9 +- 4 files changed, 93 insertions(+), 77 deletions(-) diff --git a/src/OpenSpartan.Workshop/Core/UserContextManager.cs b/src/OpenSpartan.Workshop/Core/UserContextManager.cs index aafb72c..95e0e46 100644 --- a/src/OpenSpartan.Workshop/Core/UserContextManager.cs +++ b/src/OpenSpartan.Workshop/Core/UserContextManager.cs @@ -101,7 +101,7 @@ internal static async Task InitializePublicClientApplicati .WithDefaultRedirectUri() .WithAuthority(AadAuthorityAudience.PersonalMicrosoftAccount); - if ((bool)SettingsViewModel.Instance.UseBroker) + if (SettingsViewModel.Instance.UseBroker) { BrokerOptions options = new(BrokerOptions.OperatingSystems.Windows) { @@ -738,7 +738,7 @@ private static async Task await HaloClient.StatsGetMatchStats(matchId)); if (matchStats == null || matchStats.Result == null) { - LogEngine.Log($"[{completionProgress:#.00}%] [Error] Getting match stats failed for {matchId}.", LogSeverity.Error); + LogEngine.Log($"[{completionProgress:#.00}%] [Error] Getting match stats from the Halo Infinite API failed for {matchId}.", LogSeverity.Error); return null; } @@ -750,7 +750,7 @@ private static async Task await HaloClient.SkillGetMatchPlayerResult(matchId, targetPlayers!)); if (playerStatsSnapshot == null || playerStatsSnapshot.Result == null || playerStatsSnapshot.Result.Value == null) { - LogEngine.Log($"Could not obtain player stats for match {matchId}. Requested {targetPlayers.Count} XUIDs.", LogSeverity.Error); + LogEngine.Log($"Could not obtain player stats from the Halo Infinite API for match {matchId}. Requested {targetPlayers.Count} XUIDs.", LogSeverity.Error); return null; } @@ -1290,37 +1290,38 @@ internal static async Task PopulateMedalData() // Retrieve locally stored medals var medals = DataHandler.GetMedals(); - if (medals == null) + if (medals == null || medals.Count == 0) { LogEngine.Log("Locally stored medals not found.", LogSeverity.Warning); - var coreStats = HomeViewModel.Instance.ServiceRecord?.CoreStats; - var serviceRecordMedals = coreStats?.Medals; - - if (serviceRecordMedals != null && serviceRecordMedals.Count > 0) + if (HomeViewModel.Instance.ServiceRecord != null && HomeViewModel.Instance.ServiceRecord.CoreStats != null + && HomeViewModel.Instance.ServiceRecord.CoreStats.Medals != null && HomeViewModel.Instance.ServiceRecord.CoreStats.Medals.Count > 0) { - medals = serviceRecordMedals; - LogEngine.Log("Using medals from the local service record.", LogSeverity.Info); + medals = HomeViewModel.Instance.ServiceRecord.CoreStats.Medals; + LogEngine.Log("Instead of using medals from the database, using medals from the local service record.", LogSeverity.Info); } else { LogEngine.Log("Re-acquiring service record to get medal data.", LogSeverity.Info); - if (await PopulateServiceRecordData()) + var serviceRecordResult = await PopulateServiceRecordData(); + if (serviceRecordResult) { - serviceRecordMedals = coreStats?.Medals; - if (serviceRecordMedals != null && serviceRecordMedals.Count > 0) + if (HomeViewModel.Instance.ServiceRecord != null && HomeViewModel.Instance.ServiceRecord.CoreStats != null && HomeViewModel.Instance.ServiceRecord.CoreStats.Medals != null && HomeViewModel.Instance.ServiceRecord.CoreStats.Medals.Count > 0) { - medals = serviceRecordMedals; - LogEngine.Log("Using medals from the local service record after re-acquiring.", LogSeverity.Info); + medals = HomeViewModel.Instance.ServiceRecord.CoreStats.Medals; + LogEngine.Log("Instead of using medals from the database, using medals from the local service record after re-acquiring.", LogSeverity.Info); } else { LogEngine.Log("Medals could not be populated because the service record contents are empty.", LogSeverity.Warning); + return false; } } + else + { + return false; + } } - - return false; } // Join locally stored medals with metadata to create compound medals @@ -1366,35 +1367,33 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => if (spriteContent != null) { // Decode sprite content into SKBitmap - using (MemoryStream ms = new MemoryStream(spriteContent)) + using (MemoryStream ms = new(spriteContent)) { SkiaSharp.SKBitmap bmp = SkiaSharp.SKBitmap.Decode(ms); - using (var pixmap = bmp.PeekPixels()) + using var pixmap = bmp.PeekPixels(); + // Download and save medal images + foreach (var medal in MedalMetadata.Medals) { - // Download and save medal images - foreach (var medal in MedalMetadata.Medals) - { - string medalImagePath = Path.Combine(qualifiedMedalPath, $"{medal.NameId}.png"); - EnsureDirectoryExists(medalImagePath); + string medalImagePath = Path.Combine(qualifiedMedalPath, $"{medal.NameId}.png"); + EnsureDirectoryExists(medalImagePath); - // Skip writing if file already exists - if (!System.IO.File.Exists(medalImagePath)) + // Skip writing if file already exists + if (!System.IO.File.Exists(medalImagePath)) + { + // Calculate position and size of medal sprite + var row = (int)Math.Floor(medal.SpriteIndex / 16.0); + var column = (int)(medal.SpriteIndex % 16.0); + SkiaSharp.SKRectI rectI = SkiaSharp.SKRectI.Create(column * 256, row * 256, 256, 256); + + // Extract subset of pixmap and encode as PNG + var subset = pixmap.ExtractSubset(rectI); + using (var data = subset.Encode(SkiaSharp.SKPngEncoderOptions.Default)) { - // Calculate position and size of medal sprite - var row = (int)Math.Floor(medal.SpriteIndex / 16.0); - var column = (int)(medal.SpriteIndex % 16.0); - SkiaSharp.SKRectI rectI = SkiaSharp.SKRectI.Create(column * 256, row * 256, 256, 256); - - // Extract subset of pixmap and encode as PNG - var subset = pixmap.ExtractSubset(rectI); - using (var data = subset.Encode(SkiaSharp.SKPngEncoderOptions.Default)) - { - await System.IO.File.WriteAllBytesAsync(medalImagePath, data.ToArray()); - } - - // Log successful write - LogEngine.Log($"Wrote medal to file: {medalImagePath}"); + await System.IO.File.WriteAllBytesAsync(medalImagePath, data.ToArray()); } + + // Log successful write + LogEngine.Log($"Wrote medal to file: {medalImagePath}"); } } } @@ -1446,10 +1445,13 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => })).Result; } - public static async Task PopulateBattlePassData(CancellationToken cancellationToken) + public static async Task PopulateBattlePassData() { await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => BattlePassViewModel.Instance.BattlePassLoadingState = MetadataLoadingState.Loading); + await BattlePassLoadingCancellationTracker.CancelAsync(); + BattlePassLoadingCancellationTracker = new CancellationTokenSource(); + // Using this as a reference point for extra rituals and excluded events. var settings = SettingsManager.LoadSettings(); @@ -1481,7 +1483,7 @@ public static async Task PopulateBattlePassData(CancellationToken cancella // Let's get the data for each of the operations. foreach (var operation in operations.OperationRewardTracks) { - cancellationToken.ThrowIfCancellationRequested(); + BattlePassLoadingCancellationTracker.Token.ThrowIfCancellationRequested(); await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => BattlePassViewModel.Instance.BattlePassLoadingParameter = operation.RewardTrackPath); var compoundOperation = await ProcessOperation(operation, seasonRewardTracks); @@ -1496,7 +1498,7 @@ public static async Task PopulateBattlePassData(CancellationToken cancella { // Tell the user that the operations are currently being loaded by changing the // loading parameter to the reward track path. - cancellationToken.ThrowIfCancellationRequested(); + BattlePassLoadingCancellationTracker.Token.ThrowIfCancellationRequested(); await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => BattlePassViewModel.Instance.BattlePassLoadingParameter = eventEntry.RewardTrackPath); OperationCompoundModel compoundEvent = new() @@ -1873,6 +1875,11 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { LogEngine.Log($"Failed to finish updating The Exchange content. Reason: {ex.Message}"); + await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => + { + ExchangeViewModel.Instance.ExchangeLoadingState = MetadataLoadingState.Completed; + }); + return false; } } @@ -2022,41 +2029,51 @@ await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => }); // Concurrently populate MatchRecordsData and BattlePassData with other tasks - await Task.WhenAll( - PopulateMatchRecordsData().ContinueWith(async t => + Parallel.Invoke( + async () => await PopulateMatchRecordsData().ContinueWith(async t => { - if (t.Result) + if (await t) { - await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => - { - MatchesViewModel.Instance.MatchLoadingState = MetadataLoadingState.Completed; - MatchesViewModel.Instance.MatchLoadingParameter = string.Empty; - }); + LogEngine.Log("Successfully populated the match data from within the app bootstrap sequence."); } - }, TaskScheduler.FromCurrentSynchronizationContext()), - PopulateBattlePassData(BattlePassLoadingCancellationTracker.Token).ContinueWith(async t => + else if (t.IsFaulted) + { + LogEngine.Log("Could not populate the match data from within the app bootstrap sequence."); + } + + // Right now, regardless of result I want to make sure that we reset + // the completion state. + await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => + { + MatchesViewModel.Instance.MatchLoadingState = MetadataLoadingState.Completed; + MatchesViewModel.Instance.MatchLoadingParameter = string.Empty; + }); + }, TaskScheduler.Current), + async () => await PopulateBattlePassData().ContinueWith(async t => { - if (t.IsCompletedSuccessfully) + if (await t) { - await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => - { - BattlePassViewModel.Instance.BattlePassLoadingState = MetadataLoadingState.Completed; - }); + LogEngine.Log("Successfully populated the battle pass data from within the app bootstrap sequence."); } else if (t.IsFaulted) { - BattlePassLoadingCancellationTracker = new CancellationTokenSource(); + LogEngine.Log("Could not populate the battle pass data from within the app bootstrap sequence."); } - }, TaskScheduler.FromCurrentSynchronizationContext()), - PopulateCareerData(), - PopulateServiceRecordData(), - PopulateMedalData(), - PopulateExchangeData(), - PopulateCsrImages(), - PopulateSeasonCalendar(), - PopulateUserInventory(), - PopulateCustomizationData(), - PopulateDecorationData() + + await DispatcherWindow.DispatcherQueue.EnqueueAsync(() => + { + BattlePassViewModel.Instance.BattlePassLoadingState = MetadataLoadingState.Completed; + }); + }, TaskScheduler.Current), + async() => await PopulateCareerData(), + async () => await PopulateServiceRecordData(), + async () => await PopulateMedalData(), + async () => await PopulateExchangeData(), + async () => await PopulateCsrImages(), + async () => await PopulateSeasonCalendar(), + async () => await PopulateUserInventory(), + async () => await PopulateCustomizationData(), + async () => await PopulateDecorationData() ); return true; diff --git a/src/OpenSpartan.Workshop/Models/MetadataLoadingState.cs b/src/OpenSpartan.Workshop/Models/MetadataLoadingState.cs index deb8cb0..7d3e958 100644 --- a/src/OpenSpartan.Workshop/Models/MetadataLoadingState.cs +++ b/src/OpenSpartan.Workshop/Models/MetadataLoadingState.cs @@ -4,6 +4,7 @@ internal enum MetadataLoadingState { Calculating, Loading, - Completed + Completed, + Failed } } diff --git a/src/OpenSpartan.Workshop/ViewModels/MatchesViewModel.cs b/src/OpenSpartan.Workshop/ViewModels/MatchesViewModel.cs index 357a0e7..9cbcb4a 100644 --- a/src/OpenSpartan.Workshop/ViewModels/MatchesViewModel.cs +++ b/src/OpenSpartan.Workshop/ViewModels/MatchesViewModel.cs @@ -35,6 +35,7 @@ public string MatchLoadingString { MetadataLoadingState.Calculating => $"Calculating matches. Identified {MatchLoadingParameter} matches so far...", MetadataLoadingState.Loading => $"Loading match details. Currently processing {MatchLoadingParameter}...", + MetadataLoadingState.Failed => $"Failed processing match. {MatchLoadingParameter}", MetadataLoadingState.Completed => "Completed", _ => "NOOP - Never Seen", }; diff --git a/src/OpenSpartan.Workshop/Views/ExchangeView.xaml.cs b/src/OpenSpartan.Workshop/Views/ExchangeView.xaml.cs index 614f741..018b63b 100644 --- a/src/OpenSpartan.Workshop/Views/ExchangeView.xaml.cs +++ b/src/OpenSpartan.Workshop/Views/ExchangeView.xaml.cs @@ -28,13 +28,10 @@ private async void btnRefreshExchange_Click(object sender, RoutedEventArgs e) { var matchRecordsOutcome = await UserContextManager.PopulateExchangeData(); - if (matchRecordsOutcome) + await UserContextManager.DispatcherWindow.DispatcherQueue.EnqueueAsync(() => { - await UserContextManager.DispatcherWindow.DispatcherQueue.EnqueueAsync(() => - { - ExchangeViewModel.Instance.ExchangeLoadingState = MetadataLoadingState.Completed; - }); - } + ExchangeViewModel.Instance.ExchangeLoadingState = MetadataLoadingState.Completed; + }); } } } From 98f81712894d6726b361f632dcc5c950bb7cbd85 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Thu, 11 Jul 2024 16:08:10 -0700 Subject: [PATCH 18/18] Ensure that Exchange items render properly --- .../Converters/RewardTypeToStringConverter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OpenSpartan.Workshop/Converters/RewardTypeToStringConverter.cs b/src/OpenSpartan.Workshop/Converters/RewardTypeToStringConverter.cs index c10ede9..2341c28 100644 --- a/src/OpenSpartan.Workshop/Converters/RewardTypeToStringConverter.cs +++ b/src/OpenSpartan.Workshop/Converters/RewardTypeToStringConverter.cs @@ -23,7 +23,6 @@ public object Convert(object value, Type targetType, object parameter, string la case ItemClass.ChallengeReroll: return "Challenge Swap"; case ItemClass.StandardReward: - break; default: return type.ItemDetails.CommonData.Title.Value; }