From 3a0087f878015ea279cbaa42060a3b7ba23215ed Mon Sep 17 00:00:00 2001 From: he_is_the_cat <125207670+heisthecat31@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:07:08 +0100 Subject: [PATCH 01/33] Update to add better checking and quicker server checks --- Program.cs | 23 ++++++++++++++++++++--- Windows/LiveWindow/FriendsTab.xaml.cs | 2 +- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Program.cs b/Program.cs index 6c8319e..3dc5df6 100644 --- a/Program.cs +++ b/Program.cs @@ -655,9 +655,9 @@ public static void Main(string[] args, App app) private static void OnLeftGame(Frame obj) { - // Tell the Friends bot the user went offline - try { _ = FriendsTab.PushOffline(); } - catch (Exception e) { Console.WriteLine("Friends offline push error: " + e.Message); } + // Tell the Friends bot the user is now in the menu/transitioning + try { _ = FriendsTab.PushLobbyUpdate(null, null, "In Menu"); } + catch (Exception e) { Console.WriteLine("Friends menu push error: " + e.Message); } } @@ -894,6 +894,22 @@ private static async Task MainLoop() LogRow(LogType.Error, $"Error in fetch request.\n{ex}"); } + if (connectionState != lastConnectionState) + { + switch (connectionState) + { + case ConnectionState.Menu: + _ = FriendsTab.PushLobbyUpdate(null, null, "In Menu"); + break; + case ConnectionState.InLobby: + _ = FriendsTab.PushLobbyUpdate(null, null, "In Lobby"); + break; + case ConnectionState.NotConnected: + _ = FriendsTab.PushOffline(); + break; + } + } + lastConnectionState = connectionState; @@ -3747,6 +3763,7 @@ internal static void Quit() running = false; SparkClosing?.Invoke(); + try { _ = FriendsTab.PushOffline(); } catch { } SparkSettings.instance.Save(); if (closingWindow != null) diff --git a/Windows/LiveWindow/FriendsTab.xaml.cs b/Windows/LiveWindow/FriendsTab.xaml.cs index 3fd8939..79aaf25 100644 --- a/Windows/LiveWindow/FriendsTab.xaml.cs +++ b/Windows/LiveWindow/FriendsTab.xaml.cs @@ -153,7 +153,7 @@ private void StartPolling() { while (isRunning) { - await Task.Delay(20000); + await Task.Delay(2000); try { await PollFriends(); } catch { } } }); From 5497c2954d84dc575da52d245a1824a9d78dcc72 Mon Sep 17 00:00:00 2001 From: Brysonak Date: Mon, 27 Apr 2026 18:55:45 -0400 Subject: [PATCH 02/33] Make the updater use HttpClient instead of WebClient Changed all Webclient instances in the updater to HttpClient. Cleaned up code quality slightly --- Windows/LiveWindow/UpdateSpark.xaml.cs | 439 +++++++++++-------------- 1 file changed, 189 insertions(+), 250 deletions(-) diff --git a/Windows/LiveWindow/UpdateSpark.xaml.cs b/Windows/LiveWindow/UpdateSpark.xaml.cs index dbaf55e..5e3dd48 100644 --- a/Windows/LiveWindow/UpdateSpark.xaml.cs +++ b/Windows/LiveWindow/UpdateSpark.xaml.cs @@ -3,11 +3,13 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; using System.Windows; using System.Windows.Controls; using System.Diagnostics; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; @@ -15,6 +17,8 @@ namespace Spark { public partial class UpdateSparkControl : UserControl { + private static readonly HttpClient _httpClient = new HttpClient(); + private string _latestVersion = ""; private string _currentVersion = ""; private string _tempFolder = ""; @@ -32,6 +36,10 @@ public class ColorVersion public UpdateSparkControl() { InitializeComponent(); + + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Spark-Updater"); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json")); + Loaded += OnLoaded; ColorVersionDropdown.SelectionChanged += ColorVersionDropdown_SelectionChanged; } @@ -55,76 +63,66 @@ private void OnLoaded(object sender, RoutedEventArgs e) private string GetCurrentVersion() { - var version = Assembly.GetExecutingAssembly().GetName().Version; + Version version = Assembly.GetExecutingAssembly().GetName().Version; return $"{version.Major}.{version.Minor}.{version.Build}"; } -private async void CheckUpdateButton_Click(object sender, RoutedEventArgs e) -{ - CheckUpdateButton.IsEnabled = false; - StatusText.Text = "Checking for updates..."; - UpdateDetailsText.Text += $"[{DateTime.Now}] Checking for updates...\n"; - - try - { - using (var client = new WebClient()) + private async void CheckUpdateButton_Click(object sender, RoutedEventArgs e) { - client.Headers.Add("User-Agent", "Spark-Updater"); - client.Headers.Add("Accept", "application/vnd.github.v3+json"); - - // URL for the specific 'ignore' tag - string latestReleaseUrl = "https://api.github.com/repos/heisthecat31/Spark/releases/tags/ignore"; - - // Download the JSON - string json = await client.DownloadStringTaskAsync(latestReleaseUrl); - - // 1. CREATE the 'release' variable here - var release = JObject.Parse(json); - - // 2. NOW you can use it. - // We try to grab the "name" (Release Title) first. - string titleName = release["name"]?.ToString(); - - // If the title is empty, we fall back to the "tag_name" - if (string.IsNullOrWhiteSpace(titleName)) + CheckUpdateButton.IsEnabled = false; + StatusText.Text = "Checking for updates..."; + UpdateDetailsText.Text += $"[{DateTime.Now}] Checking for updates...\n"; + + try { - titleName = release["tag_name"]?.ToString(); - } + // URL for the specific tag 'ignore' tag, download it + string json = await _httpClient.GetStringAsync("https://api.github.com/repos/heisthecat31/Spark/releases/tags/ignore"); - // Remove 'v' prefix if it exists (just in case) - _latestVersion = titleName?.TrimStart('v') ?? "Unknown"; + // CREATE the 'release' variable here + JObject release = JObject.Parse(json); - LatestVersionText.Text = _latestVersion; - StatusText.Text = $"Version found: {_latestVersion}"; + string titleName = release["name"]?.ToString(); + // If the title is empty, we fall back to the "tag_name" + if (string.IsNullOrWhiteSpace(titleName)) + { + titleName = release["tag_name"]?.ToString(); + } - string releaseNotes = release["body"]?.ToString(); - UpdateDetailsText.Text += $"[{DateTime.Now}] Update found: {_latestVersion}\n"; + // Remove 'v' prefix if it exists (just in case) + _latestVersion = titleName?.TrimStart('v') ?? "Unknown"; + LatestVersionText.Text = _latestVersion; + StatusText.Text = $"Version found: {_latestVersion}"; - if (!string.IsNullOrEmpty(releaseNotes)) - { - UpdateDetailsText.Text += $"Release Notes:\n{releaseNotes}\n"; - } + string releaseNotes = release["body"]?.ToString(); + UpdateDetailsText.Text += $"[{DateTime.Now}] Update found: {_latestVersion}\n"; - await LoadAvailableVersions(release); - } - } - catch (Exception ex) - { + if (!string.IsNullOrEmpty(releaseNotes)) + { + UpdateDetailsText.Text += $"Release Notes:\n{releaseNotes}\n"; + } + + await LoadAvailableVersions(release); + } + catch (HttpRequestException ex) when (ex.Message.Contains("404")) + { StatusText.Text = "Error: Tag not found or API error."; - UpdateDetailsText.Text += $"[{DateTime.Now}] Error: {ex.Message}\n"; - - if (ex.Message.Contains("404")) { - UpdateDetailsText.Text += "Tip: Check that the tag 'ignore' exists in your GitHub Releases.\n"; + UpdateDetailsText.Text += $"[{DateTime.Now}] Error: {ex.Message}\n"; + UpdateDetailsText.Text += "Tip: Check that the tag 'ignore' exists in your GitHub Releases.\n"; + ColorVersionDropdown.IsEnabled = false; + DownloadUpdateButton.IsEnabled = false; + } + catch (Exception ex) + { + StatusText.Text = "Error checking for updates."; + UpdateDetailsText.Text += $"[{DateTime.Now}] Error: {ex.Message}\n"; + ColorVersionDropdown.IsEnabled = false; + DownloadUpdateButton.IsEnabled = false; + } + finally + { + CheckUpdateButton.IsEnabled = true; + } } - - ColorVersionDropdown.IsEnabled = false; - DownloadUpdateButton.IsEnabled = false; - } - finally - { - CheckUpdateButton.IsEnabled = true; - } -} private async Task LoadAvailableVersions(JObject release) { @@ -133,24 +131,23 @@ private async Task LoadAvailableVersions(JObject release) try { - var assets = release["assets"] as JArray; + JArray assets = release["assets"] as JArray; if (assets != null) { - foreach (var asset in assets) + foreach (JToken asset in assets) { string name = asset["name"]?.ToString(); string downloadUrl = asset["browser_download_url"]?.ToString(); // Look for Spark theme files but exclude SparkTTSCache.zip - if (name != null && downloadUrl != null && - name.StartsWith("Spark", StringComparison.OrdinalIgnoreCase) && + if (name != null && downloadUrl != null && + name.StartsWith("Spark", StringComparison.OrdinalIgnoreCase) && name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) && !name.Equals("SparkTTSCache.zip", StringComparison.OrdinalIgnoreCase)) { - string displayName = GetDisplayName(name); _availableVersions.Add(new ColorVersion { - DisplayName = displayName, + DisplayName = GetDisplayName(name), FileName = name, DownloadUrl = downloadUrl }); @@ -160,52 +157,35 @@ private async Task LoadAvailableVersions(JObject release) if (_availableVersions.Count > 0) { - Dispatcher.Invoke(() => - { - ColorVersionDropdown.ItemsSource = _availableVersions; - ColorVersionDropdown.DisplayMemberPath = "DisplayName"; - ColorVersionDropdown.SelectedValuePath = "DownloadUrl"; + ColorVersionDropdown.ItemsSource = _availableVersions; + ColorVersionDropdown.DisplayMemberPath = "DisplayName"; + ColorVersionDropdown.SelectedValuePath = "DownloadUrl"; - var defaultVersion = _availableVersions.FirstOrDefault(v => - v.FileName.Equals("Spark.zip", StringComparison.OrdinalIgnoreCase)); + ColorVersion defaultVersion = _availableVersions.FirstOrDefault(v => + v.FileName.Equals("Spark.zip", StringComparison.OrdinalIgnoreCase)); - if (defaultVersion != null) - { - ColorVersionDropdown.SelectedItem = defaultVersion; - } - else - { - ColorVersionDropdown.SelectedIndex = 0; - } - - ColorVersionDropdown.IsEnabled = true; - DownloadUpdateButton.IsEnabled = true; + ColorVersionDropdown.SelectedItem = defaultVersion ?? _availableVersions[0]; + ColorVersionDropdown.IsEnabled = true; + DownloadUpdateButton.IsEnabled = true; - UpdateDetailsText.Text += $"[{DateTime.Now}] Found {_availableVersions.Count} theme version(s):\n"; - foreach (var version in _availableVersions) - { - UpdateDetailsText.Text += $"[{DateTime.Now}] • {version.DisplayName} ({version.FileName})\n"; - } - }); + UpdateDetailsText.Text += $"[{DateTime.Now}] Found {_availableVersions.Count} theme version(s):\n"; + foreach (ColorVersion version in _availableVersions) + { + UpdateDetailsText.Text += $"[{DateTime.Now}] • {version.DisplayName} ({version.FileName})\n"; + } } else { - Dispatcher.Invoke(() => - { - StatusText.Text = "No theme versions found in release"; - ColorVersionDropdown.IsEnabled = false; - DownloadUpdateButton.IsEnabled = false; - UpdateDetailsText.Text += $"[{DateTime.Now}] Warning: No Spark*.zip theme files found in release\n"; - }); + StatusText.Text = "No theme versions found in release."; + ColorVersionDropdown.IsEnabled = false; + DownloadUpdateButton.IsEnabled = false; + UpdateDetailsText.Text += $"[{DateTime.Now}] Warning: No Spark*.zip theme files found in release\n"; } } catch (Exception ex) { - Dispatcher.Invoke(() => - { - StatusText.Text = $"Error loading versions: {ex.Message}"; - UpdateDetailsText.Text += $"[{DateTime.Now}] Error loading versions: {ex.Message}\n"; - }); + StatusText.Text = $"Error loading versions: {ex.Message}"; + UpdateDetailsText.Text += $"[{DateTime.Now}] Error loading versions: {ex.Message}\n"; } } @@ -215,84 +195,89 @@ private string GetDisplayName(string fileName) return "Default Theme"; string baseName = Path.GetFileNameWithoutExtension(fileName); - + if (baseName.StartsWith("Spark", StringComparison.OrdinalIgnoreCase)) { string themeName = baseName.Substring(5); - if (string.IsNullOrWhiteSpace(themeName)) - return "Default Theme"; - - return $"{themeName} Theme"; + return string.IsNullOrWhiteSpace(themeName) ? "Default Theme" : $"{themeName} Theme"; } - + return baseName; } private void ColorVersionDropdown_SelectionChanged(object sender, SelectionChangedEventArgs e) { - if (ColorVersionDropdown.SelectedItem != null) + if (ColorVersionDropdown.SelectedItem is ColorVersion selected) { - var selectedVersion = (ColorVersion)ColorVersionDropdown.SelectedItem; - _selectedDownloadUrl = selectedVersion.DownloadUrl; - UpdateDetailsText.Text += $"[{DateTime.Now}] Selected: {selectedVersion.DisplayName}\n"; + _selectedDownloadUrl = selected.DownloadUrl; + UpdateDetailsText.Text += $"[{DateTime.Now}] Selected: {selected.DisplayName}\n"; } } private async void DownloadUpdateButton_Click(object sender, RoutedEventArgs e) { - if (ColorVersionDropdown.SelectedItem == null) + if (ColorVersionDropdown.SelectedItem is not ColorVersion selectedVersion) { StatusText.Text = "Please select a theme version first"; return; } - var selectedVersion = (ColorVersion)ColorVersionDropdown.SelectedItem; + DownloadUpdateButton.IsEnabled = false; + CheckUpdateButton.IsEnabled = false; + ColorVersionDropdown.IsEnabled = false; + UpdateProgressBar.Visibility = Visibility.Visible; + UpdateProgressBar.Value = 0; + + StatusText.Text = $"Downloading {selectedVersion.DisplayName}..."; + UpdateDetailsText.Text += $"[{DateTime.Now}] Starting download of {selectedVersion.DisplayName}...\n"; + + string tempFilePath = Path.Combine(_tempFolder, selectedVersion.FileName); try { - DownloadUpdateButton.IsEnabled = false; - CheckUpdateButton.IsEnabled = false; - ColorVersionDropdown.IsEnabled = false; - UpdateProgressBar.Visibility = Visibility.Visible; - UpdateProgressBar.Value = 0; + await DownloadFileWithProgressAsync(selectedVersion.DownloadUrl, tempFilePath, (pct) => + { + UpdateProgressBar.Value = pct; + StatusText.Text = $"Downloading {selectedVersion.DisplayName}: {pct}%"; + }); - StatusText.Text = $"Downloading {selectedVersion.DisplayName}..."; - UpdateDetailsText.Text += $"[{DateTime.Now}] Starting download of {selectedVersion.DisplayName}...\n"; + StatusText.Text = "Download complete. Extracting and installing..."; + UpdateDetailsText.Text += $"[{DateTime.Now}] Download complete. Extracting...\n"; - string tempFilePath = Path.Combine(_tempFolder, selectedVersion.FileName); + await Task.Run(() => InstallUpdate(tempFilePath, selectedVersion.FileName)); + } + catch (Exception ex) + { + StatusText.Text = $"Error: {ex.Message}"; + UpdateDetailsText.Text += $"[{DateTime.Now}] Error downloading {selectedVersion.DisplayName}: {ex.Message}\n"; + ResetButtons(); + } + } - using (var client = new WebClient()) - { - client.Headers.Add("User-Agent", "Spark-Updater"); - - client.DownloadProgressChanged += (s, args) => - { - Dispatcher.Invoke(() => - { - UpdateProgressBar.Value = args.ProgressPercentage; - StatusText.Text = $"Downloading {selectedVersion.DisplayName}: {args.ProgressPercentage}%"; - }); - }; + private async Task DownloadFileWithProgressAsync(string url, string destPath, Action onProgress, CancellationToken ct = default) + { + using HttpResponseMessage response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct); + response.EnsureSuccessStatusCode(); - await client.DownloadFileTaskAsync(new Uri(_selectedDownloadUrl), tempFilePath); + long? totalBytes = response.Content.Headers.ContentLength; - Dispatcher.Invoke(() => - { - StatusText.Text = "Download complete. Extracting and installing..."; - UpdateDetailsText.Text += $"[{DateTime.Now}] Download complete. Extracting...\n"; - }); + await using Stream contentStream = await response.Content.ReadAsStreamAsync(ct); + await using FileStream fileStream = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true); - await Task.Run(() => InstallUpdate(tempFilePath, selectedVersion.FileName)); - } - } - catch (Exception ex) + byte[] buffer = new byte[8192]; + long bytesRead = 0; + int read; + + while ((read = await contentStream.ReadAsync(buffer, ct)) > 0) { - Dispatcher.Invoke(() => + await fileStream.WriteAsync(buffer.AsMemory(0, read), ct); + bytesRead += read; + + if (totalBytes.HasValue) { - StatusText.Text = $"Error: {ex.Message}"; - UpdateDetailsText.Text += $"[{DateTime.Now}] Error downloading {selectedVersion.DisplayName}: {ex.Message}\n"; - ResetButtons(); - }); + int pct = (int)(bytesRead * 100 / totalBytes.Value); + Dispatcher.Invoke(() => onProgress(pct)); + } } } @@ -315,6 +300,7 @@ private void InstallUpdate(string zipFilePath, string originalFileName) string currentExe = Process.GetCurrentProcess().MainModule.FileName; string targetFolder = Path.GetDirectoryName(currentExe); + string actualSourceFolder = FindActualSourceFolder(extractPath); Dispatcher.Invoke(() => { @@ -322,10 +308,7 @@ private void InstallUpdate(string zipFilePath, string originalFileName) StatusText.Text = "Creating update script..."; }); - string actualSourceFolder = FindActualSourceFolder(extractPath); - string batchFile = Path.Combine(_tempFolder, "update_spark.bat"); - string batchContent = $@" @echo off setlocal enabledelayedexpansion @@ -382,36 +365,23 @@ REM Delete this script and the temp extraction (goto) 2>nul & del ""%~f0"" exit "; - File.WriteAllText(batchFile, batchContent); Dispatcher.Invoke(() => { UpdateDetailsText.Text += $"[{DateTime.Now}] Batch file created\n"; StatusText.Text = "Starting update..."; - }); - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = batchFile, - WindowStyle = ProcessWindowStyle.Normal, - UseShellExecute = true, - WorkingDirectory = _tempFolder - }; - - Dispatcher.Invoke(() => - { - UpdateDetailsText.Text += $"[{DateTime.Now}] Launching update...\n"; - - Process.Start(psi); - - Task.Delay(500).ContinueWith(t => + Process.Start(new ProcessStartInfo { - Dispatcher.Invoke(() => - { - Process.GetCurrentProcess().Kill(); - }); + FileName = batchFile, + WindowStyle = ProcessWindowStyle.Normal, + UseShellExecute = true, + WorkingDirectory = _tempFolder }); + + + Program.Quit(); }); } catch (Exception ex) @@ -436,9 +406,7 @@ private string FindActualSourceFolder(string extractPath) { string folder = Path.GetDirectoryName(files[0]); if (!string.IsNullOrEmpty(folder)) - { return folder; - } } } catch (Exception ex) @@ -454,72 +422,46 @@ private string FindActualSourceFolder(string extractPath) private async void DownloadTTSCacheButton_Click(object sender, RoutedEventArgs e) { + DownloadTTSCacheButton.IsEnabled = false; + TTSCacheStatus.Text = "Downloading TTS Cache..."; + try { - DownloadTTSCacheButton.IsEnabled = false; - TTSCacheStatus.Text = "Downloading TTS Cache..."; + string json = await _httpClient.GetStringAsync("https://api.github.com/repos/heisthecat31/Spark/releases/latest"); + JObject release = JObject.Parse(json); - string apiUrl = "https://api.github.com/repos/heisthecat31/Spark/releases/latest"; - using (var client = new WebClient()) - { - client.Headers.Add("User-Agent", "Spark-Updater"); - string json = await client.DownloadStringTaskAsync(apiUrl); - var release = JObject.Parse(json); + JArray assets = release["assets"] as JArray; + string ttsCacheUrl = assets? + .FirstOrDefault(a => a["name"]?.ToString().Equals("SparkTTSCache.zip", StringComparison.OrdinalIgnoreCase) == true) + ?["browser_download_url"]?.ToString(); - var assets = release["assets"] as JArray; - string ttsCacheUrl = ""; + if (string.IsNullOrEmpty(ttsCacheUrl)) + throw new Exception("SparkTTSCache.zip not found in release assets."); - if (assets != null) - { - foreach (var asset in assets) - { - string name = asset["name"]?.ToString(); - if (name != null && name.Equals("SparkTTSCache.zip", StringComparison.OrdinalIgnoreCase)) - { - ttsCacheUrl = asset["browser_download_url"]?.ToString(); - break; - } - } - } + // Use the custom cache folder configured in settings, or the default Spark directory fallback + string ttsCacheFolder = TTSController.CacheFolder; - if (string.IsNullOrEmpty(ttsCacheUrl)) - { - throw new Exception("SparkTTSCache.zip not found in release assets"); - } + if (Directory.Exists(ttsCacheFolder)) + Directory.Delete(ttsCacheFolder, true); - // Use the custom cache folder configured in settings, or the default Spark directory fallback - string ttsCacheFolder = TTSController.CacheFolder; + Directory.CreateDirectory(ttsCacheFolder); - if (Directory.Exists(ttsCacheFolder)) - { - Directory.Delete(ttsCacheFolder, true); - } - - Directory.CreateDirectory(ttsCacheFolder); + string tempTtsZip = Path.Combine(ttsCacheFolder, "SparkTTSCache.zip"); - string tempTtsZip = Path.Combine(ttsCacheFolder, "SparkTTSCache.zip"); + await DownloadFileWithProgressAsync(ttsCacheUrl, tempTtsZip, _ => { }); - await client.DownloadFileTaskAsync(new Uri(ttsCacheUrl), tempTtsZip); + ZipFile.ExtractToDirectory(tempTtsZip, ttsCacheFolder); + File.Delete(tempTtsZip); - ZipFile.ExtractToDirectory(tempTtsZip, ttsCacheFolder); - File.Delete(tempTtsZip); - - Dispatcher.Invoke(() => - { - TTSCacheStatus.Text = "TTS Cache downloaded successfully!"; - UpdateDetailsText.Text += $"[{DateTime.Now}] TTS Cache downloaded to: {ttsCacheFolder}\n"; - new MessageBox($"Success: TTS Cache downloaded and extracted to:\n{ttsCacheFolder}").Show(); - }); - } + TTSCacheStatus.Text = "TTS Cache downloaded successfully!"; + UpdateDetailsText.Text += $"[{DateTime.Now}] TTS Cache downloaded to: {ttsCacheFolder}\n"; + new MessageBox($"Success: TTS Cache downloaded and extracted to:\n{ttsCacheFolder}").Show(); } catch (Exception ex) { - Dispatcher.Invoke(() => - { - TTSCacheStatus.Text = $"Error: {ex.Message}"; - UpdateDetailsText.Text += $"[{DateTime.Now}] TTS Cache error: {ex.Message}\n"; - new MessageBox($"Error downloading TTS Cache: {ex.Message}").Show(); - }); + TTSCacheStatus.Text = $"Error: {ex.Message}"; + UpdateDetailsText.Text += $"[{DateTime.Now}] TTS Cache error: {ex.Message}\n"; + new MessageBox($"Error downloading TTS Cache: {ex.Message}").Show(); } finally { @@ -529,45 +471,42 @@ private async void DownloadTTSCacheButton_Click(object sender, RoutedEventArgs e private async void DownloadHapticsFixButton_Click(object sender, RoutedEventArgs e) { - try + if (SparkSettings.instance == null || string.IsNullOrEmpty(SparkSettings.instance.echoVRPath)) { - if (SparkSettings.instance == null || string.IsNullOrEmpty(SparkSettings.instance.echoVRPath)) - { - new MessageBox("Error: EchoVR Path is not set in Spark Settings. Please set it in the main settings first.").Show(); - return; - } + new MessageBox("Error: EchoVR Path is not set in Spark Settings. Please set it in the main settings first.").Show(); + return; + } - string echoDir = Path.GetDirectoryName(SparkSettings.instance.echoVRPath); - if (!Directory.Exists(echoDir)) - { - new MessageBox($"Error: EchoVR directory not found at:\n{echoDir}").Show(); - return; - } + string echoDir = Path.GetDirectoryName(SparkSettings.instance.echoVRPath); + if (!Directory.Exists(echoDir)) + { + new MessageBox($"Error: EchoVR directory not found at:\n{echoDir}").Show(); + return; + } - DownloadHapticsFixButton.IsEnabled = false; - HapticsFixStatus.Text = "Downloading Haptics Fix..."; - UpdateDetailsText.Text += $"[{DateTime.Now}] Starting Haptics Fix download...\n"; + DownloadHapticsFixButton.IsEnabled = false; + HapticsFixStatus.Text = "Downloading Haptics Fix..."; + UpdateDetailsText.Text += $"[{DateTime.Now}] Starting Haptics Fix download...\n"; - string url = "https://github.com/heisthecat31/EchoVR-Haptics/releases/download/haptics/HapticsFix.zip"; + try + { string zipPath = Path.Combine(_tempFolder, "HapticsFix.zip"); string extractPath = Path.Combine(_tempFolder, "HapticsFix_Extracted"); - using (WebClient client = new WebClient()) - { - client.Headers.Add("User-Agent", "Spark-Updater"); - await client.DownloadFileTaskAsync(new Uri(url), zipPath); - } + await DownloadFileWithProgressAsync( + "https://github.com/heisthecat31/EchoVR-Haptics/releases/download/haptics/HapticsFix.zip", + zipPath, + _ => { }); UpdateDetailsText.Text += $"[{DateTime.Now}] Download complete. Extracting...\n"; if (Directory.Exists(extractPath)) Directory.Delete(extractPath, true); - + ZipFile.ExtractToDirectory(zipPath, extractPath); // Copy files - string[] filesToCopy = { "dbgcore.dll", "haptics_config.txt" }; - foreach (string fileName in filesToCopy) + foreach (string fileName in new[] { "dbgcore.dll", "haptics_config.txt" }) { string sourceFile = Path.Combine(extractPath, fileName); string destFile = Path.Combine(echoDir, fileName); From a90966fa62de9b450bc5f84863dea4f2aa09db82 Mon Sep 17 00:00:00 2001 From: he_is_the_cat <125207670+heisthecat31@users.noreply.github.com> Date: Sun, 17 May 2026 17:19:32 +0100 Subject: [PATCH 03/33] Update 3.0.6 Hid FriendTab, Added Update checker on startup, Included needed .NET components in build, Fixed crashing issues at startup. --- .github/workflows/build.yml | 56 +++ IgniteBot.Installer/Spark.Installer.wixproj | 5 +- Program.cs | 55 +- Spark.csproj | 12 +- SparkMSIX/Package.appxmanifest | 2 +- SparkSettings.cs | 3 +- Windows/LiveWindow/FriendsTab.xaml.cs | 94 +++- Windows/LiveWindow/LiveWindow.xaml.cs | 2 +- Windows/LiveWindow/UpdateSpark.xaml.cs | 532 +++++++++++++------- Windows/UpdatePromptWindow.xaml | 148 ++++++ Windows/UpdatePromptWindow.xaml.cs | 235 +++++++++ 11 files changed, 909 insertions(+), 235 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 Windows/UpdatePromptWindow.xaml create mode 100644 Windows/UpdatePromptWindow.xaml.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..52ff2e2 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,56 @@ +name: Build Spark + +on: + push: + branches: + - main + - master + - develop + pull_request: + branches: + - main + - master + - develop + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore Dependencies + run: dotnet restore Spark.sln + + - name: Build + run: dotnet build Spark.sln --configuration Release --no-restore + + - name: Publish Framework-Dependent + run: dotnet publish Spark.csproj -c Release -p:PublishSingleFile=false --no-build -o ./publish-fd + + - name: Publish Self-Contained + run: dotnet publish Spark.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o ./publish-sc + + - name: Upload Framework-Dependent Artifact + uses: actions/upload-artifact@v4 + with: + name: Spark-Framework-Dependent + path: ./publish-fd/ + retention-days: 30 + + - name: Upload Self-Contained Artifact + uses: actions/upload-artifact@v4 + with: + name: Spark-Self-Contained + path: ./publish-sc/ + retention-days: 30 diff --git a/IgniteBot.Installer/Spark.Installer.wixproj b/IgniteBot.Installer/Spark.Installer.wixproj index 6163711..2297aea 100644 --- a/IgniteBot.Installer/Spark.Installer.wixproj +++ b/IgniteBot.Installer/Spark.Installer.wixproj @@ -38,6 +38,7 @@ True Binaries;Content;Satellites INSTALLFOLDER + SelfContained=true;RuntimeIdentifier=win-x64;AppendRuntimeIdentifierToOutputPath=false @@ -47,7 +48,7 @@ true - + @@ -63,5 +64,7 @@ + + \ No newline at end of file diff --git a/Program.cs b/Program.cs index 3dc5df6..f2ba1bd 100644 --- a/Program.cs +++ b/Program.cs @@ -413,12 +413,22 @@ public static void Main(string[] args, App app) netMQEvents = new NetMQEvents(); - InstalledSpeakerSystemVersion = FindEchoSpeakerSystemInstallVersion(); - if (InstalledSpeakerSystemVersion.Length > 0) + _ = Task.Run(() => { - string[] latestSpeakerSystemVer = GetLatestSpeakerSystemURLVer(); - IsSpeakerSystemUpdateAvailable = latestSpeakerSystemVer[1] != InstalledSpeakerSystemVersion; - } + try + { + InstalledSpeakerSystemVersion = FindEchoSpeakerSystemInstallVersion(); + if (InstalledSpeakerSystemVersion.Length > 0) + { + string[] latestSpeakerSystemVer = GetLatestSpeakerSystemURLVer(); + IsSpeakerSystemUpdateAvailable = latestSpeakerSystemVer[1] != InstalledSpeakerSystemVersion; + } + } + catch (Exception ex) + { + Logger.Error($"[SpeakerSystem] Background update check failed: {ex.Message}"); + } + }); SparkSettings.instance.sparkExeLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Spark.exe"); @@ -492,15 +502,34 @@ public static void Main(string[] args, App app) - if (!SparkSettings.instance.onlyActivateHighlightsWhenGameIsOpen && - SparkSettings.instance.isNVHighlightsEnabled) - { - HighlightsHelper.SetupNVHighlights(); - } - else + _ = Task.Run(() => { - HighlightsHelper.InitHighlightsSDK(true); - } + if (!SparkSettings.instance.onlyActivateHighlightsWhenGameIsOpen && + SparkSettings.instance.isNVHighlightsEnabled) + { + HighlightsHelper.SetupNVHighlights(); + } + else + { + HighlightsHelper.InitHighlightsSDK(true); + } + + // Safely update UI once done, if highlights successfully initialized + if (HighlightsHelper.didHighlightsInit && HighlightsHelper.isNVHighlightsEnabled) + { + Application.Current?.Dispatcher?.Invoke(() => + { + if (liveWindow != null) + { + liveWindow.showHighlights.IsEnabled = HighlightsHelper.DoNVClipsExist(); + liveWindow.showHighlights.Visibility = Visibility.Visible; + liveWindow.showHighlights.Content = HighlightsHelper.DoNVClipsExist() + ? Properties.Resources.Show + " " + HighlightsHelper.nvHighlightClipCount + " " + Properties.Resources.Highlights + : Properties.Resources.No_clips_available; + } + }); + } + }); // only enable Highlights when game is open ConnectedToGame += (_, _) => diff --git a/Spark.csproj b/Spark.csproj index 55a547d..7796689 100644 --- a/Spark.csproj +++ b/Spark.csproj @@ -8,7 +8,7 @@ Spark.App img\ignite_logo.ico true - 3.0.5 + 3.0.6 https://www.ignitevr.gg/spark IgniteVR NtsFranz, VTSxKING, iblowatsports @@ -19,7 +19,8 @@ Spark Spark 14 - 3.0.5 + 3.0.6 + win-x64 @@ -272,6 +273,10 @@ Code UpdateWindow.xaml + + Code + UpdatePromptWindow.xaml + @@ -328,6 +333,9 @@ Designer + + Designer + diff --git a/SparkMSIX/Package.appxmanifest b/SparkMSIX/Package.appxmanifest index eb778a2..28ec030 100644 --- a/SparkMSIX/Package.appxmanifest +++ b/SparkMSIX/Package.appxmanifest @@ -9,7 +9,7 @@ + Version="3.0.6.0" /> Spark - EchoVR Companion App diff --git a/SparkSettings.cs b/SparkSettings.cs index 8894837..3d4ed0c 100644 --- a/SparkSettings.cs +++ b/SparkSettings.cs @@ -168,9 +168,10 @@ public string echoVRPath public bool showPrivateMatchRulesTab { get; set; } = true; public bool showCreateServerTab { get; set; } = true; public bool showPlayerCardTab { get; set; } = true; - public bool showFriendsTab { get; set; } = true; + public bool showFriendsTab { get; set; } = false; public string myFriendCode { get; set; } = ""; public List friendCodes { get; set; } = new List(); + public string ignoredUpdateVersion { get; set; } = ""; diff --git a/Windows/LiveWindow/FriendsTab.xaml.cs b/Windows/LiveWindow/FriendsTab.xaml.cs index 79aaf25..d62bf5a 100644 --- a/Windows/LiveWindow/FriendsTab.xaml.cs +++ b/Windows/LiveWindow/FriendsTab.xaml.cs @@ -23,6 +23,9 @@ public partial class FriendsTab : UserControl private bool isRunning = true; private bool isPublic = true; private static readonly HttpClient http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; + private readonly object stateLock = new object(); + private bool isRefreshing = false; + private bool isPollingActive = false; // ─── Bot API — same Ignite API server, same Discord OAuth token ────── @@ -55,52 +58,74 @@ public FriendsTab() { InitializeComponent(); FriendsItemsControl.ItemsSource = friends; + + // React when the user logs into Discord - registered ONCE in constructor to avoid event handler leak + DiscordOAuth.Authenticated += () => Task.Run(async () => await RefreshAll()); } private void UserControl_Loaded(object sender, RoutedEventArgs e) { - // React when the user logs into Discord - DiscordOAuth.Authenticated += () => Dispatcher.Invoke(async () => await RefreshAll()); - // Initialise immediately if already logged in - Dispatcher.BeginInvoke(new Action(async () => await RefreshAll())); + Task.Run(async () => await RefreshAll()); } // ─── Auth state ──────────────────────────────────────────────────────── private async Task RefreshAll() { - if (!BotConfigured) + lock (stateLock) { - ShowNotLoggedIn(); - return; + if (isRefreshing) return; + isRefreshing = true; } - ShowLoggedIn(); - SetStatus(null, "Connecting..."); - await RegisterWithBot(); - await PollFriends(); - StartPolling(); + try + { + if (!BotConfigured) + { + ShowNotLoggedIn(); + return; + } + + ShowLoggedIn(); + SetStatus(null, "Connecting..."); + await RegisterWithBot(); + await PollFriends(); + StartPolling(); + } + finally + { + lock (stateLock) + { + isRefreshing = false; + } + } } private void ShowNotLoggedIn() { - NotLoggedInPanel.Visibility = Visibility.Visible; - FriendsSection.Visibility = Visibility.Collapsed; - MyFriendCodeSection.Visibility = Visibility.Collapsed; - SetStatus(false, "Log in with Discord to use Friends"); + Dispatcher.Invoke(() => + { + NotLoggedInPanel.Visibility = Visibility.Visible; + FriendsSection.Visibility = Visibility.Collapsed; + MyFriendCodeSection.Visibility = Visibility.Collapsed; + SetStatus(false, "Log in with Discord to use Friends"); + }); } private void ShowLoggedIn() { - NotLoggedInPanel.Visibility = Visibility.Collapsed; - FriendsSection.Visibility = Visibility.Visible; - - if (!string.IsNullOrEmpty(SparkSettings.instance.myFriendCode)) + Dispatcher.Invoke(() => { - MyFriendCodeText.Text = FormatCode(SparkSettings.instance.myFriendCode); - MyFriendCodeSection.Visibility = Visibility.Visible; - } + NotLoggedInPanel.Visibility = Visibility.Collapsed; + FriendsSection.Visibility = Visibility.Visible; + + if (!string.IsNullOrEmpty(SparkSettings.instance.myFriendCode)) + { + MyFriendCodeText.Text = FormatCode(SparkSettings.instance.myFriendCode); + MyFriendCodeSection.Visibility = Visibility.Visible; + } + }); } // ─── Bot API calls ───────────────────────────────────────────────────── @@ -148,13 +173,28 @@ private async Task RegisterWithBot() private void StartPolling() { - if (!isRunning) return; + lock (stateLock) + { + if (isPollingActive || !isRunning) return; + isPollingActive = true; + } + Task.Run(async () => { - while (isRunning) + try + { + while (isRunning) + { + await Task.Delay(2500); + try { await PollFriends(); } catch { } + } + } + finally { - await Task.Delay(2000); - try { await PollFriends(); } catch { } + lock (stateLock) + { + isPollingActive = false; + } } }); } diff --git a/Windows/LiveWindow/LiveWindow.xaml.cs b/Windows/LiveWindow/LiveWindow.xaml.cs index c8d3f09..af0f9f1 100644 --- a/Windows/LiveWindow/LiveWindow.xaml.cs +++ b/Windows/LiveWindow/LiveWindow.xaml.cs @@ -236,7 +236,7 @@ public LiveWindow() SetDashboardItem1Visibility(SparkSettings.instance.dashboardItem1); - _ = CheckForAppUpdate(); + _ = Task.Run(async () => await UpdateSparkControl.CheckForUpdatesBackgroundAsync()); initialized = true; } diff --git a/Windows/LiveWindow/UpdateSpark.xaml.cs b/Windows/LiveWindow/UpdateSpark.xaml.cs index 5e3dd48..683d28c 100644 --- a/Windows/LiveWindow/UpdateSpark.xaml.cs +++ b/Windows/LiveWindow/UpdateSpark.xaml.cs @@ -3,13 +3,11 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; +using System.Net; using System.Windows; using System.Windows.Controls; using System.Diagnostics; using System.Reflection; -using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; @@ -17,8 +15,6 @@ namespace Spark { public partial class UpdateSparkControl : UserControl { - private static readonly HttpClient _httpClient = new HttpClient(); - private string _latestVersion = ""; private string _currentVersion = ""; private string _tempFolder = ""; @@ -36,10 +32,6 @@ public class ColorVersion public UpdateSparkControl() { InitializeComponent(); - - _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Spark-Updater"); - _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json")); - Loaded += OnLoaded; ColorVersionDropdown.SelectionChanged += ColorVersionDropdown_SelectionChanged; } @@ -63,66 +55,76 @@ private void OnLoaded(object sender, RoutedEventArgs e) private string GetCurrentVersion() { - Version version = Assembly.GetExecutingAssembly().GetName().Version; + var version = Assembly.GetExecutingAssembly().GetName().Version; return $"{version.Major}.{version.Minor}.{version.Build}"; } - private async void CheckUpdateButton_Click(object sender, RoutedEventArgs e) - { - CheckUpdateButton.IsEnabled = false; - StatusText.Text = "Checking for updates..."; - UpdateDetailsText.Text += $"[{DateTime.Now}] Checking for updates...\n"; +private async void CheckUpdateButton_Click(object sender, RoutedEventArgs e) +{ + CheckUpdateButton.IsEnabled = false; + StatusText.Text = "Checking for updates..."; + UpdateDetailsText.Text += $"[{DateTime.Now}] Checking for updates...\n"; - try + try + { + using (var client = new WebClient()) + { + client.Headers.Add("User-Agent", "Spark-Updater"); + client.Headers.Add("Accept", "application/vnd.github.v3+json"); + + // URL for the specific 'ignore' tag + string latestReleaseUrl = "https://api.github.com/repos/heisthecat31/Spark/releases/tags/ignore"; + + // Download the JSON + string json = await client.DownloadStringTaskAsync(latestReleaseUrl); + + // 1. CREATE the 'release' variable here + var release = JObject.Parse(json); + + // 2. NOW you can use it. + // We try to grab the "name" (Release Title) first. + string titleName = release["name"]?.ToString(); + + // If the title is empty, we fall back to the "tag_name" + if (string.IsNullOrWhiteSpace(titleName)) { - // URL for the specific tag 'ignore' tag, download it - string json = await _httpClient.GetStringAsync("https://api.github.com/repos/heisthecat31/Spark/releases/tags/ignore"); - - // CREATE the 'release' variable here - JObject release = JObject.Parse(json); - - string titleName = release["name"]?.ToString(); - // If the title is empty, we fall back to the "tag_name" - if (string.IsNullOrWhiteSpace(titleName)) - { - titleName = release["tag_name"]?.ToString(); - } + titleName = release["tag_name"]?.ToString(); + } - // Remove 'v' prefix if it exists (just in case) - _latestVersion = titleName?.TrimStart('v') ?? "Unknown"; - LatestVersionText.Text = _latestVersion; - StatusText.Text = $"Version found: {_latestVersion}"; + // Remove 'v' prefix if it exists (just in case) + _latestVersion = titleName?.TrimStart('v') ?? "Unknown"; - string releaseNotes = release["body"]?.ToString(); - UpdateDetailsText.Text += $"[{DateTime.Now}] Update found: {_latestVersion}\n"; + LatestVersionText.Text = _latestVersion; + StatusText.Text = $"Version found: {_latestVersion}"; - if (!string.IsNullOrEmpty(releaseNotes)) - { - UpdateDetailsText.Text += $"Release Notes:\n{releaseNotes}\n"; - } + string releaseNotes = release["body"]?.ToString(); + UpdateDetailsText.Text += $"[{DateTime.Now}] Update found: {_latestVersion}\n"; - await LoadAvailableVersions(release); - } - catch (HttpRequestException ex) when (ex.Message.Contains("404")) - { - StatusText.Text = "Error: Tag not found or API error."; - UpdateDetailsText.Text += $"[{DateTime.Now}] Error: {ex.Message}\n"; - UpdateDetailsText.Text += "Tip: Check that the tag 'ignore' exists in your GitHub Releases.\n"; - ColorVersionDropdown.IsEnabled = false; - DownloadUpdateButton.IsEnabled = false; - } - catch (Exception ex) - { - StatusText.Text = "Error checking for updates."; - UpdateDetailsText.Text += $"[{DateTime.Now}] Error: {ex.Message}\n"; - ColorVersionDropdown.IsEnabled = false; - DownloadUpdateButton.IsEnabled = false; - } - finally + if (!string.IsNullOrEmpty(releaseNotes)) { - CheckUpdateButton.IsEnabled = true; + UpdateDetailsText.Text += $"Release Notes:\n{releaseNotes}\n"; } + + await LoadAvailableVersions(release); + } + } + catch (Exception ex) + { + StatusText.Text = "Error: Tag not found or API error."; + UpdateDetailsText.Text += $"[{DateTime.Now}] Error: {ex.Message}\n"; + + if (ex.Message.Contains("404")) { + UpdateDetailsText.Text += "Tip: Check that the tag 'ignore' exists in your GitHub Releases.\n"; } + + ColorVersionDropdown.IsEnabled = false; + DownloadUpdateButton.IsEnabled = false; + } + finally + { + CheckUpdateButton.IsEnabled = true; + } +} private async Task LoadAvailableVersions(JObject release) { @@ -131,23 +133,24 @@ private async Task LoadAvailableVersions(JObject release) try { - JArray assets = release["assets"] as JArray; + var assets = release["assets"] as JArray; if (assets != null) { - foreach (JToken asset in assets) + foreach (var asset in assets) { string name = asset["name"]?.ToString(); string downloadUrl = asset["browser_download_url"]?.ToString(); // Look for Spark theme files but exclude SparkTTSCache.zip - if (name != null && downloadUrl != null && - name.StartsWith("Spark", StringComparison.OrdinalIgnoreCase) && + if (name != null && downloadUrl != null && + name.StartsWith("Spark", StringComparison.OrdinalIgnoreCase) && name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) && !name.Equals("SparkTTSCache.zip", StringComparison.OrdinalIgnoreCase)) { + string displayName = GetDisplayName(name); _availableVersions.Add(new ColorVersion { - DisplayName = GetDisplayName(name), + DisplayName = displayName, FileName = name, DownloadUrl = downloadUrl }); @@ -157,35 +160,52 @@ private async Task LoadAvailableVersions(JObject release) if (_availableVersions.Count > 0) { - ColorVersionDropdown.ItemsSource = _availableVersions; - ColorVersionDropdown.DisplayMemberPath = "DisplayName"; - ColorVersionDropdown.SelectedValuePath = "DownloadUrl"; + Dispatcher.Invoke(() => + { + ColorVersionDropdown.ItemsSource = _availableVersions; + ColorVersionDropdown.DisplayMemberPath = "DisplayName"; + ColorVersionDropdown.SelectedValuePath = "DownloadUrl"; - ColorVersion defaultVersion = _availableVersions.FirstOrDefault(v => - v.FileName.Equals("Spark.zip", StringComparison.OrdinalIgnoreCase)); + var defaultVersion = _availableVersions.FirstOrDefault(v => + v.FileName.Equals("Spark.zip", StringComparison.OrdinalIgnoreCase)); - ColorVersionDropdown.SelectedItem = defaultVersion ?? _availableVersions[0]; - ColorVersionDropdown.IsEnabled = true; - DownloadUpdateButton.IsEnabled = true; + if (defaultVersion != null) + { + ColorVersionDropdown.SelectedItem = defaultVersion; + } + else + { + ColorVersionDropdown.SelectedIndex = 0; + } - UpdateDetailsText.Text += $"[{DateTime.Now}] Found {_availableVersions.Count} theme version(s):\n"; - foreach (ColorVersion version in _availableVersions) - { - UpdateDetailsText.Text += $"[{DateTime.Now}] • {version.DisplayName} ({version.FileName})\n"; - } + ColorVersionDropdown.IsEnabled = true; + DownloadUpdateButton.IsEnabled = true; + + UpdateDetailsText.Text += $"[{DateTime.Now}] Found {_availableVersions.Count} theme version(s):\n"; + foreach (var version in _availableVersions) + { + UpdateDetailsText.Text += $"[{DateTime.Now}] • {version.DisplayName} ({version.FileName})\n"; + } + }); } else { - StatusText.Text = "No theme versions found in release."; - ColorVersionDropdown.IsEnabled = false; - DownloadUpdateButton.IsEnabled = false; - UpdateDetailsText.Text += $"[{DateTime.Now}] Warning: No Spark*.zip theme files found in release\n"; + Dispatcher.Invoke(() => + { + StatusText.Text = "No theme versions found in release"; + ColorVersionDropdown.IsEnabled = false; + DownloadUpdateButton.IsEnabled = false; + UpdateDetailsText.Text += $"[{DateTime.Now}] Warning: No Spark*.zip theme files found in release\n"; + }); } } catch (Exception ex) { - StatusText.Text = $"Error loading versions: {ex.Message}"; - UpdateDetailsText.Text += $"[{DateTime.Now}] Error loading versions: {ex.Message}\n"; + Dispatcher.Invoke(() => + { + StatusText.Text = $"Error loading versions: {ex.Message}"; + UpdateDetailsText.Text += $"[{DateTime.Now}] Error loading versions: {ex.Message}\n"; + }); } } @@ -195,89 +215,84 @@ private string GetDisplayName(string fileName) return "Default Theme"; string baseName = Path.GetFileNameWithoutExtension(fileName); - + if (baseName.StartsWith("Spark", StringComparison.OrdinalIgnoreCase)) { string themeName = baseName.Substring(5); - return string.IsNullOrWhiteSpace(themeName) ? "Default Theme" : $"{themeName} Theme"; + if (string.IsNullOrWhiteSpace(themeName)) + return "Default Theme"; + + return $"{themeName} Theme"; } - + return baseName; } private void ColorVersionDropdown_SelectionChanged(object sender, SelectionChangedEventArgs e) { - if (ColorVersionDropdown.SelectedItem is ColorVersion selected) + if (ColorVersionDropdown.SelectedItem != null) { - _selectedDownloadUrl = selected.DownloadUrl; - UpdateDetailsText.Text += $"[{DateTime.Now}] Selected: {selected.DisplayName}\n"; + var selectedVersion = (ColorVersion)ColorVersionDropdown.SelectedItem; + _selectedDownloadUrl = selectedVersion.DownloadUrl; + UpdateDetailsText.Text += $"[{DateTime.Now}] Selected: {selectedVersion.DisplayName}\n"; } } private async void DownloadUpdateButton_Click(object sender, RoutedEventArgs e) { - if (ColorVersionDropdown.SelectedItem is not ColorVersion selectedVersion) + if (ColorVersionDropdown.SelectedItem == null) { StatusText.Text = "Please select a theme version first"; return; } - DownloadUpdateButton.IsEnabled = false; - CheckUpdateButton.IsEnabled = false; - ColorVersionDropdown.IsEnabled = false; - UpdateProgressBar.Visibility = Visibility.Visible; - UpdateProgressBar.Value = 0; - - StatusText.Text = $"Downloading {selectedVersion.DisplayName}..."; - UpdateDetailsText.Text += $"[{DateTime.Now}] Starting download of {selectedVersion.DisplayName}...\n"; - - string tempFilePath = Path.Combine(_tempFolder, selectedVersion.FileName); + var selectedVersion = (ColorVersion)ColorVersionDropdown.SelectedItem; try { - await DownloadFileWithProgressAsync(selectedVersion.DownloadUrl, tempFilePath, (pct) => - { - UpdateProgressBar.Value = pct; - StatusText.Text = $"Downloading {selectedVersion.DisplayName}: {pct}%"; - }); - - StatusText.Text = "Download complete. Extracting and installing..."; - UpdateDetailsText.Text += $"[{DateTime.Now}] Download complete. Extracting...\n"; + DownloadUpdateButton.IsEnabled = false; + CheckUpdateButton.IsEnabled = false; + ColorVersionDropdown.IsEnabled = false; + UpdateProgressBar.Visibility = Visibility.Visible; + UpdateProgressBar.Value = 0; - await Task.Run(() => InstallUpdate(tempFilePath, selectedVersion.FileName)); - } - catch (Exception ex) - { - StatusText.Text = $"Error: {ex.Message}"; - UpdateDetailsText.Text += $"[{DateTime.Now}] Error downloading {selectedVersion.DisplayName}: {ex.Message}\n"; - ResetButtons(); - } - } + StatusText.Text = $"Downloading {selectedVersion.DisplayName}..."; + UpdateDetailsText.Text += $"[{DateTime.Now}] Starting download of {selectedVersion.DisplayName}...\n"; - private async Task DownloadFileWithProgressAsync(string url, string destPath, Action onProgress, CancellationToken ct = default) - { - using HttpResponseMessage response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct); - response.EnsureSuccessStatusCode(); + string tempFilePath = Path.Combine(_tempFolder, selectedVersion.FileName); - long? totalBytes = response.Content.Headers.ContentLength; + using (var client = new WebClient()) + { + client.Headers.Add("User-Agent", "Spark-Updater"); + + client.DownloadProgressChanged += (s, args) => + { + Dispatcher.Invoke(() => + { + UpdateProgressBar.Value = args.ProgressPercentage; + StatusText.Text = $"Downloading {selectedVersion.DisplayName}: {args.ProgressPercentage}%"; + }); + }; - await using Stream contentStream = await response.Content.ReadAsStreamAsync(ct); - await using FileStream fileStream = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true); + await client.DownloadFileTaskAsync(new Uri(_selectedDownloadUrl), tempFilePath); - byte[] buffer = new byte[8192]; - long bytesRead = 0; - int read; + Dispatcher.Invoke(() => + { + StatusText.Text = "Download complete. Extracting and installing..."; + UpdateDetailsText.Text += $"[{DateTime.Now}] Download complete. Extracting...\n"; + }); - while ((read = await contentStream.ReadAsync(buffer, ct)) > 0) + await Task.Run(() => InstallUpdate(tempFilePath, selectedVersion.FileName)); + } + } + catch (Exception ex) { - await fileStream.WriteAsync(buffer.AsMemory(0, read), ct); - bytesRead += read; - - if (totalBytes.HasValue) + Dispatcher.Invoke(() => { - int pct = (int)(bytesRead * 100 / totalBytes.Value); - Dispatcher.Invoke(() => onProgress(pct)); - } + StatusText.Text = $"Error: {ex.Message}"; + UpdateDetailsText.Text += $"[{DateTime.Now}] Error downloading {selectedVersion.DisplayName}: {ex.Message}\n"; + ResetButtons(); + }); } } @@ -300,7 +315,6 @@ private void InstallUpdate(string zipFilePath, string originalFileName) string currentExe = Process.GetCurrentProcess().MainModule.FileName; string targetFolder = Path.GetDirectoryName(currentExe); - string actualSourceFolder = FindActualSourceFolder(extractPath); Dispatcher.Invoke(() => { @@ -308,7 +322,10 @@ private void InstallUpdate(string zipFilePath, string originalFileName) StatusText.Text = "Creating update script..."; }); + string actualSourceFolder = FindActualSourceFolder(extractPath); + string batchFile = Path.Combine(_tempFolder, "update_spark.bat"); + string batchContent = $@" @echo off setlocal enabledelayedexpansion @@ -365,23 +382,36 @@ REM Delete this script and the temp extraction (goto) 2>nul & del ""%~f0"" exit "; + File.WriteAllText(batchFile, batchContent); Dispatcher.Invoke(() => { UpdateDetailsText.Text += $"[{DateTime.Now}] Batch file created\n"; StatusText.Text = "Starting update..."; + }); - Process.Start(new ProcessStartInfo - { - FileName = batchFile, - WindowStyle = ProcessWindowStyle.Normal, - UseShellExecute = true, - WorkingDirectory = _tempFolder - }); + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = batchFile, + WindowStyle = ProcessWindowStyle.Normal, + UseShellExecute = true, + WorkingDirectory = _tempFolder + }; + Dispatcher.Invoke(() => + { + UpdateDetailsText.Text += $"[{DateTime.Now}] Launching update...\n"; - Program.Quit(); + Process.Start(psi); + + Task.Delay(500).ContinueWith(t => + { + Dispatcher.Invoke(() => + { + Process.GetCurrentProcess().Kill(); + }); + }); }); } catch (Exception ex) @@ -406,7 +436,9 @@ private string FindActualSourceFolder(string extractPath) { string folder = Path.GetDirectoryName(files[0]); if (!string.IsNullOrEmpty(folder)) + { return folder; + } } } catch (Exception ex) @@ -422,46 +454,71 @@ private string FindActualSourceFolder(string extractPath) private async void DownloadTTSCacheButton_Click(object sender, RoutedEventArgs e) { - DownloadTTSCacheButton.IsEnabled = false; - TTSCacheStatus.Text = "Downloading TTS Cache..."; - try { - string json = await _httpClient.GetStringAsync("https://api.github.com/repos/heisthecat31/Spark/releases/latest"); - JObject release = JObject.Parse(json); + DownloadTTSCacheButton.IsEnabled = false; + TTSCacheStatus.Text = "Downloading TTS Cache..."; + + string apiUrl = "https://api.github.com/repos/heisthecat31/Spark/releases/latest"; + using (var client = new WebClient()) + { + client.Headers.Add("User-Agent", "Spark-Updater"); + string json = await client.DownloadStringTaskAsync(apiUrl); + var release = JObject.Parse(json); + + var assets = release["assets"] as JArray; + string ttsCacheUrl = ""; - JArray assets = release["assets"] as JArray; - string ttsCacheUrl = assets? - .FirstOrDefault(a => a["name"]?.ToString().Equals("SparkTTSCache.zip", StringComparison.OrdinalIgnoreCase) == true) - ?["browser_download_url"]?.ToString(); + if (assets != null) + { + foreach (var asset in assets) + { + string name = asset["name"]?.ToString(); + if (name != null && name.Equals("SparkTTSCache.zip", StringComparison.OrdinalIgnoreCase)) + { + ttsCacheUrl = asset["browser_download_url"]?.ToString(); + break; + } + } + } - if (string.IsNullOrEmpty(ttsCacheUrl)) - throw new Exception("SparkTTSCache.zip not found in release assets."); + if (string.IsNullOrEmpty(ttsCacheUrl)) + { + throw new Exception("SparkTTSCache.zip not found in release assets"); + } - // Use the custom cache folder configured in settings, or the default Spark directory fallback - string ttsCacheFolder = TTSController.CacheFolder; + string ttsCacheFolder = TTSController.CacheFolder; - if (Directory.Exists(ttsCacheFolder)) - Directory.Delete(ttsCacheFolder, true); + if (Directory.Exists(ttsCacheFolder)) + { + Directory.Delete(ttsCacheFolder, true); + } - Directory.CreateDirectory(ttsCacheFolder); + Directory.CreateDirectory(ttsCacheFolder); - string tempTtsZip = Path.Combine(ttsCacheFolder, "SparkTTSCache.zip"); + string tempTtsZip = Path.Combine(ttsCacheFolder, "SparkTTSCache.zip"); - await DownloadFileWithProgressAsync(ttsCacheUrl, tempTtsZip, _ => { }); + await client.DownloadFileTaskAsync(new Uri(ttsCacheUrl), tempTtsZip); - ZipFile.ExtractToDirectory(tempTtsZip, ttsCacheFolder); - File.Delete(tempTtsZip); + ZipFile.ExtractToDirectory(tempTtsZip, ttsCacheFolder); + File.Delete(tempTtsZip); - TTSCacheStatus.Text = "TTS Cache downloaded successfully!"; - UpdateDetailsText.Text += $"[{DateTime.Now}] TTS Cache downloaded to: {ttsCacheFolder}\n"; - new MessageBox($"Success: TTS Cache downloaded and extracted to:\n{ttsCacheFolder}").Show(); + Dispatcher.Invoke(() => + { + TTSCacheStatus.Text = "TTS Cache downloaded successfully!"; + UpdateDetailsText.Text += $"[{DateTime.Now}] TTS Cache downloaded to: {ttsCacheFolder}\n"; + new MessageBox($"Success: TTS Cache downloaded and extracted to:\n{ttsCacheFolder}").Show(); + }); + } } catch (Exception ex) { - TTSCacheStatus.Text = $"Error: {ex.Message}"; - UpdateDetailsText.Text += $"[{DateTime.Now}] TTS Cache error: {ex.Message}\n"; - new MessageBox($"Error downloading TTS Cache: {ex.Message}").Show(); + Dispatcher.Invoke(() => + { + TTSCacheStatus.Text = $"Error: {ex.Message}"; + UpdateDetailsText.Text += $"[{DateTime.Now}] TTS Cache error: {ex.Message}\n"; + new MessageBox($"Error downloading TTS Cache: {ex.Message}").Show(); + }); } finally { @@ -471,42 +528,45 @@ private async void DownloadTTSCacheButton_Click(object sender, RoutedEventArgs e private async void DownloadHapticsFixButton_Click(object sender, RoutedEventArgs e) { - if (SparkSettings.instance == null || string.IsNullOrEmpty(SparkSettings.instance.echoVRPath)) + try { - new MessageBox("Error: EchoVR Path is not set in Spark Settings. Please set it in the main settings first.").Show(); - return; - } + if (SparkSettings.instance == null || string.IsNullOrEmpty(SparkSettings.instance.echoVRPath)) + { + new MessageBox("Error: EchoVR Path is not set in Spark Settings. Please set it in the main settings first.").Show(); + return; + } - string echoDir = Path.GetDirectoryName(SparkSettings.instance.echoVRPath); - if (!Directory.Exists(echoDir)) - { - new MessageBox($"Error: EchoVR directory not found at:\n{echoDir}").Show(); - return; - } + string echoDir = Path.GetDirectoryName(SparkSettings.instance.echoVRPath); + if (!Directory.Exists(echoDir)) + { + new MessageBox($"Error: EchoVR directory not found at:\n{echoDir}").Show(); + return; + } - DownloadHapticsFixButton.IsEnabled = false; - HapticsFixStatus.Text = "Downloading Haptics Fix..."; - UpdateDetailsText.Text += $"[{DateTime.Now}] Starting Haptics Fix download...\n"; + DownloadHapticsFixButton.IsEnabled = false; + HapticsFixStatus.Text = "Downloading Haptics Fix..."; + UpdateDetailsText.Text += $"[{DateTime.Now}] Starting Haptics Fix download...\n"; - try - { + string url = "https://github.com/heisthecat31/EchoVR-Haptics/releases/download/haptics/HapticsFix.zip"; string zipPath = Path.Combine(_tempFolder, "HapticsFix.zip"); string extractPath = Path.Combine(_tempFolder, "HapticsFix_Extracted"); - await DownloadFileWithProgressAsync( - "https://github.com/heisthecat31/EchoVR-Haptics/releases/download/haptics/HapticsFix.zip", - zipPath, - _ => { }); + using (WebClient client = new WebClient()) + { + client.Headers.Add("User-Agent", "Spark-Updater"); + await client.DownloadFileTaskAsync(new Uri(url), zipPath); + } UpdateDetailsText.Text += $"[{DateTime.Now}] Download complete. Extracting...\n"; if (Directory.Exists(extractPath)) Directory.Delete(extractPath, true); - + ZipFile.ExtractToDirectory(zipPath, extractPath); // Copy files - foreach (string fileName in new[] { "dbgcore.dll", "haptics_config.txt" }) + string[] filesToCopy = { "dbgcore.dll", "haptics_config.txt" }; + foreach (string fileName in filesToCopy) { string sourceFile = Path.Combine(extractPath, fileName); string destFile = Path.Combine(echoDir, fileName); @@ -557,5 +617,99 @@ private void ResetButtons() ColorVersionDropdown.IsEnabled = true; UpdateProgressBar.Visibility = Visibility.Collapsed; } + + public static async Task CheckForUpdatesBackgroundAsync() + { + try + { + using (var client = new WebClient()) + { + client.Headers.Add("User-Agent", "Spark-Updater"); + client.Headers.Add("Accept", "application/vnd.github.v3+json"); + + string latestReleaseUrl = "https://api.github.com/repos/heisthecat31/Spark/releases/tags/ignore"; + + string json = await client.DownloadStringTaskAsync(latestReleaseUrl); + + var release = JObject.Parse(json); + + string titleName = release["name"]?.ToString(); + if (string.IsNullOrWhiteSpace(titleName)) + { + titleName = release["tag_name"]?.ToString(); + } + + string latestVersionStr = "0.0.0"; + var match = System.Text.RegularExpressions.Regex.Match(titleName ?? "", @"\d+\.\d+(\.\d+)?"); + if (match.Success) + { + latestVersionStr = match.Value; + } + + var currentAssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version; + string currentVersionStr = $"{currentAssemblyVersion.Major}.{currentAssemblyVersion.Minor}.{currentAssemblyVersion.Build}"; + + Logger.Error($"[Updater] Parsed latestVersionStr: {latestVersionStr} from release name '{titleName}'"); + Logger.Error($"[Updater] Parsed currentVersionStr: {currentVersionStr}"); + + if (latestVersionStr.Equals(SparkSettings.instance.ignoredUpdateVersion, StringComparison.OrdinalIgnoreCase)) + { + Logger.Error($"[Updater] Skipping update check because version {latestVersionStr} is marked as ignored."); + return; + } + + if (Version.TryParse(latestVersionStr, out Version latestVersion) && + Version.TryParse(currentVersionStr, out Version currentVersion)) + { + Logger.Error($"[Updater] Comparing versions: latest {latestVersion} vs current {currentVersion}. Result: latest > current = {latestVersion > currentVersion}"); + if (latestVersion > currentVersion) + { + string downloadUrl = null; + string zipFileName = "Spark.zip"; + var assets = release["assets"] as JArray; + if (assets != null) + { + var targetAsset = assets.FirstOrDefault(asset => + "Spark.zip".Equals(asset["name"]?.ToString(), StringComparison.OrdinalIgnoreCase)); + + if (targetAsset == null) + { + targetAsset = assets.FirstOrDefault(asset => + { + string name = asset["name"]?.ToString(); + return name != null && + name.StartsWith("Spark", StringComparison.OrdinalIgnoreCase) && + name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) && + !name.Equals("SparkTTSCache.zip", StringComparison.OrdinalIgnoreCase); + }); + } + + if (targetAsset != null) + { + downloadUrl = targetAsset["browser_download_url"]?.ToString(); + zipFileName = targetAsset["name"]?.ToString(); + } + } + + if (!string.IsNullOrEmpty(downloadUrl)) + { + string changelog = release["body"]?.ToString() ?? "No release notes provided."; + + Application.Current.Dispatcher.Invoke(() => + { + var prompt = new UpdatePromptWindow(latestVersionStr, changelog, downloadUrl, zipFileName); + prompt.Show(); + prompt.Focus(); + }); + } + } + } + } + } + catch (Exception ex) + { + Logger.Error($"[Updater] Background update check failed: {ex.Message}\n{ex.StackTrace}"); + } + } } } \ No newline at end of file diff --git a/Windows/UpdatePromptWindow.xaml b/Windows/UpdatePromptWindow.xaml new file mode 100644 index 0000000..2bb8960 --- /dev/null +++ b/Windows/UpdatePromptWindow.xaml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + diff --git a/Windows/LiveWindow/LiveWindow.xaml.cs b/Windows/LiveWindow/LiveWindow.xaml.cs index af0f9f1..13b0427 100644 --- a/Windows/LiveWindow/LiveWindow.xaml.cs +++ b/Windows/LiveWindow/LiveWindow.xaml.cs @@ -29,6 +29,15 @@ namespace Spark { + public class CombatLoadout + { + public string Name { get; set; } + public int Ping { get; set; } + public string Weapon { get; set; } + public string Ordnance { get; set; } + public string TacMod { get; set; } + } + /// /// Interaction logic for LiveWindow.xaml /// @@ -602,6 +611,125 @@ private void Update(object source, ElapsedEventArgs e) } #endregion + + // Combat Dashboard Logic + if (Program.lastFrame.match_type == "Echo_Combat") + { + ArenaDashboardGrid.Visibility = Visibility.Collapsed; + CombatDashboardGrid.Visibility = Visibility.Visible; + + try + { + if (!string.IsNullOrEmpty(Program.lastJSON)) + { + JObject jsonObj = JObject.Parse(Program.lastJSON); + + // Use round scores for the top header if available, otherwise fallback to points (e.g. for payload or older API) + string blueScore = jsonObj["blue_round_score"]?.ToString() ?? jsonObj["blue_points"]?.ToString() ?? "0"; + string orangeScore = jsonObj["orange_round_score"]?.ToString() ?? jsonObj["orange_points"]?.ToString() ?? "0"; + + CombatBlueScore.Text = blueScore; + CombatOrangeScore.Text = orangeScore; + + var blueLoadouts = new List(); + var orangeLoadouts = new List(); + + var teamsArray = jsonObj["teams"] as JArray; + + for (int t = 0; t < 3; t++) + { + if (t >= Program.lastFrame.teams.Count) continue; + var apiTeam = Program.lastFrame.teams[t]; + var jsonTeam = teamsArray != null && teamsArray.Count > t ? teamsArray[t] : null; + var jsonPlayers = jsonTeam?["players"] as JArray; + + for (int pIndex = 0; pIndex < apiTeam.players.Count; pIndex++) + { + var apiPlayer = apiTeam.players[pIndex]; + var jsonPlayer = jsonPlayers != null && jsonPlayers.Count > pIndex ? jsonPlayers[pIndex] : null; + + // Fallbacks to handle casing and nested data + string weapon = jsonPlayer?["Weapon"]?.ToString() ?? jsonPlayer?["weapon"]?.ToString() ?? "N/A"; + string ordnance = jsonPlayer?["Ordnance"]?.ToString() ?? jsonPlayer?["ordnance"]?.ToString() ?? "N/A"; + string tacmod = jsonPlayer?["TacMod"]?.ToString() ?? jsonPlayer?["tacmod"]?.ToString() ?? "N/A"; + + var loadout = new CombatLoadout + { + Name = apiPlayer.name, + Ping = apiPlayer.ping, + Weapon = weapon, + Ordnance = ordnance, + TacMod = tacmod + }; + + if (t == 0) blueLoadouts.Add(loadout); + else if (t == 1) orangeLoadouts.Add(loadout); + } + } + + BlueCombatLoadouts.ItemsSource = blueLoadouts; + OrangeCombatLoadouts.ItemsSource = orangeLoadouts; + + string mapName = jsonObj["map_name"]?.ToString(); + CombatMapName.Text = $"MAP: {mapName?.ToUpper()}"; + + if (mapName == "mpl_combat_fission" || mapName == "mpl_combat_gauss") + { + CombatObjectiveBorder.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#444")); + var payload = jsonObj["payload"]; + if (payload != null) + { + CombatObjectiveLabel.Text = "PAYLOAD DISTANCE"; + CombatObjectiveValue.Text = $"{payload["distance"]?.ToObject() ?? 0:N1}m"; + CombatObjectiveSecondaryLabel.Text = "SPEED"; + CombatObjectiveSecondaryValue.Text = $"{payload["speed"]?.ToObject() ?? 0:N2} m/s"; + } + } + else + { + // Capture point or other maps + bool isContested = jsonObj["contested"]?.ToObject() ?? false; + float blueProgress = jsonObj["blue_points"]?.ToObject() ?? 0; + float orangeProgress = jsonObj["orange_points"]?.ToObject() ?? 0; + + CombatObjectiveLabel.Text = "CAPTURE POINT"; + + if (isContested) + { + CombatObjectiveBorder.BorderBrush = new SolidColorBrush(Colors.Red); + CombatObjectiveValue.Text = "CONTESTED"; + } + else if (blueProgress > orangeProgress) + { + CombatObjectiveBorder.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#0088ff")); + CombatObjectiveValue.Text = "BLUE OWNS"; + } + else if (orangeProgress > blueProgress) + { + CombatObjectiveBorder.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#ff8800")); + CombatObjectiveValue.Text = "ORANGE OWNS"; + } + else + { + CombatObjectiveBorder.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#444")); + CombatObjectiveValue.Text = "NEUTRAL"; + } + + CombatObjectiveSecondaryLabel.Text = "PROGRESS"; + CombatObjectiveSecondaryValue.Text = $"{Math.Max(blueProgress, orangeProgress):N0}%"; + } + } + } + catch (Exception ex) + { + LogRow(LogType.Error, $"Error parsing combat JSON:\n{ex}"); + } + } + else + { + ArenaDashboardGrid.Visibility = Visibility.Visible; + CombatDashboardGrid.Visibility = Visibility.Collapsed; + } } bool blueReadyVisible = false; diff --git a/Windows/LiveWindow/UpdateSpark.xaml b/Windows/LiveWindow/UpdateSpark.xaml index dc1a2e4..356e13f 100644 --- a/Windows/LiveWindow/UpdateSpark.xaml +++ b/Windows/LiveWindow/UpdateSpark.xaml @@ -106,6 +106,22 @@ Foreground="{DynamicResource ControlDefaultForeground}"/> + +