From 9b0c14c8858d25256e859feea067c6ca767fed85 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 03:35:02 -0400 Subject: [PATCH 01/51] sync Wauncher standalone structure --- Wauncher/App.axaml | 68 +- Wauncher/App.axaml.cs | 179 +- Wauncher/Assets/Styles.axaml | 16 +- Wauncher/Assets/logo.png | Bin 0 -> 8314 bytes Wauncher/Assets/server.png | Bin 0 -> 4069 bytes Wauncher/Assets/settings.png | Bin 0 -> 5346 bytes Wauncher/Assets/social_discord.png | 1 + Wauncher/Assets/social_discord.svg | 2 + Wauncher/Assets/social_github.svg | 1 + Wauncher/Assets/social_instagram.png | 1 + Wauncher/Assets/social_instagram.svg | 2 + Wauncher/Assets/social_inventory.png | 1 + Wauncher/Assets/social_inventory.svg | 2 + Wauncher/Assets/social_world.png | 1 + Wauncher/Assets/social_world.svg | 24 + Wauncher/Program.cs | 65 +- Wauncher/Utils/Api.cs | 230 +++ Wauncher/Utils/Argument.cs | 68 + Wauncher/Utils/AvatarCache.cs | 112 ++ Wauncher/Utils/Console.cs | 31 + Wauncher/Utils/Debug.cs | 8 + Wauncher/Utils/Dependency.cs | 126 ++ Wauncher/Utils/DependencyChecks.cs | 41 + Wauncher/Utils/Discord.cs | 86 + Wauncher/Utils/Download.cs | 837 +++++++++ Wauncher/Utils/FriendsCache.cs | 83 + Wauncher/Utils/Game.cs | 162 ++ Wauncher/Utils/Patch.cs | 240 +++ Wauncher/Utils/ProtocolManager.cs | 9 +- Wauncher/Utils/ServerQuery.cs | 112 ++ Wauncher/Utils/Steam.cs | 98 + Wauncher/Utils/Terminal.cs | 60 + Wauncher/Utils/Version.cs | 38 + Wauncher/ViewModels/MainWindowViewModel.cs | 557 +++++- Wauncher/ViewModels/PatchNoteItem.cs | 10 + .../ViewModels/SettingsWindowViewModel.cs | 85 + Wauncher/Views/InfoWindow.axaml | 334 +++- Wauncher/Views/InfoWindow.axaml.cs | 15 +- Wauncher/Views/MainWindow.axaml | 834 ++++++++- Wauncher/Views/MainWindow.axaml.cs | 1612 ++++++++++++++++- Wauncher/Views/SettingsWindow.axaml | 203 +++ Wauncher/Views/SettingsWindow.axaml.cs | 36 + Wauncher/Wauncher.csproj | 20 +- Wauncher/Wauncher.sln | 24 + 44 files changed, 6147 insertions(+), 287 deletions(-) create mode 100644 Wauncher/Assets/logo.png create mode 100644 Wauncher/Assets/server.png create mode 100644 Wauncher/Assets/settings.png create mode 100644 Wauncher/Assets/social_discord.png create mode 100644 Wauncher/Assets/social_discord.svg create mode 100644 Wauncher/Assets/social_github.svg create mode 100644 Wauncher/Assets/social_instagram.png create mode 100644 Wauncher/Assets/social_instagram.svg create mode 100644 Wauncher/Assets/social_inventory.png create mode 100644 Wauncher/Assets/social_inventory.svg create mode 100644 Wauncher/Assets/social_world.png create mode 100644 Wauncher/Assets/social_world.svg create mode 100644 Wauncher/Utils/Api.cs create mode 100644 Wauncher/Utils/Argument.cs create mode 100644 Wauncher/Utils/AvatarCache.cs create mode 100644 Wauncher/Utils/Console.cs create mode 100644 Wauncher/Utils/Debug.cs create mode 100644 Wauncher/Utils/Dependency.cs create mode 100644 Wauncher/Utils/DependencyChecks.cs create mode 100644 Wauncher/Utils/Discord.cs create mode 100644 Wauncher/Utils/Download.cs create mode 100644 Wauncher/Utils/FriendsCache.cs create mode 100644 Wauncher/Utils/Game.cs create mode 100644 Wauncher/Utils/Patch.cs create mode 100644 Wauncher/Utils/ServerQuery.cs create mode 100644 Wauncher/Utils/Steam.cs create mode 100644 Wauncher/Utils/Terminal.cs create mode 100644 Wauncher/Utils/Version.cs create mode 100644 Wauncher/ViewModels/PatchNoteItem.cs create mode 100644 Wauncher/ViewModels/SettingsWindowViewModel.cs create mode 100644 Wauncher/Views/SettingsWindow.axaml create mode 100644 Wauncher/Views/SettingsWindow.axaml.cs create mode 100644 Wauncher/Wauncher.sln diff --git a/Wauncher/App.axaml b/Wauncher/App.axaml index 19649f3..a8ed31b 100644 --- a/Wauncher/App.axaml +++ b/Wauncher/App.axaml @@ -1,32 +1,48 @@ - - - - - - + xmlns:themes="using:Avalonia.Styling" + xmlns:fluent="using:Avalonia.Themes.Fluent"> + + + + + - - + + + + + + + - - - - - - - - - - - - - - - \ No newline at end of file + + + + #CC3A3A3A + White + #88FFFFFF + #55FFFFFF + #CCFFFFFF + #AAFFFFFF + #22FFFFFF + #33FFFFFF + #33FFFFFF + #22FFFFFF + #11FFFFFF + #44FFFFFF + #99FFFFFF + #33FFFFFF + #22FFFFFF + #223A3A3A + #FF3A3A3A + White + #6CB5F5 + + + + diff --git a/Wauncher/App.axaml.cs b/Wauncher/App.axaml.cs index ebaafb8..b85422c 100644 --- a/Wauncher/App.axaml.cs +++ b/Wauncher/App.axaml.cs @@ -1,10 +1,13 @@ -using Avalonia; +using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Platform; using CommunityToolkit.Mvvm.Input; -using Launcher.Utils; using Wauncher.Utils; +using System.Diagnostics; using Wauncher.ViewModels; using Wauncher.Views; @@ -12,10 +15,12 @@ namespace Wauncher { public partial class App : Application { + private TrayIcon? _trayIcon = null; + private NativeMenuItem? _discordRpcMenuItem = null; + public override void Initialize() { AvaloniaXamlLoader.Load(this); - Discord.Init(); ProtocolManager.RegisterURIHandler(); } @@ -23,53 +28,177 @@ public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - // Avoid duplicate validations from both Avalonia and the CommunityToolkit. - // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins DisableAvaloniaDataAnnotationValidation(); + + if (!Steam.IsInstalled()) + { + Wauncher.Utils.ConsoleManager.ShowError( + "Steam is required to use Wauncher.\n\nPlease install Steam and relaunch."); + desktop.Shutdown(); + return; + } + + if (!IsSteamRunning()) + { + Wauncher.Utils.ConsoleManager.ShowError( + "Steam must be open before using Wauncher.\n\nPlease open Steam, then relaunch Wauncher."); + desktop.Shutdown(); + return; + } + + bool hasRecentSteamUser = Steam.GetRecentLoggedInSteamID(false).GetAwaiter().GetResult(); + if (!hasRecentSteamUser) + { + Wauncher.Utils.ConsoleManager.ShowError( + "Steam is open, but no logged-in Steam account was detected.\n\nPlease sign in to Steam and relaunch Wauncher."); + desktop.Shutdown(); + return; + } + + // Always init so Discord username/avatar callbacks fire for the greeting. + // Presence is only pushed via Update() when RPC is enabled. + try + { + if (DependencyChecks.IsDiscordInstalled()) + Discord.Init(); + } + catch + { + // Discord integration is optional. + } + desktop.MainWindow = new MainWindow { DataContext = new MainWindowViewModel(), }; + desktop.Exit += (_, _) => _trayIcon?.Dispose(); } + SetupTrayIcon(); base.OnFrameworkInitializationCompleted(); } - private void DisableAvaloniaDataAnnotationValidation() + private static bool IsSteamRunning() { - // Get an array of plugins to remove - var dataValidationPluginsToRemove = - BindingPlugins.DataValidators.OfType().ToArray(); - - // remove each entry found - foreach (var plugin in dataValidationPluginsToRemove) + try { - BindingPlugins.DataValidators.Remove(plugin); + return Process.GetProcessesByName("steam").Length > 0; + } + catch + { + return false; } } + private void SetupTrayIcon() + { + var settings = SettingsWindowViewModel.LoadGlobal(); - [RelayCommand] - public void TrayIconClicked() + _discordRpcMenuItem = new NativeMenuItem + { + Header = settings.DiscordRpc ? "Discord RPC ON" : "Discord RPC OFF" + }; + _discordRpcMenuItem.Click += DiscordRpc_Click; + + var openItem = new NativeMenuItem { Header = "Open" }; + openItem.Click += (_, _) => ShowMainWindow(); + + var exitItem = new NativeMenuItem { Header = "Exit" }; + exitItem.Click += (_, _) => + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime d) + { + if (d.MainWindow is Views.MainWindow mw) + mw.ForceQuit(); + d.TryShutdown(); + } + }; + + var menu = new NativeMenu(); + menu.Items.Add(openItem); + menu.Items.Add(new NativeMenuItemSeparator()); + menu.Items.Add(_discordRpcMenuItem); + menu.Items.Add(new NativeMenuItemSeparator()); + menu.Items.Add(exitItem); + + _trayIcon = new TrayIcon + { + ToolTipText = "ClassicCounter", + Menu = menu, + }; + + try + { + var uri = new Uri("avares://Wauncher/Assets/Wauncher.ico"); + using var stream = AssetLoader.Open(uri); + _trayIcon.Icon = new WindowIcon(stream); + } + catch { } + + _trayIcon.Clicked += (_, _) => ShowMainWindow(); + + // Live sync + SettingsWindowViewModel.DiscordRpcChanged += enabled => ApplyDiscordRpc(enabled); + } + + private void ShowMainWindow() { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null) + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop + && desktop.MainWindow != null) { desktop.MainWindow.Show(); + desktop.MainWindow.WindowState = Avalonia.Controls.WindowState.Normal; desktop.MainWindow.Activate(); } } - public void ExitApplication_Click(object? sender, System.EventArgs e) + public void DiscordRpc_Click(object? sender, EventArgs e) { - switch (ApplicationLifetime) + var settings = SettingsWindowViewModel.LoadGlobal(); + settings.DiscordRpc = !settings.DiscordRpc; // auto-saves via OnDiscordRpcChanged + ApplyDiscordRpc(settings.DiscordRpc); + } + + private void ApplyDiscordRpc(bool enabled) + { + if (!DependencyChecks.IsDiscordInstalled()) + { + if (_discordRpcMenuItem != null) + _discordRpcMenuItem.Header = "Discord RPC (Discord not installed)"; + return; + } + + if (enabled) { - case IClassicDesktopStyleApplicationLifetime desktopLifetime: - desktopLifetime.TryShutdown(); - break; - case IControlledApplicationLifetime controlledLifetime: - controlledLifetime.Shutdown(); - break; + Discord.SetDetails("In Main Menu"); + Discord.SetState(null); + Discord.Update(); } + else + { + Discord.Deinitialize(); + } + + if (_discordRpcMenuItem != null) + _discordRpcMenuItem.Header = enabled ? "Discord RPC ON" : "Discord RPC OFF"; + } + + [RelayCommand] + public void TrayIconClicked() => ShowMainWindow(); + + public void ExitApplication_Click(object? sender, EventArgs e) + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime d) + d.TryShutdown(); + } + + private void DisableAvaloniaDataAnnotationValidation() + { + var toRemove = BindingPlugins.DataValidators + .OfType().ToArray(); + foreach (var plugin in toRemove) + BindingPlugins.DataValidators.Remove(plugin); } } -} \ No newline at end of file +} + diff --git a/Wauncher/Assets/Styles.axaml b/Wauncher/Assets/Styles.axaml index c0d156b..e0b55c4 100644 --- a/Wauncher/Assets/Styles.axaml +++ b/Wauncher/Assets/Styles.axaml @@ -48,13 +48,25 @@ + - + - + + + + + diff --git a/Wauncher/Assets/logo.png b/Wauncher/Assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ede32aaffd38b3034bac2ddf32569ca2702aa126 GIT binary patch literal 8314 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 \ No newline at end of file diff --git a/Wauncher/Assets/social_discord.svg b/Wauncher/Assets/social_discord.svg new file mode 100644 index 0000000..c5d1e6b --- /dev/null +++ b/Wauncher/Assets/social_discord.svg @@ -0,0 +1,2 @@ +Discord + diff --git a/Wauncher/Assets/social_github.svg b/Wauncher/Assets/social_github.svg new file mode 100644 index 0000000..14edd1e --- /dev/null +++ b/Wauncher/Assets/social_github.svg @@ -0,0 +1 @@ +GitHub diff --git a/Wauncher/Assets/social_instagram.png b/Wauncher/Assets/social_instagram.png new file mode 100644 index 0000000..4aa7e8f --- /dev/null +++ b/Wauncher/Assets/social_instagram.png @@ -0,0 +1 @@ +Instagram \ No newline at end of file diff --git a/Wauncher/Assets/social_instagram.svg b/Wauncher/Assets/social_instagram.svg new file mode 100644 index 0000000..d3e39b5 --- /dev/null +++ b/Wauncher/Assets/social_instagram.svg @@ -0,0 +1,2 @@ +Instagram + diff --git a/Wauncher/Assets/social_inventory.png b/Wauncher/Assets/social_inventory.png new file mode 100644 index 0000000..6016ee9 --- /dev/null +++ b/Wauncher/Assets/social_inventory.png @@ -0,0 +1 @@ +Counter-Strike \ No newline at end of file diff --git a/Wauncher/Assets/social_inventory.svg b/Wauncher/Assets/social_inventory.svg new file mode 100644 index 0000000..55087a8 --- /dev/null +++ b/Wauncher/Assets/social_inventory.svg @@ -0,0 +1,2 @@ +Counter-Strike + diff --git a/Wauncher/Assets/social_world.png b/Wauncher/Assets/social_world.png new file mode 100644 index 0000000..5771dd3 --- /dev/null +++ b/Wauncher/Assets/social_world.png @@ -0,0 +1 @@ +Internet Archive \ No newline at end of file diff --git a/Wauncher/Assets/social_world.svg b/Wauncher/Assets/social_world.svg new file mode 100644 index 0000000..1ef6907 --- /dev/null +++ b/Wauncher/Assets/social_world.svg @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/Wauncher/Program.cs b/Wauncher/Program.cs index b17b8c2..01b0298 100644 --- a/Wauncher/Program.cs +++ b/Wauncher/Program.cs @@ -5,7 +5,7 @@ namespace Wauncher { internal sealed class Program { - public static EventWaitHandle ProgramStarted; + public static EventWaitHandle? ProgramStarted; // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized @@ -13,41 +13,66 @@ internal sealed class Program [STAThread] public static void Main(string[] args) { - if (OnStartup(args) == false) + try { - Environment.Exit(0); - return; - } + if (OnStartup(args) == false) + { + Environment.Exit(0); + return; + } - BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } + catch (Exception ex) + { + try + { + var logPath = Path.Combine(Path.GetDirectoryName(System.Environment.ProcessPath) ?? ".", "wauncher_error.log"); + File.WriteAllText(logPath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n{ex}"); + } + catch { } + throw; + } } // Reference (COPYPASTA) // https://github.com/2dust/v2rayN/blob/d9843dc77502454b1ec48cec6244e115f1abd082/v2rayN/v2rayN.Desktop/Program.cs#L25-L52 private static bool OnStartup(string[]? Args) { - - if (Services.IsWindows()) + try { - var exePathKey = Services.GetMd5(Services.GetExePath()); - var rebootas = (Args ?? []).Any(t => t == "rebootas"); - ProgramStarted = new EventWaitHandle(false, EventResetMode.AutoReset, exePathKey, out var bCreatedNew); - if (!rebootas && !bCreatedNew) + if (Services.IsWindows()) + { + var exePathKey = Services.GetMd5(Services.GetExePath()); + var rebootas = (Args ?? []).Any(t => t == "rebootas"); + ProgramStarted = new EventWaitHandle(false, EventResetMode.AutoReset, exePathKey, out var bCreatedNew); + if (!rebootas && !bCreatedNew) + { + ProgramStarted?.Set(); + return false; + } + } + else { - ProgramStarted.Set(); - return false; + _ = new Mutex(true, "Wauncher", out var bOnlyOneInstance); + if (!bOnlyOneInstance) + { + return false; + } } + return true; } - else + catch (Exception ex) { - _ = new Mutex(true, "Wauncher", out var bOnlyOneInstance); - if (!bOnlyOneInstance) + try { - return false; + var logPath = Path.Combine(Path.GetDirectoryName(System.Environment.ProcessPath) ?? ".", "wauncher_startup_error.log"); + File.WriteAllText(logPath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\nOnStartup Error:\n{ex}"); } + catch { } + return true; // Allow app to continue anyway } - return true; } // Avalonia configuration, don't remove; also used by visual designer. diff --git a/Wauncher/Utils/Api.cs b/Wauncher/Utils/Api.cs new file mode 100644 index 0000000..d61df2b --- /dev/null +++ b/Wauncher/Utils/Api.cs @@ -0,0 +1,230 @@ +using Refit; +using Newtonsoft.Json; + +namespace Wauncher.Utils +{ + public class FullGameDownload + { + public required string File { get; set; } + public required string Link { get; set; } + public required string Hash { get; set; } + } + + public class FullGameDownloadResponse + { + public List? Files { get; set; } + public string? Error { get; set; } + } + + public interface IGitHub + { + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/repos/ClassicCounter/launcher/releases/latest")] + Task GetLatestRelease(); + + [Headers("User-Agent: ClassicCounter Wauncher", + "Accept: application/vnd.github.raw+json")] + [Get("/repos/ClassicCounter/launcher/contents/dependencies.json")] + Task GetDependencies(); + + [Headers("User-Agent: ClassicCounter Wauncher", + "Accept: application/vnd.github.raw+json")] + [Get("/repos/ClassicCounter/launcher/contents/carousel.json")] + Task GetCarouselManifest(); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/repos/ClassicCounter/launcher/contents/Wauncher/Assets")] + Task GetCarouselAssetsWauncher(); + + [Headers("User-Agent: ClassicCounter Wauncher", + "Accept: application/vnd.github.raw+json")] + [Get("/repos/ClassicCounter/launcher/contents/Wauncher/patchnotes.md")] + Task GetPatchNotesWauncher(); + } + + public class FriendInfo + { + [JsonProperty("steamid")] + public string SteamId { get; set; } = ""; + + [JsonProperty("steamid2")] + public string? SteamId2 + { + set + { + if (!string.IsNullOrWhiteSpace(value) && string.IsNullOrWhiteSpace(SteamId)) + SteamId = value; + } + } + + [JsonProperty("username")] + public string Username { get; set; } = ""; + + [JsonProperty("avatar_url")] + public string AvatarUrl { get; set; } = ""; + + [JsonProperty("avatar")] + public string? Avatar + { + set + { + if (!string.IsNullOrWhiteSpace(value)) + AvatarUrl = value; + } + } + + [JsonProperty("custom_username")] + public string? CustomUsername + { + set + { + if (!string.IsNullOrWhiteSpace(value)) + Username = value; + } + } + + [JsonProperty("custom_avatar")] + public string? CustomAvatar + { + set + { + if (!string.IsNullOrWhiteSpace(value)) + AvatarUrl = value; + } + } + + [JsonProperty("status")] + public string Status { get; set; } = "Offline"; // "Online" | "Offline" + + public string DotColor => Status == "Online" ? "#4CAF50" : "#888888"; + public bool IsOffline => Status == "Offline"; + public double AvatarOpacity => IsOffline ? 0.35 : 1.0; + public string StatusText => IsOffline ? "Offline" : "In Game"; + public string StatusColor => IsOffline ? "#666666" : "#999999"; + } + + public class FriendsResponse + { + public List? Friends { get; set; } + } + + public interface IEddies + { + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/friendsapi.php")] + Task GetFriends([AliasAs("steamid64")] string steamId64); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/friendsapi.php")] + Task GetFriendsBySteamId2([AliasAs("steamid2")] string steamId2); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/selfinfo.php")] + Task GetSelfInfo([AliasAs("steamid64")] string steamId64); + } + + public interface IClassicCounter + { + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/patch/get")] + Task GetPatches(); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/game/get")] + Task GetFullGameValidate(); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/game/full")] + Task GetFullGameDownload([Query] string steam_id); + } + + public static class Api + { + private static HttpClientHandler _httpClientHandler = new HttpClientHandler() + { + ServerCertificateCustomValidationCallback = (message, cert, chain, sslErrors) => true + }; + private static HttpClient ClassicCounterApiHttpClient = new HttpClient(_httpClientHandler) + { + BaseAddress = new Uri("https://classiccounter.cc/api") + }; + private static RefitSettings _settings = new RefitSettings(new NewtonsoftJsonContentSerializer()); + public static IGitHub GitHub = RestService.For("https://api.github.com", _settings); + public static IClassicCounter ClassicCounter = Argument.Exists("--ssl-bypass") + ? RestService.For(ClassicCounterApiHttpClient, _settings) + : RestService.For("https://classiccounter.cc/api", _settings); // THIS IS NOT IDEAL, CHANGE THE WORLD OF TRUSTED CERTIFICATES + public static IEddies Eddies = RestService.For("https://eddies.cc/api", _settings); + + public static List ParseFriendsPayload(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return new List(); + + try + { + var wrapped = JsonConvert.DeserializeObject(json); + if (wrapped?.Friends != null && wrapped.Friends.Count > 0) + return NormalizeFriends(wrapped.Friends); + } + catch + { + // Fall through to array parse. + } + + try + { + var flat = JsonConvert.DeserializeObject>(json); + if (flat != null) + return NormalizeFriends(flat); + } + catch + { + // Ignore and return empty. + } + + return new List(); + } + + public static FriendInfo? ParseSelfInfoPayload(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return null; + + try + { + var parsed = JsonConvert.DeserializeObject(json); + if (parsed == null) + return null; + + return NormalizeFriends(new[] { parsed }).FirstOrDefault(); + } + catch + { + return null; + } + } + + private static List NormalizeFriends(IEnumerable friends) + { + var normalized = new List(); + foreach (var f in friends) + { + var username = string.IsNullOrWhiteSpace(f.Username) ? "Unknown" : f.Username; + var status = string.Equals(f.Status, "Online", StringComparison.OrdinalIgnoreCase) + ? "Online" + : "Offline"; + + normalized.Add(new FriendInfo + { + SteamId = f.SteamId ?? string.Empty, + Username = username, + AvatarUrl = f.AvatarUrl ?? string.Empty, + Status = status + }); + } + + return normalized; + } + } +} + diff --git a/Wauncher/Utils/Argument.cs b/Wauncher/Utils/Argument.cs new file mode 100644 index 0000000..81e7bda --- /dev/null +++ b/Wauncher/Utils/Argument.cs @@ -0,0 +1,68 @@ +namespace Wauncher.Utils +{ + public static class Argument + { + private static List _launcherArguments = new() + { + "--debug-mode", + "--skip-updates", + "--skip-validating", + "--validate-all", + "--patch-only", + "--gc", + "--disable-rpc", + "--install-dependencies", + "--protocol-command", + "--ssl-bypass" + }; + + private static List _additionalArguments = new(); + public static void AddArgument(string argument) + { + if (!_additionalArguments.Any(a => string.Equals(a, argument, StringComparison.OrdinalIgnoreCase))) + _additionalArguments.Add(argument); + } + + public static void ClearAdditionalArguments() + { + _additionalArguments.Clear(); + } + + public static bool Exists(string argument) + { + IEnumerable arguments = Environment.GetCommandLineArgs(); + + foreach (string arg in arguments) + if (arg.ToLowerInvariant() == argument) return true; + + return false; + } + + public static List GenerateGameArguments(bool passLauncherArguments = false) + { + IEnumerable launcherArguments = Environment.GetCommandLineArgs(); + List gameArguments = new(); + + foreach (string arg in launcherArguments) + if (arg.StartsWith("cc://")) + { + string protocolArgument = arg.Replace("cc://", ""); + string[] protocolArguments = protocolArgument.Split('/'); + switch (protocolArguments[0]) + { + case "connect": + gameArguments.Add("+" + protocolArguments[0]); + gameArguments.Add(protocolArguments[1]); + break; + } + } + else if ((passLauncherArguments || !_launcherArguments.Contains(arg.ToLowerInvariant())) + && !arg.EndsWith(".exe")) + gameArguments.Add(arg.ToLowerInvariant()); + + gameArguments.AddRange(_additionalArguments); + return gameArguments; + } + } +} + diff --git a/Wauncher/Utils/AvatarCache.cs b/Wauncher/Utils/AvatarCache.cs new file mode 100644 index 0000000..cb1fdff --- /dev/null +++ b/Wauncher/Utils/AvatarCache.cs @@ -0,0 +1,112 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text; + +namespace Wauncher.Utils +{ + public static class AvatarCache + { + private static readonly HttpClient _http = new(); + private static readonly ConcurrentDictionary _inFlight = new(); + private static readonly string _cacheDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache", + "avatars"); + + private const int MaxAvatarBytes = 20 * 1024 * 1024; // 20 MB + + public static string GetDisplaySource(string? avatarUrl) + { + if (string.IsNullOrWhiteSpace(avatarUrl)) + return string.Empty; + + var cachedPath = GetCachePath(avatarUrl); + if (File.Exists(cachedPath)) + return new Uri(cachedPath).AbsoluteUri; + + QueueWarmCache(avatarUrl); + return avatarUrl; + } + + public static void QueueWarmCache(string? avatarUrl) + { + if (string.IsNullOrWhiteSpace(avatarUrl)) + return; + + if (!_inFlight.TryAdd(avatarUrl, 0)) + return; + + _ = Task.Run(async () => + { + try + { + await EnsureCachedAsync(avatarUrl); + } + catch + { + // Best-effort cache warmup only. + } + finally + { + _inFlight.TryRemove(avatarUrl, out _); + } + }); + } + + private static async Task EnsureCachedAsync(string avatarUrl) + { + var cachePath = GetCachePath(avatarUrl); + if (File.Exists(cachePath)) + return; + + Directory.CreateDirectory(_cacheDir); + + using var response = await _http.GetAsync(avatarUrl, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + + await using var input = await response.Content.ReadAsStreamAsync(); + var tempPath = cachePath + ".tmp"; + await using var output = File.Create(tempPath); + + var buffer = new byte[81920]; + int read; + int total = 0; + while ((read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length))) > 0) + { + total += read; + if (total > MaxAvatarBytes) + throw new InvalidDataException("Avatar exceeds size limit."); + + await output.WriteAsync(buffer.AsMemory(0, read)); + } + + output.Close(); + File.Move(tempPath, cachePath, overwrite: true); + } + + private static string GetCachePath(string avatarUrl) + { + var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(avatarUrl))).ToLowerInvariant(); + var ext = GetExtensionFromUrl(avatarUrl); + return Path.Combine(_cacheDir, $"{hash}{ext}"); + } + + private static string GetExtensionFromUrl(string avatarUrl) + { + try + { + var uri = new Uri(avatarUrl); + var ext = Path.GetExtension(uri.AbsolutePath); + if (!string.IsNullOrWhiteSpace(ext) && ext.Length <= 6) + return ext.ToLowerInvariant(); + } + catch + { + // ignore and fall back + } + return ".img"; + } + } +} diff --git a/Wauncher/Utils/Console.cs b/Wauncher/Utils/Console.cs new file mode 100644 index 0000000..db9a17e --- /dev/null +++ b/Wauncher/Utils/Console.cs @@ -0,0 +1,31 @@ +using System.Runtime.InteropServices; + +namespace Wauncher.Utils +{ + public static class ConsoleManager + { + [DllImport("kernel32.dll")] + private static extern IntPtr GetConsoleWindow(); + + [DllImport("user32.dll")] + private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type); + + private const int SW_HIDE = 0; + private const int SW_SHOW = 5; + private const uint MB_ICONERROR = 0x00000010; + + private static IntPtr ConsoleHandle = GetConsoleWindow(); + + public static void HideConsole() => ShowWindow(ConsoleHandle, SW_HIDE); + public static void ShowConsole() => ShowWindow(ConsoleHandle, SW_SHOW); + + public static void ShowError(string message) + { + if (OperatingSystem.IsWindows()) + MessageBox(IntPtr.Zero, message, "ClassicCounter Error", MB_ICONERROR); + } + } +} diff --git a/Wauncher/Utils/Debug.cs b/Wauncher/Utils/Debug.cs new file mode 100644 index 0000000..859b2de --- /dev/null +++ b/Wauncher/Utils/Debug.cs @@ -0,0 +1,8 @@ +namespace Wauncher.Utils +{ + public static class Debug + { + public static bool Enabled() => Argument.Exists("--debug-mode"); + } +} + diff --git a/Wauncher/Utils/Dependency.cs b/Wauncher/Utils/Dependency.cs new file mode 100644 index 0000000..f2638d2 --- /dev/null +++ b/Wauncher/Utils/Dependency.cs @@ -0,0 +1,126 @@ +using Microsoft.Win32; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Spectre.Console; +using System.Diagnostics; + +namespace Wauncher.Utils +{ + public class Dependency // to everyone seeing this: I am sorry, I think I am doing my best copying the rest of the code :innocent: + { + [JsonProperty(PropertyName = "name")] + public required string Name { get; set; } + + [JsonProperty(PropertyName = "download_url")] + public string? URL { get; set; } + + [JsonProperty(PropertyName = "path")] + public required string Path { get; set; } + + [JsonProperty(PropertyName = "registry")] + public required List RegistryList { get; set; } + + public class Registry + { + [JsonProperty(PropertyName = "path")] + public required string Path { get; set; } + + [JsonProperty(PropertyName = "key")] + public required string Key { get; set; } + + [JsonProperty(PropertyName = "value")] + public required string Value { get; set; } + } + } + + public class Dependencies(bool success, List localDependencies, List remoteDependencies) + { + public bool Success = success; + public List LocalDependencies = localDependencies; + public List RemoteDependencies = remoteDependencies; + } + + public static class DependencyManager + { + private static Process? _process; + public static string directory = Directory.GetCurrentDirectory(); + + public async static Task> Get() + { + List dependencies = new List(); + + if (Debug.Enabled()) + Terminal.Debug("Getting list of dependencies."); + try + { + string responseString = await Api.GitHub.GetDependencies(); + + JObject responseJson = JObject.Parse(responseString); + + if (responseJson["files"] != null) + dependencies = responseJson["files"]!.ToObject()!.ToList(); + } + catch + { + if (Debug.Enabled()) + Terminal.Debug("Couldn't get list of dependencies."); + } + return dependencies; + } + + public static bool IsInstalled(StatusContext ctx, Dependency dependency) + { + Dependency.Registry registry = dependency.RegistryList.First(); + using (RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)) + { + using (RegistryKey? key = hklm.OpenSubKey($@"{registry.Path}")) + { + string? keyValue = key?.GetValue(registry.Key) as string; + if (keyValue != registry.Value) + { + Terminal.Warning($"{dependency.Name} is installed already!"); + return true; + } + else + return false; + } + } + } + + public async static Task Install(StatusContext ctx, Dependencies dependencies) + { + _process = new Process(); + bool success = false; + + List allDependencies = new List( + dependencies.LocalDependencies.Count + + dependencies.RemoteDependencies.Count); + allDependencies.AddRange(dependencies.LocalDependencies); + allDependencies.AddRange(dependencies.RemoteDependencies); + foreach (Dependency dependency in allDependencies) + { + if (Debug.Enabled()) + Terminal.Debug($"Executing dependency installer: {dependency.Name}"); + _process.StartInfo.FileName = $"{directory}{dependency.Path}"; + _process.StartInfo.UseShellExecute = true; + _process.StartInfo.Verb = "runas"; + try + { + _process.Start(); + await _process.WaitForExitAsync(); + if (Debug.Enabled()) + Terminal.Debug($"Dependency installer {dependency.Name} has exited with status code {_process.ExitCode}"); + success = true; + } + catch + { + if (Debug.Enabled()) + Terminal.Debug($"Couldn't execute setup for dependency: {dependency.Name}"); + success = false; + } + } + return success; + } + } +} + diff --git a/Wauncher/Utils/DependencyChecks.cs b/Wauncher/Utils/DependencyChecks.cs new file mode 100644 index 0000000..6e8a930 --- /dev/null +++ b/Wauncher/Utils/DependencyChecks.cs @@ -0,0 +1,41 @@ +using Microsoft.Win32; + +namespace Wauncher.Utils +{ + public static class DependencyChecks + { + public static bool IsDiscordInstalled() + { + if (!OperatingSystem.IsWindows()) + return true; + + if (HasDiscordProtocolCommand(Registry.CurrentUser) || HasDiscordProtocolCommand(Registry.LocalMachine)) + return true; + + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + + string[] candidates = + { + Path.Combine(localAppData, "Discord", "Update.exe"), + Path.Combine(localAppData, "DiscordCanary", "Update.exe"), + Path.Combine(localAppData, "DiscordPTB", "Update.exe"), + Path.Combine(programFiles, "Discord", "Update.exe"), + Path.Combine(programFilesX86, "Discord", "Update.exe"), + }; + + return candidates.Any(File.Exists); + } + + private static bool HasDiscordProtocolCommand(RegistryKey root) + { + using var key = + root.OpenSubKey(@"Software\Classes\discord\shell\open\command") ?? + root.OpenSubKey(@"SOFTWARE\Classes\discord\shell\open\command"); + + var command = key?.GetValue(string.Empty) as string; + return !string.IsNullOrWhiteSpace(command); + } + } +} diff --git a/Wauncher/Utils/Discord.cs b/Wauncher/Utils/Discord.cs new file mode 100644 index 0000000..c1c612a --- /dev/null +++ b/Wauncher/Utils/Discord.cs @@ -0,0 +1,86 @@ +using DiscordRPC; +using DiscordRPC.Logging; +using DiscordRPC.Message; +using System; + +namespace Wauncher.Utils +{ + public static class Discord + { + private static readonly string _appId = "1133457462024994947"; + private static DiscordRpcClient _client = new DiscordRpcClient(_appId); + private static RichPresence _presence = new RichPresence(); + public static string? CurrentUserId { get; private set; } + public static string? CurrentUserAvatar { get; private set; } + public static string? CurrentUserUsername { get; private set; } + + public static void Init() + { + _client.OnReady += OnReady; + + _client.Logger = new ConsoleLogger() + { + Level = Debug.Enabled() ? LogLevel.Warning : LogLevel.None + }; + + if (!_client.Initialize()) + return; + + SetDetails("In Launcher"); + SetTimestamp(DateTime.UtcNow); + SetLargeArtwork("icon"); + + Update(); + } + + public static void Deinitialize() + { + if (!_client.IsDisposed) + { + // SetPresence(null) clears the presence from Discord immediately. + // Do NOT call Deinitialize/Dispose here — that prevents ClearPresence + // from flushing, so the presence stays visible in Discord. + _client.SetPresence(null); + } + } + + public static void Update() => _client.SetPresence(_presence); + + public static void SetDetails(string? details) => _presence.Details = details; + public static void SetState(string? state) => _presence.State = state; + + public static void SetTimestamp(DateTime? time) + { + if (_presence.Timestamps == null) _presence.Timestamps = new(); + _presence.Timestamps.Start = time; + } + + public static void SetLargeArtwork(string? key) + { + if (_presence.Assets == null) _presence.Assets = new(); + _presence.Assets.LargeImageKey = key; + } + + public static void SetSmallArtwork(string? key) + { + if (_presence.Assets == null) _presence.Assets = new(); + _presence.Assets.SmallImageKey = key; + } + + private static void OnReady(object sender, ReadyMessage e) + { + CurrentUserId = e.User.ID.ToString(); + CurrentUserAvatar = e.User.GetAvatarURL(User.AvatarFormat.PNG); + CurrentUserUsername = e.User.Username; + OnAvatarUpdate?.Invoke(CurrentUserAvatar); + OnUsernameUpdate?.Invoke(CurrentUserUsername); + + if (Debug.Enabled()) + Terminal.Debug($"Discord RPC: User is ready => @{e.User.Username} ({e.User.ID})"); + } + + public static event Action? OnAvatarUpdate; + public static event Action? OnUsernameUpdate; + } +} + diff --git a/Wauncher/Utils/Download.cs b/Wauncher/Utils/Download.cs new file mode 100644 index 0000000..50d06ae --- /dev/null +++ b/Wauncher/Utils/Download.cs @@ -0,0 +1,837 @@ +using Downloader; +using Refit; +using Spectre.Console; +using System.Diagnostics; +using System.Text.RegularExpressions; + +namespace Wauncher.Utils +{ + public static class DownloadManager + { + private static readonly DownloadConfiguration _settings = new() + { + ChunkCount = 8, + ParallelDownload = true + }; + // Shared only for DownloadUpdater / DownloadDependencies (console-launcher, always sequential) + private static DownloadService _downloader = new DownloadService(_settings); + + public static async Task DownloadUpdater(string path) + { + await _downloader.DownloadFileTaskAsync( + $"https://github.com/ClassicCounter/updater/releases/download/updater/updater.exe", + path + ); + } + + public static async Task DownloadDependencies(StatusContext ctx, List dependencies) + { + List local = new List(); + List remote = new List(); + Dependencies? _dependencies; + foreach (var dependency in dependencies) + { + if (!DependencyManager.IsInstalled(ctx, dependency)) + { + if (dependency.URL != null) + { + string path = Directory.GetCurrentDirectory() + dependency.Path; + if (File.Exists(path)) + File.Delete(path); + if (Debug.Enabled()) + Terminal.Debug($"Downloading {dependency.Name}"); + await _downloader.DownloadFileTaskAsync( + $"{dependency.URL}", + $"{Directory.GetCurrentDirectory()}{dependency.Path}"); + remote.Add(dependency); + } + else + { + local.Add(dependency); + } + } + } + _dependencies = new Dependencies(false, local, remote); + return _dependencies; + } + + public static async Task DownloadPatch( + Patch patch, + bool validateAll = false, + Action? onProgress = null, + Action? onExtract = null, + Action? onExtractProgress = null) + { + string originalFileName = patch.File.EndsWith(".7z") ? patch.File[..^3] : patch.File; + string downloadPath = $"{Directory.GetCurrentDirectory()}/{patch.File}"; + + if (Debug.Enabled()) + Terminal.Debug($"Starting download of: {patch.File}"); + + if (patch.File.EndsWith(".7z") && File.Exists(downloadPath)) + { + try + { + if (Debug.Enabled()) + Terminal.Debug($"Found existing .7z file, trying to delete: {downloadPath}"); + File.Delete(downloadPath); + } + catch (Exception ex) + { + if (Debug.Enabled()) + Terminal.Debug($"Failed to delete existing .7z file: {ex.Message}"); + } + } + + string baseUrl = "https://patch.classiccounter.cc"; + + // Use a fresh DownloadService per call so concurrent or back-to-back downloads + // never share state on the same instance. + using var downloader = new DownloadService(_settings); + if (onProgress != null) + downloader.DownloadProgressChanged += (sender, e) => onProgress(e); + + await downloader.DownloadFileTaskAsync( + $"{baseUrl}/{patch.File}", + $"{Directory.GetCurrentDirectory()}/{patch.File}" + ); + + if (patch.File.EndsWith(".7z")) + { + if (Debug.Enabled()) + Terminal.Debug($"Download complete, starting extraction of: {patch.File}"); + onExtract?.Invoke(); + string extractPath = $"{Directory.GetCurrentDirectory()}/{originalFileName}"; + await Extract7z(downloadPath, extractPath, onExtractProgress); + } + } + + public static async Task HandlePatches(Patches patches, StatusContext ctx, bool isGameFiles, int startingProgress = 0) + { + string fileType = isGameFiles ? "game file" : "patch"; + string fileTypePlural = isGameFiles ? "game files" : "patches"; + + var allFiles = patches.Missing.Concat(patches.Outdated).ToList(); + int totalFiles = allFiles.Count; + int completedFiles = startingProgress; + int failedFiles = 0; + + // status update + Action updateStatus = (progress, filename) => + { + var speed = progress.BytesPerSecondSpeed / (1024.0 * 1024.0); + var progressText = $"{((float)completedFiles / totalFiles * 100):F1}% ({completedFiles}/{totalFiles})"; + var status = filename.EndsWith(".7z") && progress.ProgressPercentage >= 100 ? "Extracting" : "Downloading new"; + ctx.Status = $"{status} {fileTypePlural}{GetDots().PadRight(3)} [gray]|[/] {progressText} [gray]|[/] {GetProgressBar(progress.ProgressPercentage)} {progress.ProgressPercentage:F1}% [gray]|[/] {speed:F1} MB/s"; + }; + + foreach (var patch in allFiles) + { + try + { + await DownloadPatch(patch, isGameFiles, progress => updateStatus(progress, patch.File)); + completedFiles++; + } + catch + { + failedFiles++; + Terminal.Warning($"Couldn't process {fileType}: {patch.File}, possibly due to missing permissions."); + } + } + + if (failedFiles > 0) + Terminal.Warning($"Couldn't download {failedFiles} {(failedFiles == 1 ? fileType : fileTypePlural)}!"); + } + + public static async Task DownloadFullGame(StatusContext ctx) + { + try + { + await Steam.GetRecentLoggedInSteamID(); + if (string.IsNullOrEmpty(Steam.recentSteamID2)) + { + Terminal.Error("Steam does not seem to be installed. Please make sure that you have Steam installed."); + Terminal.Error("Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + return; + } + + // pass steam id to api + var gameFiles = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); + + if (gameFiles?.Files == null || gameFiles.Files.Count == 0) + { + Terminal.Error("No game files returned from the API. You may not be whitelisted."); + Terminal.Error("Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + return; + } + + int totalFiles = gameFiles.Files.Count; + int completedFiles = 0; + List failedFiles = new List(); + + foreach (var file in gameFiles.Files) + { + string filePath = Path.Combine(Directory.GetCurrentDirectory(), file.File); + bool needsDownload = true; + + if (File.Exists(filePath)) + { + string fileHash = CalculateMD5(filePath); + if (fileHash.Equals(file.Hash, StringComparison.OrdinalIgnoreCase)) + { + needsDownload = false; + completedFiles++; + continue; + } + } + + if (needsDownload) + { + try + { + EventHandler progressHandler = (sender, e) => + { + var speed = e.BytesPerSecondSpeed / (1024.0 * 1024.0); + var progressText = $"{((float)completedFiles / totalFiles * 100):F1}% ({completedFiles}/{totalFiles})"; + ctx.Status = $"Downloading {file.File}{GetDots().PadRight(3)} [gray]|[/] {progressText} [gray]|[/] {GetProgressBar(e.ProgressPercentage)} {e.ProgressPercentage:F1}% [gray]|[/] {speed:F1} MB/s"; + }; + _downloader.DownloadProgressChanged += progressHandler; + + try + { + await _downloader.DownloadFileTaskAsync( + file.Link, + filePath + ); + + string downloadedHash = CalculateMD5(filePath); + if (!downloadedHash.Equals(file.Hash, StringComparison.OrdinalIgnoreCase)) + { + failedFiles.Add(file.File); + Terminal.Error($"Hash mismatch for {file.File}"); + continue; + } + + completedFiles++; + } + finally + { + _downloader.DownloadProgressChanged -= progressHandler; + } + } + catch (Exception ex) + { + failedFiles.Add(file.File); + Terminal.Error($"Failed to download {file.File}: {ex.Message}"); + } + } + } + + if (failedFiles.Count == 0) + { + string extractPath = Directory.GetCurrentDirectory(); + string tempExtractPath = Path.Combine(extractPath, "ClassicCounter_temp"); + + // check for running 7za.exe processes + var processes = Process.GetProcessesByName("7za"); + if (processes.Length > 0) + { + if (Debug.Enabled()) + Terminal.Debug("Found running 7za.exe process, waiting..."); + + // wait for existing 7za.exe to finish + while (Process.GetProcessesByName("7za").Length > 0) + { + ctx.Status = "Found already running extraction. Waiting for it to complete..."; + await Task.Delay(1000); + } + + // this is just code from ExtractSplitArchive (the moving folder part) + string classicCounterPath = Path.Combine(tempExtractPath, "ClassicCounter"); + if (Directory.Exists(tempExtractPath) && Directory.Exists(classicCounterPath)) + { + // check if the directory has any contents + if (Directory.GetFiles(classicCounterPath, "*.*", SearchOption.AllDirectories).Any()) + { + try + { + if (Debug.Enabled()) + Terminal.Debug("Moving contents from ClassicCounter folder to root directory..."); + + foreach (string dirPath in Directory.GetDirectories(classicCounterPath, "*", SearchOption.AllDirectories)) + { + string newDirPath = dirPath.Replace(classicCounterPath, extractPath); + Directory.CreateDirectory(newDirPath); + } + + foreach (string filePath in Directory.GetFiles(classicCounterPath, "*.*", SearchOption.AllDirectories)) + { + string newFilePath = filePath.Replace(classicCounterPath, extractPath); + + // skip launcher.exe + if (Path.GetFileName(filePath).Equals("launcher.exe", StringComparison.OrdinalIgnoreCase)) + { + if (Debug.Enabled()) + Terminal.Debug("Skipping launcher.exe"); + continue; + } + + try + { + if (File.Exists(newFilePath)) + { + File.Delete(newFilePath); + } + File.Move(filePath, newFilePath); + } + catch (Exception ex) + { + Terminal.Warning($"Failed to move file {filePath}: {ex.Message}"); + } + } + + // cleanup temp directory + try + { + Directory.Delete(tempExtractPath, true); + if (Debug.Enabled()) + Terminal.Debug("Deleted temporary extraction directory"); + } + catch (Exception ex) + { + Terminal.Warning($"Failed to cleanup temporary directory: {ex.Message}"); + } + + // cleanup .7z.xxx files + try + { + var splitArchiveFiles = Directory.GetFiles(extractPath, "*.7z.*") + .Where(f => Path.GetFileName(f).StartsWith("ClassicCounter.7z.")); + + foreach (var file in splitArchiveFiles) + { + try + { + File.Delete(file); + if (Debug.Enabled()) + Terminal.Debug($"Deleted split archive file: {file}"); + } + catch (Exception ex) + { + Terminal.Warning($"Failed to delete split archive file {file}: {ex.Message}"); + } + } + } + catch (Exception ex) + { + Terminal.Warning($"Failed to cleanup some split archive files: {ex.Message}"); + } + } + catch (Exception ex) + { + Terminal.Warning($"Some files may not have been moved correctly: {ex.Message}"); + } + } + else if (Debug.Enabled()) + { + Terminal.Debug("ClassicCounter folder exists but is empty, skipping file movement"); + } + } + else if (Debug.Enabled()) + { + Terminal.Debug("Temp directory or ClassicCounter folder not found, skipping file movement"); + } + + Terminal.Success("Extraction finished! Closing launcher..."); + Terminal.Warning("Make sure to run the launcher again if the game doesn't start afterwards."); + ctx.Status = "Done!"; + await Task.Delay(10000); + Environment.Exit(0); + } + + ctx.Status = "Extracting game files... Please do not close the launcher."; + await ExtractSplitArchive(gameFiles.Files.Select(f => f.File).ToList()); + Terminal.Success("Game files downloaded and extracted successfully!"); + } + else + { + Terminal.Error($"Failed to download {failedFiles.Count} files. Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + } + } + catch (ApiException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + Terminal.Error("You are not whitelisted on ClassicCounter! (https://classiccounter.cc/whitelist)"); + Terminal.Error("If you are whitelisted, check if you have Steam installed & you're logged into the whitelisted account."); + Terminal.Error("If you're still facing issues, use one of our other download links to download the game."); + Terminal.Warning("Closing launcher in 10 seconds..."); + await Task.Delay(10000); + Environment.Exit(1); + } + catch (ApiException ex) + { + Terminal.Error($"Failed to get game files from API: {ex.Message}"); + Terminal.Error("Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + } + catch (Exception ex) + { + Terminal.Error($"An error occurred: {ex.Message}"); + Terminal.Error("Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + } + } + + /// + /// Downloads and installs the full game from ClassicCounter's CDN. + /// Designed for use from a GUI — takes progress/status callbacks instead of a StatusContext. + /// Throws on error so the caller can handle it. + /// + public static async Task InstallFullGame( + Action? onProgress, // (filename, speed, totalPercent) + Action? onStatus, + Action? onExtractProgress = null) + { + await Steam.GetRecentLoggedInSteamID(); + if (string.IsNullOrEmpty(Steam.recentSteamID2)) + throw new Exception("Steam does not appear to be installed or you are not logged in."); + + onStatus?.Invoke("Fetching game files..."); + var gameFiles = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); + + if (gameFiles?.Files == null || gameFiles.Files.Count == 0) + throw new Exception("No game files returned. You may not be whitelisted.\nVisit classiccounter.cc/whitelist to request access."); + + int total = gameFiles.Files.Count; + int completed = 0; + + foreach (var file in gameFiles.Files) + { + string filePath = Path.Combine(Directory.GetCurrentDirectory(), file.File); + + if (File.Exists(filePath) && + CalculateMD5(filePath).Equals(file.Hash, StringComparison.OrdinalIgnoreCase)) + { + completed++; + onProgress?.Invoke(file.File, "", (double)completed / total * 100.0); + continue; + } + + using var downloader = new DownloadService(_settings); + downloader.DownloadProgressChanged += (s, e) => + onProgress?.Invoke( + file.File, + $"{e.BytesPerSecondSpeed / 1024.0 / 1024.0:F1} MB/s", + (completed + e.ProgressPercentage / 100.0) / total * 100.0); + + await downloader.DownloadFileTaskAsync(file.Link, filePath); + completed++; + } + + onStatus?.Invoke("Extracting game files..."); + await ExtractSplitArchive(gameFiles.Files.Select(f => f.File).ToList(), onExtractProgress); + } + + private static string CalculateMD5(string filename) + { + using (var md5 = System.Security.Cryptography.MD5.Create()) + using (var stream = File.OpenRead(filename)) + { + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + + // meant only for downloading whole game for now + // todo maybe make it more modular/allow other functions to use this + public static async Task ExtractSplitArchive(List files, Action? onProgress = null) + { + if (files == null || files.Count == 0) + { + throw new ArgumentException("No files provided for extraction"); + } + + files.Sort(); + + if (Debug.Enabled()) + { + Terminal.Debug($"Starting extraction of split archive:"); + foreach (var file in files) + { + Terminal.Debug($"Found part: {file}"); + } + } + + string firstFile = files[0]; + string extractPath = Directory.GetCurrentDirectory(); + string tempExtractPath = Path.Combine(extractPath, "ClassicCounter_temp"); + + try + { + Directory.CreateDirectory(tempExtractPath); + + await Download7za(); + + string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); + if (launcherDir == null) + { + throw new InvalidOperationException("Could not determine launcher directory"); + } + + string exePath = Path.Combine(launcherDir, "7za.exe"); + + using (var process = new Process()) + { + process.StartInfo = new ProcessStartInfo + { + FileName = exePath, + Arguments = $"x \"{firstFile}\" -o\"{tempExtractPath}\" -y", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + if (Debug.Enabled()) + Terminal.Debug($"Starting extraction to temp directory..."); + + process.OutputDataReceived += (_, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) return; + var pct = TryParseSevenZipProgress(e.Data); + if (pct.HasValue) onProgress?.Invoke(pct.Value); + }; + + process.Start(); + process.BeginOutputReadLine(); + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + throw new Exception($"7za extraction failed with exit code: {process.ExitCode}"); + } + } + + string classicCounterPath = Path.Combine(tempExtractPath, "ClassicCounter"); + if (Directory.Exists(classicCounterPath)) + { + if (Debug.Enabled()) + Terminal.Debug("Moving contents from ClassicCounter folder to root directory..."); + + // first, get all files and directories from the ClassicCounter folder + foreach (string dirPath in Directory.GetDirectories(classicCounterPath, "*", SearchOption.AllDirectories)) + { + // create directory in root, removing the "ClassicCounter" part from the path + string newDirPath = dirPath.Replace(classicCounterPath, extractPath); + Directory.CreateDirectory(newDirPath); + } + + foreach (string filePath in Directory.GetFiles(classicCounterPath, "*.*", SearchOption.AllDirectories)) + { + string newFilePath = filePath.Replace(classicCounterPath, extractPath); + + // skip launcher.exe + if (Path.GetFileName(filePath).Equals("launcher.exe", StringComparison.OrdinalIgnoreCase)) + { + if (Debug.Enabled()) + Terminal.Debug("Skipping launcher.exe"); + continue; + } + + try + { + if (File.Exists(newFilePath)) + { + File.Delete(newFilePath); + } + File.Move(filePath, newFilePath); + } + catch (Exception ex) + { + Terminal.Warning($"Failed to move file {filePath}: {ex.Message}"); + } + } + } + else + { + throw new DirectoryNotFoundException("ClassicCounter folder not found in extracted contents"); + } + + try + { + Directory.Delete(tempExtractPath, true); + if (Debug.Enabled()) + Terminal.Debug("Deleted temporary extraction directory"); + + foreach (string file in files) + { + File.Delete(file); + if (Debug.Enabled()) + Terminal.Debug($"Deleted archive part: {file}"); + } + } + catch (Exception ex) + { + Terminal.Warning($"Failed to cleanup some temporary files: {ex.Message}"); + } + + if (Debug.Enabled()) + Terminal.Debug("Extraction and file movement completed successfully!"); + } + catch (Exception ex) + { + Terminal.Error($"Extraction failed: {ex.Message}"); + if (Debug.Enabled()) + Terminal.Debug($"Stack trace: {ex.StackTrace}"); + + try + { + if (Directory.Exists(tempExtractPath)) + Directory.Delete(tempExtractPath, true); + } + catch { } + + throw; + } + } + + // FOR DOWNLOAD STATUS + public static int dotCount = 0; + public static DateTime lastDotUpdate = DateTime.Now; + public static string GetDots() + { + if ((DateTime.Now - lastDotUpdate).TotalMilliseconds > 500) + { + dotCount = (dotCount + 1) % 4; + lastDotUpdate = DateTime.Now; + } + return "...".Substring(0, dotCount); + } + public static string GetProgressBar(double percentage) + { + int blocks = 16; + int level = (int)(percentage / (100.0 / (blocks * 3))); + string bar = ""; + + for (int i = 0; i < blocks; i++) + { + int blockLevel = Math.Min(3, Math.Max(0, level - (i * 3))); + bar += blockLevel switch + { + 0 => "░", + 1 => "▒", + 2 => "▓", + 3 => "█", + _ => "█" + }; + } + return bar; + } + // DOWNLOAD STATUS OVER + + + + private static async Task Download7za() + { + string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); + if (launcherDir == null) + throw new InvalidOperationException("Could not determine launcher directory"); + + string exePath = Path.Combine(launcherDir, "7za.exe"); + if (File.Exists(exePath)) return; + + string[] fallbackUrls = + { + "https://fastdl.classiccounter.cc/7za.exe", + "https://ollumcc.github.io/7za.exe" + }; + + bool downloaded = false; + int retryCount = 0; + + while (!downloaded && retryCount < 10) + { + if (Debug.Enabled()) + Terminal.Debug($"7za.exe not found, downloading... (Attempt {retryCount + 1}/10)"); + + try + { + using var downloader = new DownloadService(_settings); + await downloader.DownloadFileTaskAsync(fallbackUrls[retryCount % fallbackUrls.Length], exePath); + + if (File.Exists(exePath)) + { + downloaded = true; + if (Debug.Enabled()) + Terminal.Debug($"Downloaded 7za.exe to: {exePath}"); + } + else + { + Terminal.Error($"Failed to download 7za.exe! Trying again... (Attempt {retryCount + 1})"); + retryCount++; + } + } + catch (Exception ex) + { + if (Debug.Enabled()) + Terminal.Debug($"Failed to download 7za.exe: {ex.Message}"); + retryCount++; + } + + if (retryCount > 0) + await Task.Delay(1000); + } + + if (!downloaded) + { + Terminal.Error("Couldn't download 7za.exe! Launcher will close in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + } + } + + private static async Task Extract7z(string archivePath, string outputPath, Action? onProgress = null) + { + try + { + if (!File.Exists(archivePath)) + { + if (Debug.Enabled()) + Terminal.Debug($"Archive file not found: {archivePath}"); + return; + } + + await Download7za(); + + string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); + if (launcherDir == null) + { + throw new InvalidOperationException("Could not determine launcher directory"); + } + + string exePath = Path.Combine(launcherDir, "7za.exe"); + + using (var process = new Process()) + { + process.StartInfo = new ProcessStartInfo + { + FileName = exePath, + Arguments = $"x \"{archivePath}\" -o\"{Path.GetDirectoryName(outputPath)}\" -y", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + if (Debug.Enabled()) + Terminal.Debug($"Starting extraction..."); + + process.OutputDataReceived += (_, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) return; + var pct = TryParseSevenZipProgress(e.Data); + if (pct.HasValue) onProgress?.Invoke(pct.Value); + }; + + process.Start(); + process.BeginOutputReadLine(); + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + throw new Exception($"7za extraction failed with exit code: {process.ExitCode}"); + } + + if (Debug.Enabled()) + Terminal.Debug("Extraction completed successfully!"); + } + + // delete 7z after extract + try + { + File.Delete(archivePath); + if (Debug.Enabled()) + Terminal.Debug($"Deleted archive file: {archivePath}"); + } + catch (Exception ex) + { + if (Debug.Enabled()) + Terminal.Debug($"Failed to delete archive file: {ex.Message}"); + } + } + catch (Exception ex) + { + Terminal.Error($"Extraction failed: {ex.Message}\nStack trace: {ex.StackTrace}"); + throw; + } + } + + private static readonly Regex SevenZipPercentRegex = new(@"\b(\d{1,3})%\b", RegexOptions.Compiled); + + private static double? TryParseSevenZipProgress(string line) + { + var match = SevenZipPercentRegex.Match(line); + if (!match.Success) return null; + if (!double.TryParse(match.Groups[1].Value, out var pct)) return null; + return Math.Clamp(pct, 0, 100); + } + + public static void Cleanup7zFiles() + { + try + { + string directory = Directory.GetCurrentDirectory(); + var files = Directory.GetFiles(directory, "*.7z", SearchOption.AllDirectories); + + foreach (string file in files) + { + try + { + File.Delete(file); + if (Debug.Enabled()) + Terminal.Debug($"Deleted .7z file: {file}"); + } + catch (Exception ex) + { + if (Debug.Enabled()) + Terminal.Debug($"Failed to delete .7z file {file}: {ex.Message}"); + } + } + + // Delete 7za.exe if it exists + string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); + if (launcherDir != null) + { + string sevenZaPath = Path.Combine(launcherDir, "7za.exe"); + if (File.Exists(sevenZaPath)) + { + try + { + File.Delete(sevenZaPath); + if (Debug.Enabled()) + Terminal.Debug("Deleted 7za.exe"); + } + catch (Exception ex) + { + if (Debug.Enabled()) + Terminal.Debug($"Failed to delete 7za.exe: {ex.Message}"); + } + } + } + } + catch (Exception ex) + { + if (Debug.Enabled()) + Terminal.Debug($"Failed to perform cleanup: {ex.Message}"); + } + } + } +} + diff --git a/Wauncher/Utils/FriendsCache.cs b/Wauncher/Utils/FriendsCache.cs new file mode 100644 index 0000000..6d2191a --- /dev/null +++ b/Wauncher/Utils/FriendsCache.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using Wauncher.Utils; + +namespace Wauncher.Utils +{ + public static class FriendsCache + { + private static readonly string _cacheDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache"); + + private static readonly string _cacheFile = Path.Combine(_cacheDir, "friends_cache.json"); + + private sealed class CachedFriend + { + public string Username { get; set; } = string.Empty; + public string AvatarUrl { get; set; } = string.Empty; + public string Status { get; set; } = "Offline"; + } + + private sealed class CacheEnvelope + { + public Dictionary> BySteamId { get; set; } = new(); + } + + public static async Task SaveAsync(string steamId, IEnumerable friends) + { + if (string.IsNullOrWhiteSpace(steamId)) + return; + + var envelope = LoadEnvelope(); + envelope.BySteamId[steamId] = friends.Select(f => new CachedFriend + { + Username = f.Username ?? string.Empty, + AvatarUrl = f.AvatarUrl ?? string.Empty, + Status = string.IsNullOrWhiteSpace(f.Status) ? "Offline" : f.Status + }).ToList(); + + Directory.CreateDirectory(_cacheDir); + var json = JsonSerializer.Serialize(envelope, new JsonSerializerOptions { WriteIndented = true }); + await File.WriteAllTextAsync(_cacheFile, json); + } + + public static List Load(string steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) + return new List(); + + var envelope = LoadEnvelope(); + if (!envelope.BySteamId.TryGetValue(steamId, out var cached) || cached == null) + return new List(); + + return cached.Select(c => new FriendInfo + { + Username = c.Username, + AvatarUrl = c.AvatarUrl, + Status = string.IsNullOrWhiteSpace(c.Status) ? "Offline" : c.Status + }).ToList(); + } + + private static CacheEnvelope LoadEnvelope() + { + try + { + if (!File.Exists(_cacheFile)) + return new CacheEnvelope(); + + var json = File.ReadAllText(_cacheFile); + if (string.IsNullOrWhiteSpace(json)) + return new CacheEnvelope(); + + return JsonSerializer.Deserialize(json) ?? new CacheEnvelope(); + } + catch + { + return new CacheEnvelope(); + } + } + } +} + diff --git a/Wauncher/Utils/Game.cs b/Wauncher/Utils/Game.cs new file mode 100644 index 0000000..7d27a67 --- /dev/null +++ b/Wauncher/Utils/Game.cs @@ -0,0 +1,162 @@ +using CSGSI; +using CSGSI.Nodes; +using System.Diagnostics; +using System.Net.NetworkInformation; + +namespace Wauncher.Utils +{ + public static class Game + { + private static Process? _process; + private static GameStateListener? _listener; + private static int _port; + private static MapNode? _node; + + private static string _map = "main_menu"; + private static int _scoreCT = 0; + private static int _scoreT = 0; + + public static async Task Launch() + { + List arguments = Argument.GenerateGameArguments(); + if (arguments.Count > 0) Terminal.Print($"Arguments: {string.Join(" ", arguments)}"); + + string directory = Directory.GetCurrentDirectory(); + Terminal.Print($"Directory: {directory}"); + + string gameStatePath = $"{directory}/csgo/cfg/gamestate_integration_cc.cfg"; + + if (!Argument.Exists("--disable-rpc")) + { + _port = GeneratePort(); + + if (Argument.Exists("--debug-mode")) + Terminal.Debug($"Starting Game State Integration with TCP port {_port}."); + + _listener = new($"http://localhost:{_port}/"); + _listener.NewGameState += OnNewGameState; + _listener.Start(); + + try { + await File.WriteAllTextAsync(gameStatePath, +@"""ClassicCounter"" +{ + ""uri"" ""http://localhost:" + _port + @""" + ""timeout"" ""5.0"" + ""auth"" + { + ""token"" """ + $"ClassicCounter {Version.Current}" + @""" + } + ""data"" + { + ""provider"" ""1"" + ""map"" ""1"" + ""round"" ""1"" + ""player_id"" ""1"" + ""player_weapons"" ""1"" + ""player_match_stats"" ""1"" + ""player_state"" ""1"" + ""allplayers_id"" ""1"" + ""allplayers_state"" ""1"" + ""allplayers_match_stats"" ""1"" + } +}" + ); + } + catch + { + Terminal.Error($"(!) \"/csgo/cfg/gamestate_integration_cc.cfg\" not found in the current directory!"); + } + } + else if (File.Exists(gameStatePath)) File.Delete(gameStatePath); + + _process = new Process(); + bool useGC = Argument.Exists("--gc"); + + if (Argument.Exists("--debug-mode")) + Terminal.Debug($"Launching the game with{(useGC ? "" : "out")} Game Coordinator..."); + + string gameExe = useGC ? "cc.exe" : "csgo.exe"; + _process.StartInfo.FileName = $"{directory}\\{gameExe}"; + _process.StartInfo.Arguments = string.Join(" ", arguments); + + if (!File.Exists(_process.StartInfo.FileName)) + { + Terminal.Error($"(!) {gameExe} not found in the current directory!"); + ConsoleManager.ShowError($"{gameExe} not found in the current directory!\n\nPlease make sure the launcher and game files are in the same folder."); + return false; + } + + return _process.Start(); + + } + + public static async Task Monitor() + { + if (_process == null) return; + + while (!_process.HasExited) + { + if (_node != null && _node.Name.Trim().Length != 0) + { + if (_map != _node.Name) + { + _map = _node.Name; + _scoreCT = _node.TeamCT.Score; + _scoreT = _node.TeamT.Score; + + Discord.SetDetails(_map); + Discord.SetState($"Score → {_scoreCT}:{_scoreT}"); + Discord.SetTimestamp(DateTime.UtcNow); + Discord.SetLargeArtwork($"https://assets.classiccounter.cc/maps/default/{_map}.jpg"); + Discord.SetSmallArtwork("icon"); + Discord.Update(); + } + + if (_scoreCT != _node.TeamCT.Score || _scoreT != _node.TeamT.Score) + { + _scoreCT = _node.TeamCT.Score; + _scoreT = _node.TeamT.Score; + + Discord.SetState($"Score → {_scoreCT}:{_scoreT}"); + Discord.Update(); + } + } + else if (_map != "main_menu") + { + _map = "main_menu"; + _scoreCT = 0; + _scoreT = 0; + + Discord.SetDetails("In Main Menu"); + Discord.SetState(null); + Discord.SetTimestamp(DateTime.UtcNow); + Discord.SetLargeArtwork("icon"); + Discord.SetSmallArtwork(null); + Discord.Update(); + } + + await Task.Delay(2000); + } + + _listener?.Stop(); + _listener = null; + } + + private static int GeneratePort() + { + int port = new Random().Next(1024, 65536); + + IPGlobalProperties properties = IPGlobalProperties.GetIPGlobalProperties(); + while (properties.GetActiveTcpConnections().Any(x => x.LocalEndPoint.Port == port)) + { + port = new Random().Next(1024, 65536); + } + + return port; + } + + public static void OnNewGameState(GameState gs) => _node = gs.Map; + } +} + diff --git a/Wauncher/Utils/Patch.cs b/Wauncher/Utils/Patch.cs new file mode 100644 index 0000000..9f48833 --- /dev/null +++ b/Wauncher/Utils/Patch.cs @@ -0,0 +1,240 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Concurrent; +using System.Security.Cryptography; + +namespace Wauncher.Utils +{ + public class Patch + { + [JsonProperty(PropertyName = "file")] + public required string File { get; set; } + + [JsonProperty(PropertyName = "hash")] + public required string Hash { get; set; } + }; + + public class Patches(bool success, List missing, List outdated) + { + public bool Success = success; + public List Missing = missing; + public List Outdated = outdated; + } + + public static class PatchManager + { + private static string GetOriginalFileName(string fileName) + { + return fileName.EndsWith(".7z") ? fileName[..^3] : fileName; + } + + private static async Task> GetPatches(bool validateAll = false) + { + List patches = new List(); + + try + { + string responseString = await Api.ClassicCounter.GetPatches(); + + JObject responseJson = JObject.Parse(responseString); + + if (responseJson["files"] != null) + patches = responseJson["files"]!.ToObject()!.ToList(); + } + catch + { + if (Debug.Enabled()) + Terminal.Debug($"Couldn't get {(validateAll ? "full game" : "patch")} API data."); + } + + return patches; + } + + private static async Task GetHash(string filePath) + { + using var md5 = MD5.Create(); + await using var stream = File.OpenRead(filePath); + byte[] hash = await Task.Run(() => md5.ComputeHash(stream)); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + + public static async Task ValidatePatches(bool validateAll = false, bool deleteOutdatedFiles = true) + { + List patches = await GetPatches(validateAll); + List missing = new(); + List outdated = new(); + Patch? dirPatch = null; + + // first only check pak_dat.vpk + var pakDatPatch = patches.FirstOrDefault(p => p.File == "csgo/pak_dat.vpk"); + bool skipValidation = false; + + if (pakDatPatch != null && !validateAll) + { + string pakDatPath = $"{Directory.GetCurrentDirectory()}/csgo/pak_dat.vpk"; + + if (Debug.Enabled()) + Terminal.Debug("Checking csgo/pak_dat.vpk first..."); + + if (File.Exists(pakDatPath)) + { + if (Debug.Enabled()) + Terminal.Debug("Checking hash for: csgo/pak_dat.vpk"); + + string pakDatHash = await GetHash(pakDatPath); + if (pakDatHash == pakDatPatch.Hash) + { + if (Debug.Enabled()) + Terminal.Debug("csgo/pak_dat.vpk is up to date - skipping other file checks"); + skipValidation = true; + return new Patches(true, missing, outdated); + } + else + { + if (Debug.Enabled()) + Terminal.Debug("csgo/pak_dat.vpk is outdated - will check all files"); + if (deleteOutdatedFiles) + File.Delete(pakDatPath); + } + } + else + { + if (Debug.Enabled()) + Terminal.Debug("Missing: csgo/pak_dat.vpk - will check all files"); + } + } + + if (!skipValidation) + { + // find pak01_dir.vpk from patch api + dirPatch = patches.FirstOrDefault(p => p.File.Contains("pak01_dir.vpk")); + bool needPak01Update = false; + + if (dirPatch != null) + { + string dirPath = $"{Directory.GetCurrentDirectory()}/csgo/pak01_dir.vpk"; + + if (Debug.Enabled()) + Terminal.Debug("Checking csgo/pak01_dir.vpk first..."); + + if (File.Exists(dirPath)) + { + if (Debug.Enabled()) + Terminal.Debug("Checking hash for: csgo/pak01_dir.vpk"); + + string dirHash = await GetHash(dirPath); + if (dirHash != dirPatch.Hash) + { + if (Debug.Enabled()) + Terminal.Debug("csgo/pak01_dir.vpk is outdated!"); + + if (deleteOutdatedFiles) + File.Delete(dirPath); + outdated.Add(dirPatch); + needPak01Update = true; + } + else if (!Argument.Exists("--validate-all")) + { + if (Debug.Enabled()) + Terminal.Debug("csgo/pak01_dir.vpk is up to date - will skip pak01 files"); + } + else + { + if (Debug.Enabled()) + Terminal.Debug("csgo/pak01_dir.vpk is up to date - checking all files anyway due to --validate-all"); + } + } + else + { + if (Debug.Enabled()) + Terminal.Debug("Missing: csgo/pak01_dir.vpk!"); + + missing.Add(dirPatch); + needPak01Update = true; + } + + if (!needPak01Update) + { + patches.Remove(dirPatch); + } + } + + var concurrentMissing = new ConcurrentBag(); + var concurrentOutdated = new ConcurrentBag(); + + var parallelOptions = new ParallelOptions + { + MaxDegreeOfParallelism = 4 + }; + + await Parallel.ForEachAsync(patches, parallelOptions, async (patch, cancellationToken) => + { + string originalFileName = GetOriginalFileName(patch.File); + + // skip dir file (we already checked it) + if (originalFileName.Contains("pak01_dir.vpk")) + return; + + // are you a pak01 file? + bool isPak01File = originalFileName.Contains("pak01_"); + string path = Path.Combine(Directory.GetCurrentDirectory(), originalFileName); + + if (isPak01File && !needPak01Update && !Argument.Exists("--validate-all")) + { + if (!File.Exists(path)) + { + if (Debug.Enabled()) + Terminal.Debug($"Missing: {originalFileName}"); + + concurrentMissing.Add(patch); + return; + } + + if (Debug.Enabled()) + Terminal.Debug($"Skipping hash check for: {originalFileName} (pak01_dir.vpk up to date)"); + + return; + } + + if (!File.Exists(path)) + { + if (Debug.Enabled()) + Terminal.Debug($"Missing: {originalFileName}"); + + concurrentMissing.Add(patch); + return; + } + + if (Debug.Enabled()) + Terminal.Debug($"Checking hash for: {originalFileName}{(isPak01File && Argument.Exists("--validate-all") ? " (--validate-all)" : "")}"); + + string hash = await GetHash(path); + if (hash != patch.Hash) + { + if (Debug.Enabled()) + Terminal.Debug($"Outdated: {originalFileName}"); + + if (deleteOutdatedFiles) + File.Delete(path); + concurrentOutdated.Add(patch); + } + }); + + missing.AddRange(concurrentMissing); + outdated.AddRange(concurrentOutdated); + + // if pak01_dir.vpk needs update, move it to end of lists + if (needPak01Update && dirPatch != null) + { + if (outdated.Remove(dirPatch)) + outdated.Add(dirPatch); + if (missing.Remove(dirPatch)) + missing.Add(dirPatch); + } + } + + return new Patches(patches.Count > 0, missing, outdated); + } + } +} + diff --git a/Wauncher/Utils/ProtocolManager.cs b/Wauncher/Utils/ProtocolManager.cs index b81f5ea..eba8ed5 100644 --- a/Wauncher/Utils/ProtocolManager.cs +++ b/Wauncher/Utils/ProtocolManager.cs @@ -5,8 +5,11 @@ namespace Wauncher.Utils public class ProtocolManager { public static void RegisterURIHandler() - { - var appCurrentLocation = Path.Combine(new FileInfo(System.Environment.ProcessPath).Directory.FullName, "wauncher.exe"); + { + var appCurrentLocation = Services.GetExePath(); + if (string.IsNullOrWhiteSpace(appCurrentLocation)) + return; + EnsureKeyExists(Registry.CurrentUser, "Software/Classes/cc", "ClassicCounter"); SetValue(Registry.CurrentUser, "Software/Classes/cc", "URL Protocol", string.Empty); EnsureKeyExists(Registry.CurrentUser, "Software/Classes/cc/DefaultIcon", $"{appCurrentLocation},1"); @@ -19,7 +22,7 @@ private static void SetValue(RegistryKey rootKey, string keys, string valueName, key.SetValue(valueName, value); } - private static RegistryKey EnsureKeyExists(RegistryKey rootKey, string keys, string defaultValue = null) + private static RegistryKey EnsureKeyExists(RegistryKey rootKey, string keys, string? defaultValue = null) { if (rootKey == null) { diff --git a/Wauncher/Utils/ServerQuery.cs b/Wauncher/Utils/ServerQuery.cs new file mode 100644 index 0000000..5b06876 --- /dev/null +++ b/Wauncher/Utils/ServerQuery.cs @@ -0,0 +1,112 @@ +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Wauncher.Utils +{ + public class ServerQueryResult + { + public bool Online { get; set; } + public int Players { get; set; } + public int MaxPlayers { get; set; } + public string Map { get; set; } = ""; + } + + public static class ServerQuery + { + private static readonly byte[] A2S_INFO_REQUEST = + { + 0xFF, 0xFF, 0xFF, 0xFF, 0x54, + 0x53, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x20, 0x45, 0x6E, 0x67, + 0x69, 0x6E, 0x65, 0x20, 0x51, 0x75, 0x65, 0x72, 0x79, 0x00 + }; + + public static async Task QueryAsync(string ipPort, int timeoutMs = 2000) + { + var result = new ServerQueryResult(); + try + { + var parts = ipPort.Split(':'); + string host = parts[0]; + int port = int.Parse(parts[1]); + + var addresses = await Dns.GetHostAddressesAsync(host); + if (addresses.Length == 0) return result; + + var endpoint = new IPEndPoint(addresses[0], port); + + using var udp = new UdpClient(); + + await udp.SendAsync(A2S_INFO_REQUEST, A2S_INFO_REQUEST.Length, endpoint); + + var cts = new CancellationTokenSource(timeoutMs); + var recv = await udp.ReceiveAsync(cts.Token); + byte[] data = recv.Buffer; + + // Some servers respond with a challenge packet (0x41) before sending the real info. + // Re-send the request with the 4-byte challenge appended and wait for the real reply. + if (data.Length >= 9 && data[4] == 0x41) + { + var challengeRequest = new byte[A2S_INFO_REQUEST.Length + 4]; + Buffer.BlockCopy(A2S_INFO_REQUEST, 0, challengeRequest, 0, A2S_INFO_REQUEST.Length); + Buffer.BlockCopy(data, 5, challengeRequest, A2S_INFO_REQUEST.Length, 4); + + cts = new CancellationTokenSource(timeoutMs); + await udp.SendAsync(challengeRequest, challengeRequest.Length, endpoint); + recv = await udp.ReceiveAsync(cts.Token); + data = recv.Buffer; + } + + // A2S_INFO response: 4×0xFF + 0x49 header, then null-terminated strings: + // [0] Server name [1] Map [2] Folder [3] Game then 2-byte AppID + // then Players, MaxPlayers, ... + if (data.Length < 6 || data[4] != 0x49) return result; + + int pos = 5; + + // Read each null-terminated string + string ReadString() + { + int start = pos; + while (pos < data.Length && data[pos] != 0x00) pos++; + var s = Encoding.UTF8.GetString(data, start, pos - start); + pos++; // skip null terminator + return s; + } + + ReadString(); // [0] Server name — skip + result.Map = ReadString(); // [1] Map name — keep + ReadString(); // [2] Folder — skip + ReadString(); // [3] Game — skip + + pos += 2; // AppID (2 bytes) + + if (pos + 2 > data.Length) return result; + + result.Players = data[pos]; + result.MaxPlayers = data[pos + 1]; + result.Online = true; + } + catch { /* timeout or unreachable = offline */ } + + return result; + } + + public static async Task RefreshServers(IEnumerable servers) + { + var tasks = servers + .Where(s => !s.IsNone) + .Select(async s => + { + var r = await QueryAsync(s.IpPort); + s.IsOnline = r.Online; + s.Players = r.Players; + s.MaxPlayers = r.MaxPlayers; + s.Map = r.Map; + // Each setter fires its own targeted PropertyChanged notifications. + }); + + await Task.WhenAll(tasks); + } + } +} diff --git a/Wauncher/Utils/Steam.cs b/Wauncher/Utils/Steam.cs new file mode 100644 index 0000000..4d96aa0 --- /dev/null +++ b/Wauncher/Utils/Steam.cs @@ -0,0 +1,98 @@ +using Microsoft.Win32; +using Gameloop.Vdf; + +namespace Wauncher.Utils +{ + public class Steam + { + public static string? recentSteamID64 { get; private set; } + public static string? recentSteamID2 { get; private set; } + + private static string? steamPath { get; set; } + private static string? GetSteamInstallPath() + { + using (RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)) + { + using (RegistryKey? key = hklm.OpenSubKey(@"SOFTWARE\Wow6432Node\Valve\Steam") ?? hklm.OpenSubKey(@"SOFTWARE\Valve\Steam")) + { + steamPath = key?.GetValue("InstallPath") as string; + if (Debug.Enabled()) + Terminal.Debug($"Steam folder found at {steamPath}"); + return steamPath; + } + } + } + + public static bool IsInstalled() + { + var path = GetSteamInstallPath(); + return !string.IsNullOrWhiteSpace(path) && Directory.Exists(path); + } + + public static async Task GetRecentLoggedInSteamID() + { + await GetRecentLoggedInSteamID(true); + } + + public static async Task GetRecentLoggedInSteamID(bool exitOnMissing) + { + recentSteamID64 = null; + recentSteamID2 = null; + + steamPath = GetSteamInstallPath(); + if (string.IsNullOrEmpty(steamPath) || !Directory.Exists(steamPath)) + { + if (!exitOnMissing) + return false; + + Terminal.Error("Your Steam install couldn't be found."); + Terminal.Error("Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + return false; + } + + var loginUsersPath = Path.Combine(steamPath, "config", "loginusers.vdf"); + if (!File.Exists(loginUsersPath)) + { + if (!exitOnMissing) + return false; + + Terminal.Error("Steam login data couldn't be found."); + Terminal.Error("Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + return false; + } + + dynamic loginUsers = VdfConvert.Deserialize(File.ReadAllText(loginUsersPath)); + foreach (var user in loginUsers.Value) + { + var mostRecent = user.Value.MostRecent.Value; + if (mostRecent == "1") + { + recentSteamID64 = user.Key; + recentSteamID2 = ConvertToSteamID2(user.Key); + } + } + if (Debug.Enabled() && !string.IsNullOrEmpty(recentSteamID64)) + { + Terminal.Debug($"Most recent Steam account (SteamID64): {recentSteamID64}"); + Terminal.Debug($"Most recent Steam account (SteamID2): {recentSteamID2}"); + } + + return !string.IsNullOrEmpty(recentSteamID2); + } + + private static string ConvertToSteamID2(string steamID64) + { + ulong id64 = ulong.Parse(steamID64); + ulong constValue = 76561197960265728; + ulong accountID = id64 - constValue; + ulong y = accountID % 2; + ulong z = accountID / 2; + return $"STEAM_1:{y}:{z}"; + } + } +} + diff --git a/Wauncher/Utils/Terminal.cs b/Wauncher/Utils/Terminal.cs new file mode 100644 index 0000000..ff23061 --- /dev/null +++ b/Wauncher/Utils/Terminal.cs @@ -0,0 +1,60 @@ +using Spectre.Console; +using System.Reflection; + +namespace Wauncher.Utils +{ + public static class Terminal + { + private static string _prefix = "[orange1]Classic[/][blue]Counter[/]"; + private static string _grey = "grey82"; + private static string _seperator = "[grey50]|[/]"; + private static readonly string _steamHappy = LoadSteamHappy(); + + private static string LoadSteamHappy() + { + try + { + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Wauncher.Assets.steamhappy.txt"); + if (stream == null) + return string.Empty; + + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + catch + { + return string.Empty; + } + } + + public static void Init() + { + AnsiConsole.MarkupLine($"{_prefix} {_seperator} [{_grey}]Wauncher maintained by [/][purple4_1]koolych[/][{_grey}][/]"); + AnsiConsole.MarkupLine($"{_prefix} {_seperator} [{_grey}]Coded by [/][lightcoral]heapy[/][{_grey}][/]"); + AnsiConsole.MarkupLine($"{_prefix} {_seperator} [{_grey}]https://github.com/ClassicCounter [/]"); + AnsiConsole.MarkupLine($"{_prefix} {_seperator} [{_grey}]Version: {Version.Current}[/]"); + } + + public static void Print(object? message) + => AnsiConsole.MarkupLine($"{_prefix} {_seperator} [{_grey}]{Markup.Escape(message?.ToString() ?? string.Empty)}[/]"); + + public static void Success(object? message) + => AnsiConsole.MarkupLine($"{_prefix} {_seperator} [green1]{Markup.Escape(message?.ToString() ?? string.Empty)}[/]"); + + public static void Warning(object? message) + => AnsiConsole.MarkupLine($"{_prefix} {_seperator} [yellow]{Markup.Escape(message?.ToString() ?? string.Empty)}[/]"); + + public static void Error(object? message) + => AnsiConsole.MarkupLine($"{_prefix} {_seperator} [red]{Markup.Escape(message?.ToString() ?? string.Empty)}[/]"); + + public static void Debug(object? message) + => AnsiConsole.MarkupLine($"[purple]{Markup.Escape(message?.ToString() ?? string.Empty)}[/]"); + + public static void SteamHappy() => + AnsiConsole.Write(_steamHappy); + + private static string Date() + => $"[{_grey}]{DateTime.Now.ToString("HH:mm:ss")}[/]"; + } +} + diff --git a/Wauncher/Utils/Version.cs b/Wauncher/Utils/Version.cs new file mode 100644 index 0000000..1c6ac2b --- /dev/null +++ b/Wauncher/Utils/Version.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json.Linq; +using System.Reflection; + +namespace Wauncher.Utils +{ + public static class Version + { + public static string Current => + Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0"; + + public async static Task GetLatestVersion() + { + if (Debug.Enabled()) + Terminal.Debug("Getting latest version."); + + try + { + string responseString = await Api.GitHub.GetLatestRelease(); + JObject responseJson = JObject.Parse(responseString); + + if (responseJson["tag_name"] == null) + throw new Exception("\"tag_name\" doesn't exist in response."); + + var tag = ((string?)responseJson["tag_name"] ?? Current).Trim(); + if (tag.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + tag = tag[1..]; + return string.IsNullOrWhiteSpace(tag) ? Current : tag; + } + catch + { + if (Debug.Enabled()) + Terminal.Debug("Couldn't get latest version."); + } + + return Current; + } + } +} diff --git a/Wauncher/ViewModels/MainWindowViewModel.cs b/Wauncher/ViewModels/MainWindowViewModel.cs index e64c68b..919db26 100644 --- a/Wauncher/ViewModels/MainWindowViewModel.cs +++ b/Wauncher/ViewModels/MainWindowViewModel.cs @@ -1,45 +1,568 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; -using Launcher.Utils; +using Wauncher.Utils; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Net.NetworkInformation; +using System.Text; +using System.Threading; +using FriendInfo = Wauncher.Utils.FriendInfo; namespace Wauncher.ViewModels { + public class ServerInfo : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + + public string Name { get; set; } = ""; + public string IpPort { get; set; } = ""; + + private int _players; + private int _maxPlayers; + private bool _isOnline; + private string _map = ""; + + public int Players + { + get => _players; + set + { + if (_players == value) return; + _players = value; + Notify(nameof(Players), nameof(PlayerCount)); + } + } + + public int MaxPlayers + { + get => _maxPlayers; + set + { + if (_maxPlayers == value) return; + _maxPlayers = value; + Notify(nameof(MaxPlayers), nameof(PlayerCount)); + } + } + + public bool IsOnline + { + get => _isOnline; + set + { + if (_isOnline == value) return; + _isOnline = value; + Notify(nameof(IsOnline), nameof(DotColor)); + } + } + + public string Map + { + get => _map; + set + { + if (_map == value) return; + _map = value; + Notify(nameof(Map), nameof(MapDisplay)); + } + } + + public bool IsNone => string.IsNullOrEmpty(IpPort); + + public string PlayerCount => IsNone ? "" : $"{Players}/{MaxPlayers}"; + public string DotColor => IsNone ? "Transparent" : (IsOnline ? "#4CAF50" : "#F44336"); + public string NameColor => IsNone ? "#66FFFFFF" : "White"; + public string MapDisplay => (!IsNone && !string.IsNullOrEmpty(Map)) ? Map : ""; + + private void Notify(params string[] names) + { + Dispatcher.UIThread.Post(() => + { + foreach (var name in names) + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + }); + } + } + public partial class MainWindowViewModel : ViewModelBase { - public string GameStatus { get; private set; } = "Game Status: "; - - public string ProtocolManager { get; private set; } = "Selected server: "; - + [ObservableProperty] + private string _gameStatus = "Not Running"; + + [ObservableProperty] + private string _protocolManager = "None"; + [ObservableProperty] private string _profilePicture = "https://avatars.githubusercontent.com/u/75831703?v=4"; [ObservableProperty] private string _usernameGreeting = "Hello, username"; - - public string WhitelistStatus { get; set; } = "Gray"; - + + [ObservableProperty] + private string _whitelistDotColor = "Gray"; + + [ObservableProperty] + private string _whitelistText = "Unknown"; + + [ObservableProperty] + private bool _isDropdownOpen = false; + + [ObservableProperty] + private string _activeRightTab = "Friends"; + + public bool IsFriendsTabActive => ActiveRightTab == "Friends"; + public bool IsPatchNotesTabActive => ActiveRightTab == "PatchNotes"; + + partial void OnActiveRightTabChanged(string value) + { + OnPropertyChanged(nameof(IsFriendsTabActive)); + OnPropertyChanged(nameof(IsPatchNotesTabActive)); + } + + [ObservableProperty] + private bool _isOfflineMode = false; + + public bool IsOnlineMode => !IsOfflineMode; + partial void OnIsOfflineModeChanged(bool value) => OnPropertyChanged(nameof(IsOnlineMode)); + + [ObservableProperty] + private bool _isUpdating = false; + + [ObservableProperty] + private bool _isInstalling = false; + + [ObservableProperty] + private bool _isNeedingInstall = false; + + [ObservableProperty] + private bool _isCheckingUpdates = false; + + public bool IsCheckingOrUpdating => IsCheckingUpdates || IsUpdating || IsInstalling; + public bool IsUpdatingOrInstalling => IsUpdating || IsInstalling; + + partial void OnIsCheckingUpdatesChanged(bool value) => OnPropertyChanged(nameof(IsCheckingOrUpdating)); + partial void OnIsInstallingChanged(bool value) + { + OnPropertyChanged(nameof(LaunchButtonText)); + OnPropertyChanged(nameof(IsCheckingOrUpdating)); + OnPropertyChanged(nameof(IsUpdatingOrInstalling)); + } + partial void OnIsNeedingInstallChanged(bool value) => OnPropertyChanged(nameof(LaunchButtonText)); + + [ObservableProperty] + private string _updateStatus = ""; + + [ObservableProperty] + private string _updateStatusFile = ""; + + [ObservableProperty] + private string _updateStatusSpeed = ""; + + [ObservableProperty] + private double _updateProgress = 0; + + [ObservableProperty] + private bool _updateIndeterminate = false; + + [ObservableProperty] + private bool _updateAvailable = false; + + public string LaunchButtonText => + IsInstalling ? "Installing Game..." : + IsUpdating ? "Updating..." : + IsNeedingInstall ? "Install Game" : + UpdateAvailable ? "Update" : + "Launch Game"; + + partial void OnIsUpdatingChanged(bool value) + { + OnPropertyChanged(nameof(LaunchButtonText)); + OnPropertyChanged(nameof(IsCheckingOrUpdating)); + OnPropertyChanged(nameof(IsUpdatingOrInstalling)); + } + partial void OnUpdateAvailableChanged(bool value) => OnPropertyChanged(nameof(LaunchButtonText)); + + [ObservableProperty] + private ServerInfo? _selectedServer; + + // What the SELECTED SERVER label shows + public string SelectedLabel => SelectedServer?.IsNone == false + ? SelectedServer.Name + : "Server not selected..."; + + public bool IsNoServerSelected => SelectedServer == null || SelectedServer.IsNone; + public bool IsServerSelected => SelectedServer != null && !SelectedServer.IsNone; + + public ObservableCollection Servers { get; } = new() + { + // ── None (clears selection) ────────────────────────────────────── + new ServerInfo { Name = "None", IpPort = "", IsOnline = false }, + + // ── Real servers ───────────────────────────────────────────────── + new ServerInfo { Name = "NA | PUG | 64 Tick", IpPort = "na.classiccounter.cc:27015", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "NA | PUG-2 | 64 Tick", IpPort = "na.classiccounter.cc:27016", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "EU | PUG | 64 Tick", IpPort = "eu.classiccounter.cc:27016", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "EU | PUG | 128 Tick", IpPort = "eu.classiccounter.cc:27015", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "EU | PUG-2 | 128 Tick",IpPort = "eu.classiccounter.cc:27022", Players = 0, MaxPlayers = 10, IsOnline = true }, + }; + + partial void OnSelectedServerChanged(ServerInfo? value) + { + // Update the label shown in the trigger button + ProtocolManager = (value == null || value.IsNone) ? "None" : value.Name; + OnPropertyChanged(nameof(SelectedLabel)); + OnPropertyChanged(nameof(IsNoServerSelected)); + OnPropertyChanged(nameof(IsServerSelected)); + } + public MainWindowViewModel() { if (Argument.Exists("--protocol-command")) + ProtocolManager = "Ready to Launch!"; + + _ = LoadSelfProfileAsync(); + + CheckWhitelistStatus(); + UpdateOfflineMode(); + NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; + + // Query servers immediately, then refresh every 30 seconds + _ = RefreshServersSafeAsync(); + _serverRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; + _serverRefreshTimer.Tick += async (_, _) => await RefreshServersSafeAsync(); + _serverRefreshTimer.Start(); + + // Query friends immediately, then refresh every 30 seconds + _ = RefreshFriendsSafeAsync(); + _friendsTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; + _friendsTimer.Tick += async (_, _) => await RefreshFriendsSafeAsync(); + _friendsTimer.Start(); + } + + private DispatcherTimer? _serverRefreshTimer; + private int _serverRefreshInProgress; + + // ── Friends ─────────────────────────────────────────────────────────────── + public ObservableCollection Friends { get; } = new(); + + [ObservableProperty] private bool _friendsShowStatus = true; + [ObservableProperty] private string _friendsStatus = "Loading..."; + [ObservableProperty] private bool _showNoFriendsState = false; + public bool ShowGenericFriendsStatus => FriendsShowStatus && !ShowNoFriendsState; + + partial void OnFriendsShowStatusChanged(bool value) => OnPropertyChanged(nameof(ShowGenericFriendsStatus)); + partial void OnShowNoFriendsStateChanged(bool value) => OnPropertyChanged(nameof(ShowGenericFriendsStatus)); + + private DispatcherTimer? _friendsTimer; + private int _friendsRefreshInProgress; + private string _lastRenderedFriendsSignature = string.Empty; + private string _lastKnownSteamId2 = string.Empty; + + private async Task LoadSelfProfileAsync() + { + try + { + bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); + if (!hasSteam || string.IsNullOrWhiteSpace(Steam.recentSteamID64)) + return; + + var rawSelfJson = await Api.Eddies.GetSelfInfo(Steam.recentSteamID64); + var self = Api.ParseSelfInfoPayload(rawSelfJson); + if (self == null) + return; + + Dispatcher.UIThread.Post(() => + { + if (!string.IsNullOrWhiteSpace(self.AvatarUrl)) + ProfilePicture = AvatarCache.GetDisplaySource(self.AvatarUrl); + + if (!string.IsNullOrWhiteSpace(self.Username)) + UsernameGreeting = $"Hello, {self.Username}"; + }); + } + catch + { + // Best-effort profile load; keep defaults on failure. + } + } + + private async Task RefreshServersAsync() + { + if (IsOfflineMode) + { + foreach (var s in Servers.Where(s => !s.IsNone)) + { + s.IsOnline = false; + s.Players = 0; + s.MaxPlayers = 0; + s.Map = ""; + } + return; + } + + await ServerQuery.RefreshServers(Servers.Where(s => !s.IsNone)); + + // Re-order by player count descending; None always stays at index 0 + var sorted = Servers.Where(s => !s.IsNone) + .OrderByDescending(s => s.Players) + .ToList(); + int insertAt = 1; + foreach (var server in sorted) + { + int from = Servers.IndexOf(server); + if (from != insertAt) + Servers.Move(from, insertAt); + insertAt++; + } + } + + private async Task RefreshServersSafeAsync() + { + if (Interlocked.Exchange(ref _serverRefreshInProgress, 1) == 1) + return; + + try + { + await RefreshServersAsync(); + } + finally + { + Interlocked.Exchange(ref _serverRefreshInProgress, 0); + } + } + + private async Task RefreshFriendsAsync() + { + try + { + if (IsOfflineMode) + { + var steamIdForCache = !string.IsNullOrWhiteSpace(_lastKnownSteamId2) + ? _lastKnownSteamId2 + : (Steam.recentSteamID2 ?? string.Empty); + + if (TryShowCachedFriends(steamIdForCache, forceOfflineStatus: true)) + return; + + Dispatcher.UIThread.Post(() => + { + Friends.Clear(); + _lastRenderedFriendsSignature = string.Empty; + ShowNoFriendsState = false; + FriendsStatus = "Offline mode"; + FriendsShowStatus = true; + }); + return; + } + + bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); + string steamId = Steam.recentSteamID2 ?? string.Empty; + if (!string.IsNullOrWhiteSpace(steamId)) + _lastKnownSteamId2 = steamId; + + if (!hasSteam) + { + Dispatcher.UIThread.Post(() => + { + ShowNoFriendsState = false; + FriendsStatus = "Steam is not installed."; + FriendsShowStatus = true; + }); + return; + } + + if (string.IsNullOrEmpty(Steam.recentSteamID2)) + { + Dispatcher.UIThread.Post(() => + { + ShowNoFriendsState = false; + FriendsStatus = "Sign in to Steam to see friends."; + FriendsShowStatus = true; + }); + return; + } + + string rawFriendsJson; + try + { + rawFriendsJson = await Api.Eddies.GetFriends(Steam.recentSteamID64 ?? string.Empty); + } + catch + { + rawFriendsJson = await Api.Eddies.GetFriendsBySteamId2(Steam.recentSteamID2 ?? string.Empty); + } + var apiFriends = Api.ParseFriendsPayload(rawFriendsJson) + .OrderBy(f => f.Status == "Offline" ? 1 : 0) + .ToList(); + + await FriendsCache.SaveAsync(steamId, apiFriends); + + Dispatcher.UIThread.Post(() => + { + var sorted = apiFriends; + + foreach (var f in sorted) + f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); + + var signature = BuildFriendsSignature(sorted); + if (!string.Equals(signature, _lastRenderedFriendsSignature, StringComparison.Ordinal)) + { + Friends.Clear(); + foreach (var f in sorted) + Friends.Add(f); + _lastRenderedFriendsSignature = signature; + } + + FriendsShowStatus = Friends.Count == 0; + ShowNoFriendsState = Friends.Count == 0; + FriendsStatus = Friends.Count == 0 ? "No friends found." : ""; + }); + } + catch { - ProtocolManager = ProtocolManager + "Ready to Launch!"; + if (TryShowCachedFriends(Steam.recentSteamID2 ?? string.Empty, forceOfflineStatus: true)) + return; + + Dispatcher.UIThread.Post(() => + { + Friends.Clear(); + _lastRenderedFriendsSignature = string.Empty; + ShowNoFriendsState = false; + FriendsStatus = IsOfflineMode ? "Offline mode" : "Couldn't load friends right now."; + FriendsShowStatus = true; + }); } + } + + private bool TryShowCachedFriends(string steamId, bool forceOfflineStatus) + { + var cached = FriendsCache.Load(steamId); + if (cached.Count == 0) + return false; + + var sorted = cached + .OrderBy(f => f.Status == "Offline" ? 1 : 0) + .ToList(); - Discord.OnAvatarUpdate += (avatarUrl) => + if (forceOfflineStatus) { - if (!string.IsNullOrEmpty(avatarUrl)) + foreach (var f in sorted) + f.Status = "Offline"; + } + + foreach (var f in sorted) + f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); + + Dispatcher.UIThread.Post(() => + { + var signature = BuildFriendsSignature(sorted); + if (!string.Equals(signature, _lastRenderedFriendsSignature, StringComparison.Ordinal)) { - Dispatcher.UIThread.Post(() => ProfilePicture = avatarUrl); + Friends.Clear(); + foreach (var f in sorted) + Friends.Add(f); + _lastRenderedFriendsSignature = signature; } - }; - Discord.OnUsernameUpdate += (username) => + FriendsShowStatus = false; + ShowNoFriendsState = false; + FriendsStatus = ""; + }); + + return true; + } + + private static string BuildFriendsSignature(IEnumerable friends) + { + var sb = new StringBuilder(); + foreach (var f in friends) { - if (!string.IsNullOrEmpty(username)) + sb.Append(f.Username ?? string.Empty) + .Append('\u001f') + .Append(f.AvatarUrl ?? string.Empty) + .Append('\u001f') + .Append(f.Status ?? "Offline") + .Append('\u001e'); + } + return sb.ToString(); + } + + private async Task RefreshFriendsSafeAsync() + { + if (Interlocked.Exchange(ref _friendsRefreshInProgress, 1) == 1) + return; + + try + { + await RefreshFriendsAsync(); + } + finally + { + Interlocked.Exchange(ref _friendsRefreshInProgress, 0); + } + } + + + + + + private void CheckWhitelistStatus() + { + Task.Run(async () => + { + try + { + bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); + if (!hasSteam) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = "Gray"; + WhitelistText = "Unknown"; + }); + return; + } + + if (string.IsNullOrEmpty(Steam.recentSteamID2)) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = "Gray"; + WhitelistText = "Unknown"; + }); + return; + } + + var response = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); + bool whitelisted = response?.Files != null && response.Files.Count > 0; + + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = whitelisted ? "#4CAF50" : "#F44336"; + WhitelistText = whitelisted ? "Whitelisted" : "Not Whitelisted"; + }); + } + catch { - Dispatcher.UIThread.Post(() => UsernameGreeting = $"Hello, {username}"); + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = "Gray"; + WhitelistText = "Unknown"; + }); } - }; + }); + } + + private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) + { + Dispatcher.UIThread.Post(UpdateOfflineMode); + } + + private void UpdateOfflineMode() + { + IsOfflineMode = !NetworkInterface.GetIsNetworkAvailable(); } } } + + diff --git a/Wauncher/ViewModels/PatchNoteItem.cs b/Wauncher/ViewModels/PatchNoteItem.cs new file mode 100644 index 0000000..cdd8904 --- /dev/null +++ b/Wauncher/ViewModels/PatchNoteItem.cs @@ -0,0 +1,10 @@ +namespace Wauncher.ViewModels +{ + public class PatchNoteItem + { + public string Text { get; set; } = string.Empty; + public bool IsMajorHeader { get; set; } + public bool IsHeader { get; set; } + public bool IsBullet { get; set; } + } +} diff --git a/Wauncher/ViewModels/SettingsWindowViewModel.cs b/Wauncher/ViewModels/SettingsWindowViewModel.cs new file mode 100644 index 0000000..fe256b2 --- /dev/null +++ b/Wauncher/ViewModels/SettingsWindowViewModel.cs @@ -0,0 +1,85 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Wauncher.ViewModels +{ + public partial class SettingsWindowViewModel : ViewModelBase + { + // ── Static events — fire whenever these settings change on ANY instance ── + public static event Action? DiscordRpcChanged; + + [ObservableProperty] + private bool _minimizeToTray = true; + + [ObservableProperty] + private bool _discordRpc = true; + + [ObservableProperty] + private bool _skipUpdates = false; + + [ObservableProperty] + private string _launchOptions = string.Empty; + + public SettingsWindowViewModel() + { + Load(); + } + + partial void OnMinimizeToTrayChanged(bool value) => Save(); + partial void OnSkipUpdatesChanged(bool value) => Save(); + partial void OnLaunchOptionsChanged(string value) => Save(); + + partial void OnDiscordRpcChanged(bool value) + { + Save(); + DiscordRpcChanged?.Invoke(value); + } + + private void Load() + { + try + { + string path = SettingsPath(); + if (!File.Exists(path)) { Save(); return; } + + foreach (var line in File.ReadAllLines(path)) + { + // Split only on the first "=" so values like "+set key=value" are preserved. + int eq = line.IndexOf('='); + if (eq <= 0) continue; + + var key = line[..eq].Trim(); + var value = line[(eq + 1)..]; + + switch (key) + { + case "MinimizeToTray": MinimizeToTray = value.Trim() == "true"; break; + case "DiscordRpc": DiscordRpc = value.Trim() == "true"; break; + case "SkipUpdates": SkipUpdates = value.Trim() == "true"; break; + case "LaunchOptions": LaunchOptions = value; break; + } + } + } + catch { } + } + + public void Save() + { + try + { + File.WriteAllLines(SettingsPath(), new[] + { + $"MinimizeToTray={MinimizeToTray.ToString().ToLower()}", + $"DiscordRpc={DiscordRpc.ToString().ToLower()}", + $"SkipUpdates={SkipUpdates.ToString().ToLower()}", + $"LaunchOptions={LaunchOptions}", + }); + } + catch { } + } + + public static SettingsWindowViewModel LoadGlobal() => new(); + + public static string SettingsPath() => + Path.Combine(new FileInfo(System.Environment.ProcessPath ?? "").Directory?.FullName ?? Directory.GetCurrentDirectory(), "wauncher_settings.cfg"); + } +} diff --git a/Wauncher/Views/InfoWindow.axaml b/Wauncher/Views/InfoWindow.axaml index d43fb24..5c275ac 100644 --- a/Wauncher/Views/InfoWindow.axaml +++ b/Wauncher/Views/InfoWindow.axaml @@ -1,93 +1,247 @@ - - - - - - - - - - - - - - Thank You for playing ClassicCounter and using Wauncher. - Special thanks to h4rmy, heapy and eddies for maintaining this project. - - Built using: - - and - - - - by - - - - Suggest via - - or - - - Something is not working? Read - - - - - - - + x:Class="Wauncher.InfoWindow" + xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:svg="clr-namespace:Avalonia.Svg.Controls;assembly=Avalonia.Svg.Skia" + xmlns:vm="using:Wauncher.ViewModels" + Title="Info" + Width="560" + Height="420" + d:DesignHeight="420" + d:DesignWidth="560" + x:DataType="vm:InfoWindowViewModel" + SystemDecorations="None" + WindowStartupLocation="CenterOwner" + TransparencyLevelHint="AcrylicBlur" + Background="Transparent" + mc:Ignorable="d"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wauncher/Views/InfoWindow.axaml.cs b/Wauncher/Views/InfoWindow.axaml.cs index 1d6a8a7..ff84242 100644 --- a/Wauncher/Views/InfoWindow.axaml.cs +++ b/Wauncher/Views/InfoWindow.axaml.cs @@ -1,4 +1,6 @@ +using Avalonia; using Avalonia.Controls; +using Avalonia.Input; using Wauncher.ViewModels; namespace Wauncher; @@ -10,4 +12,15 @@ public InfoWindow() InitializeComponent(); DataContext = new InfoWindowViewModel(); } -} \ No newline at end of file + + private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + BeginMoveDrag(e); + } + + private void CloseButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + Close(); + } +} diff --git a/Wauncher/Views/MainWindow.axaml b/Wauncher/Views/MainWindow.axaml index a9e454f..c4bcf1a 100644 --- a/Wauncher/Views/MainWindow.axaml +++ b/Wauncher/Views/MainWindow.axaml @@ -1,77 +1,759 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index d5a312f..fdb5003 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -1,35 +1,1607 @@ -using Avalonia.Controls; -using Launcher.Utils; - -namespace Wauncher.Views -{ +using System.IO; +using System.Net.Http; +using System.Linq; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Text.Json; +using System.Text; +using System.Text.RegularExpressions; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Threading; +using Wauncher.Utils; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Wauncher.ViewModels; +using Wauncher.Views; + +namespace Wauncher.Views +{ public partial class MainWindow : Window { - private InfoWindow? _infoWindow = null; - public MainWindow() + private InfoWindow? _infoWindow = null; + private SettingsWindow? _settingsWindow = null; + private SettingsWindowViewModel _settings; + private int _launchInProgress; + private int _updateInProgress; + private int _installInProgress; + + private bool _dropdownOpen = false; + + private const double HeightClosed = 720; + private const double HeightOpen = 720; + + // ── Image carousel (center content area) ────────────────────────────────── + private Image[] _carouselImages = Array.Empty(); + private DispatcherTimer? _carouselTimer; + private int _currentCarouselIndex = 0; + private const int CarouselRotationIntervalSeconds = 5; + private readonly List _zoomCts = new(); + private static string WauncherDirectory => + Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); + + public MainWindow() + { + InitializeComponent(); + _settings = SettingsWindowViewModel.LoadGlobal(); + + this.Loaded += (_, _) => + { + var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = buttonColor; + ArrowButton.Background = buttonColor; + LaunchUpdateButton.IsEnabled = true; + }; + + this.Opened += (_, _) => + { + _ = SetupCarouselAsync(); + _ = StartupAsync(); + _ = LoadPatchNotesAsync(); + + if (DataContext is MainWindowViewModel vm2) + vm2.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(MainWindowViewModel.IsUpdating) || + e.PropertyName == nameof(MainWindowViewModel.IsInstalling)) + SetLaunchGlow(vm2.IsUpdating || vm2.IsInstalling); + }; + }; + + // Window minimize always goes to taskbar; tray hide only happens on game launch. + + this.Closing += (s, e) => + { + if (_forceClose) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + if (_settings.MinimizeToTray) + { + e.Cancel = true; + Hide(); + } + }; + + // Ensure carousel timer is stopped whenever the window closes, + // regardless of which code path triggered it. + this.Closed += (_, _) => TeardownCarousel(); + } + + // ── Image carousel (center content area) ────────────────────────────────── + private static readonly HttpClient _http = new(); + private static string PatchNotesCachePath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache", + "patchnotes.md"); + + private async Task SetupCarouselAsync() + { + try + { + TeardownCarousel(); + + var carouselContainer = this.FindControl("CarouselContainer"); + var offlinePanel = this.FindControl("CarouselOfflinePanel"); + var offlineSubText = this.FindControl("CarouselOfflineSubText"); + if (carouselContainer == null) + return; + + bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); + var bitmaps = hasInternet + ? await LoadCarouselFromGitHubAsync() + : null; + + if (bitmaps == null || bitmaps.Count == 0) + bitmaps = LoadEmbeddedCarouselImages(); + + if (bitmaps.Count == 0) + { + if (offlinePanel != null) + offlinePanel.IsVisible = true; + if (offlineSubText != null) + { + offlineSubText.Text = hasInternet + ? "Carousel is temporarily unavailable." + : "Connect to Wi-Fi or Ethernet to load the carousel."; + } + return; + } + + if (offlinePanel != null) + offlinePanel.IsVisible = false; + + _carouselImages = CreateCarouselImages(bitmaps); + EnsureZoomSlots(_carouselImages.Length); + + foreach (var existingImage in carouselContainer.Children.OfType().ToList()) + carouselContainer.Children.Remove(existingImage); + + int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; + for (int i = 0; i < _carouselImages.Length; i++) + { + if (overlayIndex >= 0) + { + carouselContainer.Children.Insert(overlayIndex, _carouselImages[i]); + overlayIndex++; + } + else + { + carouselContainer.Children.Add(_carouselImages[i]); + } + } + + _currentCarouselIndex = 0; + _carouselImages[0].Opacity = 1.0; + StartZoomOut(_carouselImages[0], 0); + + _carouselTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CarouselRotationIntervalSeconds) }; + _carouselTimer.Tick += (_, _) => RotateCarousel(); + _carouselTimer.Start(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine("Carousel: " + ex.Message); + } + } + + private async Task?> LoadCarouselFromGitHubAsync() + { + try + { + var json = await Api.GitHub.GetCarouselAssetsWauncher(); + var assets = JsonConvert.DeserializeObject>(json); + if (assets == null || assets.Count == 0) + return null; + + var urls = assets + .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) + .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) + .OrderBy(a => GetCarouselSortIndex(a.Name)) + .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) + .Select(a => a.DownloadUrl!) + .ToList(); + + if (urls.Count == 0) + return null; + + var bitmaps = new List(); + foreach (var url in urls) + { + try + { + var bytes = await _http.GetByteArrayAsync(url); + using var ms = new MemoryStream(bytes); + bitmaps.Add(new Bitmap(ms)); + } + catch { } + } + return bitmaps.Count > 0 ? bitmaps : null; + } + catch { return null; } + } + + private static int GetCarouselSortIndex(string name) { - InitializeComponent(); + var match = Regex.Match(name, @"^carousel_(\d+)", RegexOptions.IgnoreCase); + if (match.Success && int.TryParse(match.Groups[1].Value, out var index)) + return index; + return int.MaxValue; } - private async void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + private sealed class GitHubAssetEntry { - await Game.Launch(); - Discord.SetDetails("In Main Menu"); - Discord.Update(); - await Game.Monitor(); + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("type")] + public string Type { get; set; } = string.Empty; + + [JsonProperty("download_url")] + public string? DownloadUrl { get; set; } } - private void Button_Info(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + + private static Image[] CreateCarouselImages(IReadOnlyList bitmaps) { - if (_infoWindow == null) + var images = new Image[bitmaps.Count]; + for (int i = 0; i < bitmaps.Count; i++) { - _infoWindow = new InfoWindow(); - _infoWindow.Closed += (s, e) => _infoWindow = null; - _infoWindow.Show(this); + images[i] = new Image + { + Source = bitmaps[i], + Stretch = Stretch.UniformToFill, + Opacity = 0.0, + Transitions = new Transitions + { + new DoubleTransition + { + Property = Visual.OpacityProperty, + Duration = TimeSpan.FromSeconds(1.5), + Easing = new CubicEaseInOut() + } + } + }; + } + + return images; + } + + private void EnsureZoomSlots(int count) + { + while (_zoomCts.Count < count) + _zoomCts.Add(null); + } + + private static List LoadEmbeddedCarouselImages() + { + var bitmaps = new List(); + string[] files = { "carousel_0.png", "carousel_1.png", "carousel_2.png", "carousel_3.png" }; + foreach (var file in files) + { + try + { + var uri = new Uri("avares://Wauncher/Assets/" + file); + using var stream = AssetLoader.Open(uri); + bitmaps.Add(new Bitmap(stream)); + } + catch { } + } + return bitmaps; + } + + private void RotateCarousel() + { + if (_carouselImages.Length == 0) + return; + + // Fade out current image (zoom continues through the crossfade) + _carouselImages[_currentCarouselIndex].Opacity = 0.0; + + // Move to next image + _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; + + // Fade in next image and start fresh zoom-out + StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); + _carouselImages[_currentCarouselIndex].Opacity = 1.0; + } + + private void TeardownCarousel() + { + _carouselTimer?.Stop(); + _carouselTimer = null; + for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); + _carouselImages = Array.Empty(); + } + + private void StartZoomOut(Image img, int slot) + { + StopZoom(slot); + _zoomCts[slot] = new System.Threading.CancellationTokenSource(); + var cts = _zoomCts[slot]!; + + img.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); + var scale = new ScaleTransform(1.15, 1.15); + img.RenderTransform = scale; + + const double startScale = 1.15; + const double endScale = 1.0; + var totalMs = 6000.0; + var startTime = DateTime.UtcNow; + + var zoomTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) }; + zoomTimer.Tick += (_, _) => + { + if (cts.IsCancellationRequested) { zoomTimer.Stop(); return; } + var t = Math.Min((DateTime.UtcNow - startTime).TotalMilliseconds / totalMs, 1.0); + var s = startScale + (endScale - startScale) * t; + scale.ScaleX = s; + scale.ScaleY = s; + if (t >= 1.0) zoomTimer.Stop(); + }; + zoomTimer.Start(); + } + + private void StopZoom(int slot) + { + if (slot < 0 || slot >= _zoomCts.Count) + return; + + _zoomCts[slot]?.Cancel(); + _zoomCts[slot] = null; + } + + // ── Server dropdown ─────────────────────────────────────────── + private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) + { + CloseDropdown(); + return; + } + + _dropdownOpen = !_dropdownOpen; + + if (DataContext is MainWindowViewModel vm) + vm.IsDropdownOpen = _dropdownOpen; + + if (_dropdownOpen) + { + ServerListPanel.MaxHeight = 270; + } + else + { + ServerListPanel.MaxHeight = 0; + } + } + + private void ServerItem_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is Button btn && btn.Tag is ServerInfo server && + DataContext is MainWindowViewModel vm) + { + vm.SelectedServer = server.IsNone ? null : server; + } + CloseDropdown(); + } + + private void CloseDropdown() + { + _dropdownOpen = false; + if (DataContext is MainWindowViewModel vm) + vm.IsDropdownOpen = false; + + ServerListPanel.MaxHeight = 0; + } + + // ── Game launch ─────────────────────────────────────────── + private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || Volatile.Read(ref _launchInProgress) == 1) + return; + else if (vm.IsNeedingInstall) + _ = InstallGameFromCdnAsync(); + else if (_settings.SkipUpdates) + _ = LaunchGameAsync(); + else if (vm.UpdateAvailable) + { + if (_selfUpdateAvailable) + _ = Button_SelfUpdateAsync(); + else + Button_Update(sender, e); } else + _ = LaunchGameAsync(); + } + + private async Task LaunchGameAsync() + { + if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) + return; + + var vm = DataContext as MainWindowViewModel; + try + { + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm != null) vm.GameStatus = "Running"; + + // Clear any arguments left over from a previous launch before adding new ones. + Argument.ClearAdditionalArguments(); + + Argument.AddArgument("-novid"); + + if (!_settings.DiscordRpc) + Argument.AddArgument("--disable-rpc"); + + if (!string.IsNullOrWhiteSpace(_settings.LaunchOptions)) + foreach (var arg in ParseLaunchOptions(_settings.LaunchOptions)) + Argument.AddArgument(arg); + + var selected = vm?.SelectedServer; + if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) + { + Argument.AddArgument("+connect"); + Argument.AddArgument(selected.IpPort); + } + + await Game.Launch(); + + if (_settings.MinimizeToTray) Hide(); + + if (_settings.DiscordRpc) + { + Discord.SetDetails((selected != null && !selected.IsNone) + ? $"Playing on {selected.Name}" : "In Main Menu"); + Discord.Update(); + } + + await Game.Monitor(); + } + catch (Exception ex) + { + Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); + } + finally + { + if (vm != null) vm.GameStatus = "Not Running"; + Interlocked.Exchange(ref _launchInProgress, 0); + } + } + + // ── Window chrome ─────────────────────────────────────────── + private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + BeginMoveDrag(e); + } + + private void MinimizeButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + WindowState = WindowState.Minimized; + } + + private bool _forceClose = false; + + private void CloseButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + _forceClose = true; + TeardownCarousel(); + Close(); + } + + public void ForceQuit() + { + _forceClose = true; + TeardownCarousel(); + Close(); + } + + private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var dir = WauncherDirectory; + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = dir, + UseShellExecute = true + }); + } + + // ── Update ───────────────────────────────────────────────────── + private CancellationTokenSource? _updateCts; + private Patches? _cachedPatches; + private bool _selfUpdateAvailable; + private string _selfUpdateDownloadUrl = string.Empty; + private string _selfUpdateVersion = string.Empty; + private int _autoSelfUpdateTriggered; + + /// + /// Called on window open. If csgo.exe is missing, triggers a full CDN install. + /// Otherwise runs the normal patch update check. + /// + private async Task StartupAsync() + { + // Yield to let Avalonia finish its initial layout/styling pass + // (Loaded sets the button disabled/gray; we need that to settle before overriding) + await Task.Delay(50); + + LaunchUpdateButton.IsEnabled = true; + + string csgoExe = Path.Combine(WauncherDirectory, "csgo.exe"); + if (DataContext is not MainWindowViewModel vm) + return; + + if (!File.Exists(csgoExe)) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + if (vm?.IsOfflineMode == true) + { + vm.IsNeedingInstall = false; + vm.UpdateAvailable = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + _settings = SettingsWindowViewModel.LoadGlobal(); + if (_settings.SkipUpdates) + { + vm!.IsNeedingInstall = false; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + await CheckForUpdatesAsync(); + } + + private async Task InstallGameFromCdnAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + if (Interlocked.Exchange(ref _installInProgress, 1) == 1) + return; + + bool installSucceeded = false; + vm.IsNeedingInstall = false; + vm.IsInstalling = true; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Connecting..."; + vm.UpdateStatusSpeed = ""; + + try + { + await DownloadManager.InstallFullGame( + onProgress: (file, speed, percent) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = $"Installing {ShortFileName(file)} {percent:F0}%"; + vm.UpdateStatusSpeed = string.IsNullOrWhiteSpace(speed) ? "" : speed; + vm.UpdateProgress = percent; + vm.UpdateIndeterminate = false; + }); + }, + onStatus: status => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = status; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); + if (!vm.UpdateIndeterminate) + vm.UpdateProgress = 0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = extractPercent; + }); + }); + + // Immediately apply any post-install patches so first-time installs + // end in a launch-ready state without requiring a second manual update. + Dispatcher.UIThread.Post(() => + { + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Checking post-install patches..."; + vm.UpdateStatusSpeed = ""; + }); + + var patches = await Task.Run(() => PatchManager.ValidatePatches()); + var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); + if (allPatches.Count > 0) + { + int totalFiles = allPatches.Count; + int completed = 0; + + foreach (var patch in allPatches) + { + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); + + completed++; + vm.UpdateProgress = (double)completed / totalFiles * 100.0; + } + } + + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = "Game installed and fully updated!"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + vm.UpdateProgress = 100; + }); + installSucceeded = true; + await Task.Delay(1500); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Install error: {ex.Message}"; + vm.UpdateStatusSpeed = ""; + await Task.Delay(4000); + } + finally + { + vm.IsInstalling = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + + _cachedPatches = null; + + if (!installSucceeded && !File.Exists(Path.Combine(WauncherDirectory, "csgo.exe"))) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + } + else + { + try + { + await CheckForUpdatesAsync(); + } + catch { } + } + + LaunchUpdateButton.IsEnabled = true; + Interlocked.Exchange(ref _installInProgress, 0); + } + } + + private async Task LoadPatchNotesAsync() + { + try + { + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Loading latest patch notes..."; + PatchNotesVersion.IsVisible = true; + }); + + if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) + { + Dispatcher.UIThread.Post(() => + { + var cachedItems = LoadCachedPatchNotes(); + if (cachedItems.Count > 0) + { + PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; + PatchNotesList.ItemsSource = cachedItems; + } + else + { + PatchNotesVersion.Text = "Patch notes are unavailable offline."; + PatchNotesList.ItemsSource = new List(); + } + + PatchNotesVersion.IsVisible = true; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + return; + } + + var md = await Api.GitHub.GetPatchNotesWauncher(); + var items = ParsePatchNotes(md); + SavePatchNotesCache(md); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = items.Count > 0 + ? $"Updated {DateTime.Now:MMM d, h:mm tt}" + : "Patch notes are currently empty."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + catch + { + var items = LoadCachedPatchNotes(); + if (items.Count == 0) + { + items = BuildFallbackPatchNotes(); + } + + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Using fallback patch notes."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + } + + private static void SavePatchNotesCache(string markdown) + { + try + { + var directory = Path.GetDirectoryName(PatchNotesCachePath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(PatchNotesCachePath, markdown); + } + catch + { + // Caching is best-effort; keep patch notes functional if disk write fails. + } + } + + private static List LoadCachedPatchNotes() + { + try + { + if (!File.Exists(PatchNotesCachePath)) + { + return new List(); + } + + var markdown = File.ReadAllText(PatchNotesCachePath); + return ParsePatchNotes(markdown); + } + catch + { + return new List(); + } + } + + private static List BuildFallbackPatchNotes() + { + return new List + { + new() { Text = "Anniversary Update", IsMajorHeader = true }, + new() { Text = "What's Changed", IsHeader = true }, + new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, + new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, + new() { Text = "Bug fixes and security improvements.", IsBullet = true }, + }; + } + + private static List ParsePatchNotes(string markdown) + { + var items = new List(); + foreach (var raw in markdown.Split('\n')) + { + var line = raw.TrimEnd(); + if (string.IsNullOrWhiteSpace(line)) continue; + + line = line.Trim(); + line = line.Replace("**", "").Replace("__", ""); + line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); + line = Regex.Replace(line, @"`([^`]*)`", "$1"); + + if (line.StartsWith("# ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsMajorHeader = true + }); + } + else if (line.StartsWith("## ") || line.StartsWith("### ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsHeader = true + }); + } + else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Substring(2).Trim(), + IsBullet = true + }); + } + else if (Regex.IsMatch(line, @"^\d+\.\s+")) + { + var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); + items.Add(new ViewModels.PatchNoteItem + { + Text = bulletText, + IsBullet = true + }); + } + else if (line.StartsWith("**") && line.EndsWith("**")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Trim('*', ' '), + IsHeader = true + }); + } + else + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', '*', '-', ' '), + IsBullet = true + }); + } + } + return items; + } + + private sealed class GitHubRelease + { + [JsonProperty("tag_name")] + public string? TagName { get; set; } + + [JsonProperty("assets")] + public List? Assets { get; set; } + } + + private sealed class GitHubReleaseAsset + { + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("browser_download_url")] + public string DownloadUrl { get; set; } = string.Empty; + } + + private static string NormalizeVersionToken(string? version) + { + if (string.IsNullOrWhiteSpace(version)) + return "0.0.0"; + + var cleaned = version.Trim(); + if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + cleaned = cleaned[1..]; + + cleaned = Regex.Replace(cleaned, @"[^0-9\.]", string.Empty); + return string.IsNullOrWhiteSpace(cleaned) ? "0.0.0" : cleaned; + } + + private static bool TryParseVersion(string value, out global::System.Version parsed) + { + if (global::System.Version.TryParse(value, out parsed!)) + return true; + + var tokens = value.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 0) + { + parsed = new global::System.Version(0, 0, 0); + return false; + } + + while (tokens.Length < 3) + tokens = tokens.Append("0").ToArray(); + + return global::System.Version.TryParse(string.Join('.', tokens.Take(4)), out parsed!); + } + + private async Task CheckForSelfUpdateAsync() + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + + try + { + var latestReleaseJson = await Api.GitHub.GetLatestRelease(); + var release = JsonConvert.DeserializeObject(latestReleaseJson); + if (release == null) + return false; + + var currentVersion = NormalizeVersionToken(Wauncher.Utils.Version.Current); + var latestVersion = NormalizeVersionToken(release.TagName); + if (!TryParseVersion(currentVersion, out var current) || !TryParseVersion(latestVersion, out var latest)) + return false; + + if (latest <= current) + return false; + + var assets = release.Assets ?? new List(); + var preferred = assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + string.Equals(a.Name, "wauncher.exe", StringComparison.OrdinalIgnoreCase)); + + preferred ??= assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + a.Name.Contains("wauncher", StringComparison.OrdinalIgnoreCase) && + a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); + + if (preferred == null) + return false; + + _selfUpdateAvailable = true; + _selfUpdateDownloadUrl = preferred.DownloadUrl; + _selfUpdateVersion = latestVersion; + return true; + } + catch + { + return false; + } + } + + private async Task DownloadFileWithProgressAsync(string url, string destination, Action? onProgress, CancellationToken token) + { + using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); + response.EnsureSuccessStatusCode(); + + var totalBytes = response.Content.Headers.ContentLength; + await using var input = await response.Content.ReadAsStreamAsync(token); + await using var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true); + + var buffer = new byte[81920]; + long received = 0; + while (true) { - _infoWindow.Activate(); + int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), token); + if (read == 0) + break; + + await output.WriteAsync(buffer.AsMemory(0, read), token); + received += read; + if (totalBytes.HasValue && totalBytes.Value > 0) + { + onProgress?.Invoke((double)received / totalBytes.Value * 100.0); + } + } + + onProgress?.Invoke(100.0); + } + + private static string BuildSelfUpdateScript(string stagedExePath, string currentExePath) + { + return +$@"@echo off +setlocal +set ""SRC={stagedExePath}"" +set ""DST={currentExePath}"" + +for /L %%i in (1,1,60) do ( + copy /Y ""%SRC%"" ""%DST%"" >nul 2>nul && goto copied + timeout /t 1 /nobreak >nul +) + +exit /b 1 + +:copied +start """" ""%DST%"" +del /Q ""%SRC%"" >nul 2>nul +del /Q ""%~f0"" >nul 2>nul +exit /b 0 +"; + } + + private async Task Button_SelfUpdateAsync() + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) + return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = "Downloading Wauncher update..."; + vm.UpdateStatusSpeed = ""; + + try + { + if (string.IsNullOrWhiteSpace(_selfUpdateDownloadUrl)) + throw new Exception("No self-update package URL found."); + + var updatesDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "self-update"); + Directory.CreateDirectory(updatesDir); + + var safeVersion = Regex.Replace(_selfUpdateVersion, @"[^0-9A-Za-z\.\-_]", string.Empty); + if (string.IsNullOrWhiteSpace(safeVersion)) + safeVersion = "latest"; + + var stagedExePath = Path.Combine(updatesDir, $"wauncher_{safeVersion}.exe"); + await DownloadFileWithProgressAsync(_selfUpdateDownloadUrl, stagedExePath, percent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateProgress = percent; + vm.UpdateStatusFile = $"Downloading Wauncher update... {percent:F0}%"; + }); + }, token); + + var currentExePath = Services.GetExePath(); + if (string.IsNullOrWhiteSpace(currentExePath)) + throw new Exception("Could not locate current Wauncher executable."); + + var scriptPath = Path.Combine(updatesDir, "apply_wauncher_update.cmd"); + var script = BuildSelfUpdateScript(stagedExePath, currentExePath); + File.WriteAllText(scriptPath, script, Encoding.ASCII); + + Process.Start(new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/c \"{scriptPath}\"", + WorkingDirectory = updatesDir, + CreateNoWindow = true, + UseShellExecute = false, + }); + + vm.UpdateStatusFile = "Restarting Wauncher to apply update..."; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 100; + await Task.Delay(500, token); + ForceQuit(); + } + catch (OperationCanceledException) + { + vm.UpdateStatusFile = "Update cancelled."; + vm.UpdateStatusSpeed = ""; + await Task.Delay(800); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Self-update failed: {ex.Message}"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + await Task.Delay(2500); + } + finally + { + if (!_forceClose) + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch + { + // keep UI responsive even if refresh fails + } + } + + Interlocked.Exchange(ref _updateInProgress, 0); + } + } + + private async Task CheckForUpdatesAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsOfflineMode) + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + _cachedPatches = null; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + LaunchUpdateButton.IsEnabled = true; + return; + } + + vm.IsCheckingUpdates = true; + LaunchUpdateButton.Background = new SolidColorBrush(Color.Parse("#555555")); + ArrowButton.Background = new SolidColorBrush(Color.Parse("#555555")); + LaunchUpdateButton.IsEnabled = false; + try + { + bool hasSelfUpdate = await CheckForSelfUpdateAsync(); + if (hasSelfUpdate) + { + _cachedPatches = null; + vm.UpdateAvailable = true; + var selfUpdateColor = new SolidColorBrush(Color.Parse("#FFC107")); + LaunchUpdateButton.Background = selfUpdateColor; + ArrowButton.Background = selfUpdateColor; + + if (Interlocked.Exchange(ref _autoSelfUpdateTriggered, 1) == 0) + { + _ = Button_SelfUpdateAsync(); + } + + return; + } + + if (_settings.SkipUpdates) + { + _cachedPatches = null; + vm.UpdateAvailable = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + return; + } + + var patches = await Task.Run(() => PatchManager.ValidatePatches(deleteOutdatedFiles: false)); + bool hasUpdates = patches.Missing.Count > 0 || patches.Outdated.Count > 0; + + // Cache the result so Button_Update can consume it without re-validating. + _cachedPatches = patches; + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + vm.UpdateAvailable = hasUpdates; + var buttonColor = new SolidColorBrush( + Color.Parse(hasUpdates ? "#FFC107" : "#4CAF50")); + LaunchUpdateButton.Background = buttonColor; + ArrowButton.Background = buttonColor; + } + catch + { + var defaultColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = defaultColor; + ArrowButton.Background = defaultColor; + } + finally + { + vm.IsCheckingUpdates = false; + LaunchUpdateButton.IsEnabled = true; + } + } + + private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + + try + { + // Use the result already computed by CheckForUpdatesAsync when available, + // to avoid a redundant full validation on every update click. + var patches = _cachedPatches ?? await Task.Run(() => PatchManager.ValidatePatches(), token); + _cachedPatches = null; // consumed — force fresh check next time + if (token.IsCancellationRequested) return; + + bool hasPatches = patches.Missing.Count > 0 || patches.Outdated.Count > 0; + if (!hasPatches) + { + vm.UpdateStatusFile = "Game is up to date!"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 100; + await Task.Delay(2000); + return; + } + + var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); + int totalFiles = allPatches.Count; + int completed = 0; + + foreach (var patch in allPatches) + { + if (token.IsCancellationRequested) break; + + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); + + completed++; + vm.UpdateProgress = (double)completed / totalFiles * 100.0; + } + + if (!token.IsCancellationRequested) + { + vm.UpdateStatusFile = "Update complete!"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 100; + await Task.Delay(2000); + } + } + catch (OperationCanceledException) + { + vm.UpdateStatusFile = "Update cancelled."; + vm.UpdateStatusSpeed = ""; + await Task.Delay(800); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Error: {ex.Message}"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + await Task.Delay(3000); + } + finally + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _cachedPatches = null; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch { } + + Interlocked.Exchange(ref _updateInProgress, 0); } } + + private void Button_CancelUpdate(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + _updateCts?.Cancel(); + } + + // ── Settings / Info windows ──────────────────────────────────────── + private void FriendsTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "Friends"; + } + + private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "PatchNotes"; + PatchNotesScroll.Offset = new Vector(0, 0); + } + + private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: FriendInfo friend }) + return; + + var profileId = ResolveProfileSteamId(friend.SteamId); + if (string.IsNullOrWhiteSpace(profileId)) + return; + + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = $"https://eddies.cc/profiles/{profileId}", + UseShellExecute = true + }); + } + catch + { + // Best-effort open. + } + } + + private static string ResolveProfileSteamId(string? steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) + return string.Empty; + + var value = steamId.Trim(); + if (ulong.TryParse(value, out _)) + return value; + + if (TryConvertSteamId2To64(value, out var steamId64)) + return steamId64.ToString(); + + return string.Empty; + } + + private static bool TryConvertSteamId2To64(string steamId2, out ulong steamId64) + { + steamId64 = 0; + var match = Regex.Match(steamId2, @"^STEAM_[0-5]:([0-1]):(\d+)$", RegexOptions.IgnoreCase); + if (!match.Success) + return false; + + if (!ulong.TryParse(match.Groups[1].Value, out var y)) + return false; + if (!ulong.TryParse(match.Groups[2].Value, out var z)) + return false; + + steamId64 = 76561197960265728UL + (z * 2UL) + y; + return true; + } + + private void Button_Settings(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (_settingsWindow == null) + { + _settingsWindow = new SettingsWindow(); + _settingsWindow.Closed += (s, e) => + { + _settingsWindow = null; + _settings = SettingsWindowViewModel.LoadGlobal(); + _ = CheckForUpdatesAsync(); + }; + _settingsWindow.Show(this); + } + else _settingsWindow.Activate(); + } + + private void Button_Info(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (_infoWindow == null) + { + _infoWindow = new InfoWindow(); + _infoWindow.Closed += (s, e) => _infoWindow = null; + _infoWindow.Show(this); + } + else _infoWindow.Activate(); + } + + // ── Launch button glow + color ──────────────────────────────────────────── + private void SetLaunchGlow(bool updating) + { + var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); + LaunchUpdateButton.Background = brush; + ArrowButton.Background = brush; + LaunchButtonGlow.BoxShadow = updating + ? BoxShadows.Parse("0 0 18 2 #55FFC107") + : BoxShadows.Parse("0 0 18 2 #554CAF50"); + } + + private static string ShortFileName(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return path; + + var normalized = path.Replace('\\', '/'); + if (normalized.Length <= 42) + return normalized; + + var fileName = Path.GetFileName(normalized); + if (fileName.Length <= 30) + return fileName; + + return fileName[..27] + "..."; + } + + private static string FormatDownloadSpeedAndEta(object progressArgs) + { + double speedBytes = 0; + if (TryGetDoubleProperty(progressArgs, "AverageBytesPerSecondSpeed", out var avg) && avg > 0) + speedBytes = avg; + else if (TryGetDoubleProperty(progressArgs, "BytesPerSecondSpeed", out var cur) && cur > 0) + speedBytes = cur; + + var speedText = speedBytes > 0 + ? $"{speedBytes / 1024.0 / 1024.0:F1} MB/s" + : ""; + + if (speedBytes <= 0 || + !TryGetLongProperty(progressArgs, "TotalBytesToReceive", out var totalBytes) || + !TryGetLongProperty(progressArgs, "ReceivedBytesSize", out var receivedBytes) || + totalBytes <= 0 || receivedBytes < 0 || receivedBytes >= totalBytes) + { + return speedText; + } + + var remainingBytes = totalBytes - receivedBytes; + var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); + var etaText = $"ETA {FormatEta(eta)}"; + + return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; + } + + private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) + { + if (watch == null || !watch.IsRunning || percent <= 1.0) + return ""; + + var elapsed = watch.Elapsed.TotalSeconds; + var total = elapsed / (percent / 100.0); + var remaining = Math.Max(0, total - elapsed); + return $"ETA {FormatEta(TimeSpan.FromSeconds(remaining))}"; + } + + private static string FormatEta(TimeSpan eta) + { + if (eta.TotalHours >= 1) + return eta.ToString(@"hh\:mm\:ss"); + return eta.ToString(@"mm\:ss"); + } + + private static bool TryGetDoubleProperty(object obj, string propertyName, out double value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToDouble(raw); + return true; + } + catch + { + return false; + } + } + + private static bool TryGetLongProperty(object obj, string propertyName, out long value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToInt64(raw); + return true; + } + catch + { + return false; + } + } + + // Minimal parser for launch options that supports quoted values. + private static IEnumerable ParseLaunchOptions(string options) + { + if (string.IsNullOrWhiteSpace(options)) + yield break; + + var current = new StringBuilder(); + bool inQuotes = false; + + foreach (var ch in options) + { + if (ch == '"') + { + inQuotes = !inQuotes; + continue; + } + + if (char.IsWhiteSpace(ch) && !inQuotes) + { + if (current.Length > 0) + { + yield return current.ToString(); + current.Clear(); + } + continue; + } + + current.Append(ch); + } + + if (current.Length > 0) + yield return current.ToString(); + } + } -} \ No newline at end of file +} + + diff --git a/Wauncher/Views/SettingsWindow.axaml b/Wauncher/Views/SettingsWindow.axaml new file mode 100644 index 0000000..5ae67c6 --- /dev/null +++ b/Wauncher/Views/SettingsWindow.axaml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wauncher/Views/SettingsWindow.axaml.cs b/Wauncher/Views/SettingsWindow.axaml.cs new file mode 100644 index 0000000..c5efae8 --- /dev/null +++ b/Wauncher/Views/SettingsWindow.axaml.cs @@ -0,0 +1,36 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Wauncher.ViewModels; + +namespace Wauncher.Views +{ + public partial class SettingsWindow : Window + { + public SettingsWindow() + { + InitializeComponent(); + DataContext = new SettingsWindowViewModel(); + + SettingsWindowViewModel.DiscordRpcChanged += OnDiscordRpcChangedExternally; + this.Closed += (_, _) => SettingsWindowViewModel.DiscordRpcChanged -= OnDiscordRpcChangedExternally; + } + + private void OnDiscordRpcChangedExternally(bool enabled) + { + if (DataContext is SettingsWindowViewModel vm && vm.DiscordRpc != enabled) + vm.DiscordRpc = enabled; + } + + private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + BeginMoveDrag(e); + } + + private void CloseButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + Close(); + } + } +} diff --git a/Wauncher/Wauncher.csproj b/Wauncher/Wauncher.csproj index 27333d1..263e830 100644 --- a/Wauncher/Wauncher.csproj +++ b/Wauncher/Wauncher.csproj @@ -7,13 +7,15 @@ enable enable true + true app.manifest true + NU1701 - true true + true false - 3.0.0 + 3.0.5 $(Version) $(Version) wauncher @@ -25,7 +27,8 @@ - + + @@ -44,10 +47,13 @@ All + + + + + + + - - - - diff --git a/Wauncher/Wauncher.sln b/Wauncher/Wauncher.sln new file mode 100644 index 0000000..76aaceb --- /dev/null +++ b/Wauncher/Wauncher.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wauncher", "Wauncher.csproj", "{26B503D9-F947-924C-D517-78C86C319A7C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {26B503D9-F947-924C-D517-78C86C319A7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26B503D9-F947-924C-D517-78C86C319A7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26B503D9-F947-924C-D517-78C86C319A7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26B503D9-F947-924C-D517-78C86C319A7C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2B5005F6-85FA-4467-97A0-541E7DFF4DFA} + EndGlobalSection +EndGlobal From f69a440ca6870be8e92f4f96f6aec5be992cbd2b Mon Sep 17 00:00:00 2001 From: eddies Date: Tue, 10 Mar 2026 03:39:34 -0400 Subject: [PATCH 02/51] Proprietary Copyright Licensing Information --- Wauncher/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Wauncher/README.md diff --git a/Wauncher/README.md b/Wauncher/README.md new file mode 100644 index 0000000..352d4c3 --- /dev/null +++ b/Wauncher/README.md @@ -0,0 +1,3 @@ +© 2026 ClassicCounter. All Rights Reserved. Do Not Redistribute. +This software and its code are protected by copyright law and international treaties. Unauthorized reproduction or distribution may result in civil and criminal penalties. +You may not use or distribute Wauncher outside of ClassicCounter or ClassicCounter servers without explicit written or recorded permission from a ClassicCounter staff member. From 0764d0dd2fdb8838a31d9df5135b65520084f1b0 Mon Sep 17 00:00:00 2001 From: eddies Date: Tue, 10 Mar 2026 03:50:35 -0400 Subject: [PATCH 03/51] LICENSE --- Wauncher/{README.md => LICENSE} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Wauncher/{README.md => LICENSE} (100%) diff --git a/Wauncher/README.md b/Wauncher/LICENSE similarity index 100% rename from Wauncher/README.md rename to Wauncher/LICENSE From 31a3d1f77893edf700bccd1cee231b4a7857e001 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 03:53:06 -0400 Subject: [PATCH 04/51] remove deprecated --ssl-bypass launcher argument --- Wauncher/Utils/Argument.cs | 134 ++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/Wauncher/Utils/Argument.cs b/Wauncher/Utils/Argument.cs index 81e7bda..a053c27 100644 --- a/Wauncher/Utils/Argument.cs +++ b/Wauncher/Utils/Argument.cs @@ -1,68 +1,68 @@ -namespace Wauncher.Utils -{ - public static class Argument - { - private static List _launcherArguments = new() - { - "--debug-mode", - "--skip-updates", - "--skip-validating", - "--validate-all", - "--patch-only", - "--gc", - "--disable-rpc", - "--install-dependencies", - "--protocol-command", - "--ssl-bypass" - }; - - private static List _additionalArguments = new(); - public static void AddArgument(string argument) - { - if (!_additionalArguments.Any(a => string.Equals(a, argument, StringComparison.OrdinalIgnoreCase))) - _additionalArguments.Add(argument); - } - - public static void ClearAdditionalArguments() - { - _additionalArguments.Clear(); - } - - public static bool Exists(string argument) - { - IEnumerable arguments = Environment.GetCommandLineArgs(); - - foreach (string arg in arguments) - if (arg.ToLowerInvariant() == argument) return true; - - return false; - } - - public static List GenerateGameArguments(bool passLauncherArguments = false) - { - IEnumerable launcherArguments = Environment.GetCommandLineArgs(); - List gameArguments = new(); - - foreach (string arg in launcherArguments) - if (arg.StartsWith("cc://")) - { - string protocolArgument = arg.Replace("cc://", ""); - string[] protocolArguments = protocolArgument.Split('/'); - switch (protocolArguments[0]) - { - case "connect": - gameArguments.Add("+" + protocolArguments[0]); - gameArguments.Add(protocolArguments[1]); - break; - } - } - else if ((passLauncherArguments || !_launcherArguments.Contains(arg.ToLowerInvariant())) - && !arg.EndsWith(".exe")) - gameArguments.Add(arg.ToLowerInvariant()); - - gameArguments.AddRange(_additionalArguments); - return gameArguments; - } - } -} +namespace Wauncher.Utils +{ + public static class Argument + { + private static List _launcherArguments = new() + { + "--debug-mode", + "--skip-updates", + "--skip-validating", + "--validate-all", + "--patch-only", + "--gc", + "--disable-rpc", + "--install-dependencies", + "--protocol-command", + "--ssl-bypass" + }; + + private static List _additionalArguments = new(); + public static void AddArgument(string argument) + { + if (!_additionalArguments.Any(a => string.Equals(a, argument, StringComparison.OrdinalIgnoreCase))) + _additionalArguments.Add(argument); + } + + public static void ClearAdditionalArguments() + { + _additionalArguments.Clear(); + } + + public static bool Exists(string argument) + { + IEnumerable arguments = Environment.GetCommandLineArgs(); + + foreach (string arg in arguments) + if (arg.ToLowerInvariant() == argument) return true; + + return false; + } + + public static List GenerateGameArguments(bool passLauncherArguments = false) + { + IEnumerable launcherArguments = Environment.GetCommandLineArgs(); + List gameArguments = new(); + + foreach (string arg in launcherArguments) + if (arg.StartsWith("cc://")) + { + string protocolArgument = arg.Replace("cc://", ""); + string[] protocolArguments = protocolArgument.Split('/'); + switch (protocolArguments[0]) + { + case "connect": + gameArguments.Add("+" + protocolArguments[0]); + gameArguments.Add(protocolArguments[1]); + break; + } + } + else if ((passLauncherArguments || !_launcherArguments.Contains(arg.ToLowerInvariant())) + && !arg.EndsWith(".exe")) + gameArguments.Add(arg.ToLowerInvariant()); + + gameArguments.AddRange(_additionalArguments); + return gameArguments; + } + } +} From 6ca8234fb8b45a36c1cea7a94c3caabb2acd048a Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 03:54:48 -0400 Subject: [PATCH 05/51] remove insecure ssl bypass from api client --- Wauncher/Utils/Api.cs | 458 +++++++++++++++++++++--------------------- 1 file changed, 229 insertions(+), 229 deletions(-) diff --git a/Wauncher/Utils/Api.cs b/Wauncher/Utils/Api.cs index d61df2b..813c217 100644 --- a/Wauncher/Utils/Api.cs +++ b/Wauncher/Utils/Api.cs @@ -1,230 +1,230 @@ -using Refit; -using Newtonsoft.Json; - -namespace Wauncher.Utils -{ - public class FullGameDownload - { - public required string File { get; set; } - public required string Link { get; set; } - public required string Hash { get; set; } - } - - public class FullGameDownloadResponse - { - public List? Files { get; set; } - public string? Error { get; set; } - } - - public interface IGitHub - { - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/repos/ClassicCounter/launcher/releases/latest")] - Task GetLatestRelease(); - - [Headers("User-Agent: ClassicCounter Wauncher", - "Accept: application/vnd.github.raw+json")] - [Get("/repos/ClassicCounter/launcher/contents/dependencies.json")] - Task GetDependencies(); - - [Headers("User-Agent: ClassicCounter Wauncher", - "Accept: application/vnd.github.raw+json")] - [Get("/repos/ClassicCounter/launcher/contents/carousel.json")] - Task GetCarouselManifest(); - - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/repos/ClassicCounter/launcher/contents/Wauncher/Assets")] - Task GetCarouselAssetsWauncher(); - - [Headers("User-Agent: ClassicCounter Wauncher", - "Accept: application/vnd.github.raw+json")] - [Get("/repos/ClassicCounter/launcher/contents/Wauncher/patchnotes.md")] - Task GetPatchNotesWauncher(); - } - - public class FriendInfo - { - [JsonProperty("steamid")] - public string SteamId { get; set; } = ""; - - [JsonProperty("steamid2")] - public string? SteamId2 - { - set - { - if (!string.IsNullOrWhiteSpace(value) && string.IsNullOrWhiteSpace(SteamId)) - SteamId = value; - } - } - - [JsonProperty("username")] - public string Username { get; set; } = ""; - - [JsonProperty("avatar_url")] - public string AvatarUrl { get; set; } = ""; - - [JsonProperty("avatar")] - public string? Avatar - { - set - { - if (!string.IsNullOrWhiteSpace(value)) - AvatarUrl = value; - } - } - - [JsonProperty("custom_username")] - public string? CustomUsername - { - set - { - if (!string.IsNullOrWhiteSpace(value)) - Username = value; - } - } - - [JsonProperty("custom_avatar")] - public string? CustomAvatar - { - set - { - if (!string.IsNullOrWhiteSpace(value)) - AvatarUrl = value; - } - } - - [JsonProperty("status")] - public string Status { get; set; } = "Offline"; // "Online" | "Offline" - - public string DotColor => Status == "Online" ? "#4CAF50" : "#888888"; - public bool IsOffline => Status == "Offline"; - public double AvatarOpacity => IsOffline ? 0.35 : 1.0; - public string StatusText => IsOffline ? "Offline" : "In Game"; - public string StatusColor => IsOffline ? "#666666" : "#999999"; - } - - public class FriendsResponse - { - public List? Friends { get; set; } - } - - public interface IEddies - { - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/friendsapi.php")] - Task GetFriends([AliasAs("steamid64")] string steamId64); - - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/friendsapi.php")] - Task GetFriendsBySteamId2([AliasAs("steamid2")] string steamId2); - - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/selfinfo.php")] - Task GetSelfInfo([AliasAs("steamid64")] string steamId64); - } - - public interface IClassicCounter - { - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/patch/get")] - Task GetPatches(); - - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/game/get")] - Task GetFullGameValidate(); - - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/game/full")] - Task GetFullGameDownload([Query] string steam_id); - } - - public static class Api - { - private static HttpClientHandler _httpClientHandler = new HttpClientHandler() - { - ServerCertificateCustomValidationCallback = (message, cert, chain, sslErrors) => true - }; - private static HttpClient ClassicCounterApiHttpClient = new HttpClient(_httpClientHandler) - { - BaseAddress = new Uri("https://classiccounter.cc/api") - }; - private static RefitSettings _settings = new RefitSettings(new NewtonsoftJsonContentSerializer()); - public static IGitHub GitHub = RestService.For("https://api.github.com", _settings); - public static IClassicCounter ClassicCounter = Argument.Exists("--ssl-bypass") - ? RestService.For(ClassicCounterApiHttpClient, _settings) - : RestService.For("https://classiccounter.cc/api", _settings); // THIS IS NOT IDEAL, CHANGE THE WORLD OF TRUSTED CERTIFICATES - public static IEddies Eddies = RestService.For("https://eddies.cc/api", _settings); - - public static List ParseFriendsPayload(string? json) - { - if (string.IsNullOrWhiteSpace(json)) - return new List(); - - try - { - var wrapped = JsonConvert.DeserializeObject(json); - if (wrapped?.Friends != null && wrapped.Friends.Count > 0) - return NormalizeFriends(wrapped.Friends); - } - catch - { - // Fall through to array parse. - } - - try - { - var flat = JsonConvert.DeserializeObject>(json); - if (flat != null) - return NormalizeFriends(flat); - } - catch - { - // Ignore and return empty. - } - - return new List(); - } - - public static FriendInfo? ParseSelfInfoPayload(string? json) - { - if (string.IsNullOrWhiteSpace(json)) - return null; - - try - { - var parsed = JsonConvert.DeserializeObject(json); - if (parsed == null) - return null; - - return NormalizeFriends(new[] { parsed }).FirstOrDefault(); - } - catch - { - return null; - } - } - - private static List NormalizeFriends(IEnumerable friends) - { - var normalized = new List(); - foreach (var f in friends) - { - var username = string.IsNullOrWhiteSpace(f.Username) ? "Unknown" : f.Username; - var status = string.Equals(f.Status, "Online", StringComparison.OrdinalIgnoreCase) - ? "Online" - : "Offline"; - - normalized.Add(new FriendInfo - { - SteamId = f.SteamId ?? string.Empty, - Username = username, - AvatarUrl = f.AvatarUrl ?? string.Empty, - Status = status - }); - } - - return normalized; - } - } -} +using Refit; +using Newtonsoft.Json; + +namespace Wauncher.Utils +{ + public class FullGameDownload + { + public required string File { get; set; } + public required string Link { get; set; } + public required string Hash { get; set; } + } + + public class FullGameDownloadResponse + { + public List? Files { get; set; } + public string? Error { get; set; } + } + + public interface IGitHub + { + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/repos/ClassicCounter/launcher/releases/latest")] + Task GetLatestRelease(); + + [Headers("User-Agent: ClassicCounter Wauncher", + "Accept: application/vnd.github.raw+json")] + [Get("/repos/ClassicCounter/launcher/contents/dependencies.json")] + Task GetDependencies(); + + [Headers("User-Agent: ClassicCounter Wauncher", + "Accept: application/vnd.github.raw+json")] + [Get("/repos/ClassicCounter/launcher/contents/carousel.json")] + Task GetCarouselManifest(); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/repos/ClassicCounter/launcher/contents/Wauncher/Assets")] + Task GetCarouselAssetsWauncher(); + + [Headers("User-Agent: ClassicCounter Wauncher", + "Accept: application/vnd.github.raw+json")] + [Get("/repos/ClassicCounter/launcher/contents/Wauncher/patchnotes.md")] + Task GetPatchNotesWauncher(); + } + + public class FriendInfo + { + [JsonProperty("steamid")] + public string SteamId { get; set; } = ""; + + [JsonProperty("steamid2")] + public string? SteamId2 + { + set + { + if (!string.IsNullOrWhiteSpace(value) && string.IsNullOrWhiteSpace(SteamId)) + SteamId = value; + } + } + + [JsonProperty("username")] + public string Username { get; set; } = ""; + + [JsonProperty("avatar_url")] + public string AvatarUrl { get; set; } = ""; + + [JsonProperty("avatar")] + public string? Avatar + { + set + { + if (!string.IsNullOrWhiteSpace(value)) + AvatarUrl = value; + } + } + + [JsonProperty("custom_username")] + public string? CustomUsername + { + set + { + if (!string.IsNullOrWhiteSpace(value)) + Username = value; + } + } + + [JsonProperty("custom_avatar")] + public string? CustomAvatar + { + set + { + if (!string.IsNullOrWhiteSpace(value)) + AvatarUrl = value; + } + } + + [JsonProperty("status")] + public string Status { get; set; } = "Offline"; // "Online" | "Offline" + + public string DotColor => Status == "Online" ? "#4CAF50" : "#888888"; + public bool IsOffline => Status == "Offline"; + public double AvatarOpacity => IsOffline ? 0.35 : 1.0; + public string StatusText => IsOffline ? "Offline" : "In Game"; + public string StatusColor => IsOffline ? "#666666" : "#999999"; + } + + public class FriendsResponse + { + public List? Friends { get; set; } + } + + public interface IEddies + { + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/friendsapi.php")] + Task GetFriends([AliasAs("steamid64")] string steamId64); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/friendsapi.php")] + Task GetFriendsBySteamId2([AliasAs("steamid2")] string steamId2); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/selfinfo.php")] + Task GetSelfInfo([AliasAs("steamid64")] string steamId64); + } + + public interface IClassicCounter + { + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/patch/get")] + Task GetPatches(); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/game/get")] + Task GetFullGameValidate(); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/game/full")] + Task GetFullGameDownload([Query] string steam_id); + } + + public static class Api + { + private static HttpClientHandler _httpClientHandler = new HttpClientHandler() + { + ServerCertificateCustomValidationCallback = (message, cert, chain, sslErrors) => true + }; + private static HttpClient ClassicCounterApiHttpClient = new HttpClient(_httpClientHandler) + { + BaseAddress = new Uri("https://classiccounter.cc/api") + }; + private static RefitSettings _settings = new RefitSettings(new NewtonsoftJsonContentSerializer()); + public static IGitHub GitHub = RestService.For("https://api.github.com", _settings); + public static IClassicCounter ClassicCounter = Argument.Exists("--ssl-bypass") + ? RestService.For(ClassicCounterApiHttpClient, _settings) + : RestService.For("https://classiccounter.cc/api", _settings); // THIS IS NOT IDEAL, CHANGE THE WORLD OF TRUSTED CERTIFICATES + public static IEddies Eddies = RestService.For("https://eddies.cc/api", _settings); + + public static List ParseFriendsPayload(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return new List(); + + try + { + var wrapped = JsonConvert.DeserializeObject(json); + if (wrapped?.Friends != null && wrapped.Friends.Count > 0) + return NormalizeFriends(wrapped.Friends); + } + catch + { + // Fall through to array parse. + } + + try + { + var flat = JsonConvert.DeserializeObject>(json); + if (flat != null) + return NormalizeFriends(flat); + } + catch + { + // Ignore and return empty. + } + + return new List(); + } + + public static FriendInfo? ParseSelfInfoPayload(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return null; + + try + { + var parsed = JsonConvert.DeserializeObject(json); + if (parsed == null) + return null; + + return NormalizeFriends(new[] { parsed }).FirstOrDefault(); + } + catch + { + return null; + } + } + + private static List NormalizeFriends(IEnumerable friends) + { + var normalized = new List(); + foreach (var f in friends) + { + var username = string.IsNullOrWhiteSpace(f.Username) ? "Unknown" : f.Username; + var status = string.Equals(f.Status, "Online", StringComparison.OrdinalIgnoreCase) + ? "Online" + : "Offline"; + + normalized.Add(new FriendInfo + { + SteamId = f.SteamId ?? string.Empty, + Username = username, + AvatarUrl = f.AvatarUrl ?? string.Empty, + Status = status + }); + } + + return normalized; + } + } +} From 3e65c4e9d0b9637c6616f5a4cdc13ea0ce03cd8c Mon Sep 17 00:00:00 2001 From: eddies Date: Tue, 10 Mar 2026 03:55:39 -0400 Subject: [PATCH 06/51] Update LICENSE --- Wauncher/LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wauncher/LICENSE b/Wauncher/LICENSE index 352d4c3..d1fd9fe 100644 --- a/Wauncher/LICENSE +++ b/Wauncher/LICENSE @@ -1,3 +1,3 @@ © 2026 ClassicCounter. All Rights Reserved. Do Not Redistribute. This software and its code are protected by copyright law and international treaties. Unauthorized reproduction or distribution may result in civil and criminal penalties. -You may not use or distribute Wauncher outside of ClassicCounter or ClassicCounter servers without explicit written or recorded permission from a ClassicCounter staff member. +You may not modify, use, or distribute Wauncher outside of ClassicCounter or ClassicCounter servers without explicit written or recorded permission from a ClassicCounter staff member. From 624690470cd17a1c9e23cfade219acf427aef40e Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 03:56:27 -0400 Subject: [PATCH 07/51] remove carousel local fallback and rely on offline panel --- Wauncher/Views/MainWindow.axaml.cs | 2548 ++++++++++++++-------------- 1 file changed, 1274 insertions(+), 1274 deletions(-) diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index fdb5003..fa6ed69 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -1,64 +1,64 @@ -using System.IO; -using System.Net.Http; -using System.Linq; -using System.Net.NetworkInformation; -using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Text.Json; -using System.Text; -using System.Text.RegularExpressions; -using Avalonia.Animation; -using Avalonia.Animation.Easings; -using Avalonia; +using System.IO; +using System.Net.Http; +using System.Linq; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Text.Json; +using System.Text; +using System.Text.RegularExpressions; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Threading; -using Wauncher.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Wauncher.ViewModels; -using Wauncher.Views; +using Avalonia.Platform; +using Avalonia.Threading; +using Wauncher.Utils; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Wauncher.ViewModels; +using Wauncher.Views; namespace Wauncher.Views { - public partial class MainWindow : Window - { - private InfoWindow? _infoWindow = null; - private SettingsWindow? _settingsWindow = null; - private SettingsWindowViewModel _settings; - private int _launchInProgress; - private int _updateInProgress; - private int _installInProgress; - - private bool _dropdownOpen = false; + public partial class MainWindow : Window + { + private InfoWindow? _infoWindow = null; + private SettingsWindow? _settingsWindow = null; + private SettingsWindowViewModel _settings; + private int _launchInProgress; + private int _updateInProgress; + private int _installInProgress; + + private bool _dropdownOpen = false; private const double HeightClosed = 720; private const double HeightOpen = 720; // ── Image carousel (center content area) ────────────────────────────────── - private Image[] _carouselImages = Array.Empty(); - private DispatcherTimer? _carouselTimer; - private int _currentCarouselIndex = 0; - private const int CarouselRotationIntervalSeconds = 5; - private readonly List _zoomCts = new(); - private static string WauncherDirectory => - Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); + private Image[] _carouselImages = Array.Empty(); + private DispatcherTimer? _carouselTimer; + private int _currentCarouselIndex = 0; + private const int CarouselRotationIntervalSeconds = 5; + private readonly List _zoomCts = new(); + private static string WauncherDirectory => + Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); public MainWindow() { InitializeComponent(); _settings = SettingsWindowViewModel.LoadGlobal(); - this.Loaded += (_, _) => - { - var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = buttonColor; - ArrowButton.Background = buttonColor; - LaunchUpdateButton.IsEnabled = true; - }; + this.Loaded += (_, _) => + { + var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = buttonColor; + ArrowButton.Background = buttonColor; + LaunchUpdateButton.IsEnabled = true; + }; this.Opened += (_, _) => { @@ -93,75 +93,75 @@ public MainWindow() this.Closed += (_, _) => TeardownCarousel(); } - // ── Image carousel (center content area) ────────────────────────────────── - private static readonly HttpClient _http = new(); - private static string PatchNotesCachePath => - Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "cache", - "patchnotes.md"); - - private async Task SetupCarouselAsync() - { - try - { - TeardownCarousel(); - - var carouselContainer = this.FindControl("CarouselContainer"); - var offlinePanel = this.FindControl("CarouselOfflinePanel"); - var offlineSubText = this.FindControl("CarouselOfflineSubText"); - if (carouselContainer == null) - return; - - bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); - var bitmaps = hasInternet - ? await LoadCarouselFromGitHubAsync() - : null; - - if (bitmaps == null || bitmaps.Count == 0) - bitmaps = LoadEmbeddedCarouselImages(); - - if (bitmaps.Count == 0) - { - if (offlinePanel != null) - offlinePanel.IsVisible = true; - if (offlineSubText != null) - { - offlineSubText.Text = hasInternet - ? "Carousel is temporarily unavailable." - : "Connect to Wi-Fi or Ethernet to load the carousel."; - } - return; - } - - if (offlinePanel != null) - offlinePanel.IsVisible = false; - - _carouselImages = CreateCarouselImages(bitmaps); - EnsureZoomSlots(_carouselImages.Length); - - foreach (var existingImage in carouselContainer.Children.OfType().ToList()) - carouselContainer.Children.Remove(existingImage); - - int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; - for (int i = 0; i < _carouselImages.Length; i++) - { - if (overlayIndex >= 0) - { - carouselContainer.Children.Insert(overlayIndex, _carouselImages[i]); - overlayIndex++; - } - else - { - carouselContainer.Children.Add(_carouselImages[i]); - } - } - - _currentCarouselIndex = 0; - _carouselImages[0].Opacity = 1.0; - StartZoomOut(_carouselImages[0], 0); + // ── Image carousel (center content area) ────────────────────────────────── + private static readonly HttpClient _http = new(); + private static string PatchNotesCachePath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache", + "patchnotes.md"); + + private async Task SetupCarouselAsync() + { + try + { + TeardownCarousel(); + + var carouselContainer = this.FindControl("CarouselContainer"); + var offlinePanel = this.FindControl("CarouselOfflinePanel"); + var offlineSubText = this.FindControl("CarouselOfflineSubText"); + if (carouselContainer == null) + return; + + bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); + var bitmaps = hasInternet + ? await LoadCarouselFromGitHubAsync() + : null; + + if (bitmaps == null || bitmaps.Count == 0) + bitmaps = LoadEmbeddedCarouselImages(); + + if (bitmaps.Count == 0) + { + if (offlinePanel != null) + offlinePanel.IsVisible = true; + if (offlineSubText != null) + { + offlineSubText.Text = hasInternet + ? "Carousel is temporarily unavailable." + : "Connect to Wi-Fi or Ethernet to load the carousel."; + } + return; + } + + if (offlinePanel != null) + offlinePanel.IsVisible = false; + + _carouselImages = CreateCarouselImages(bitmaps); + EnsureZoomSlots(_carouselImages.Length); + + foreach (var existingImage in carouselContainer.Children.OfType().ToList()) + carouselContainer.Children.Remove(existingImage); + + int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; + for (int i = 0; i < _carouselImages.Length; i++) + { + if (overlayIndex >= 0) + { + carouselContainer.Children.Insert(overlayIndex, _carouselImages[i]); + overlayIndex++; + } + else + { + carouselContainer.Children.Add(_carouselImages[i]); + } + } + + _currentCarouselIndex = 0; + _carouselImages[0].Opacity = 1.0; + StartZoomOut(_carouselImages[0], 0); _carouselTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CarouselRotationIntervalSeconds) }; _carouselTimer.Tick += (_, _) => RotateCarousel(); @@ -173,35 +173,35 @@ private async Task SetupCarouselAsync() } } - private async Task?> LoadCarouselFromGitHubAsync() - { - try - { - var json = await Api.GitHub.GetCarouselAssetsWauncher(); - var assets = JsonConvert.DeserializeObject>(json); - if (assets == null || assets.Count == 0) - return null; - - var urls = assets - .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) - .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) - .OrderBy(a => GetCarouselSortIndex(a.Name)) - .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) - .Select(a => a.DownloadUrl!) - .ToList(); - - if (urls.Count == 0) - return null; - - var bitmaps = new List(); - foreach (var url in urls) - { - try + private async Task?> LoadCarouselFromGitHubAsync() + { + try + { + var json = await Api.GitHub.GetCarouselAssetsWauncher(); + var assets = JsonConvert.DeserializeObject>(json); + if (assets == null || assets.Count == 0) + return null; + + var urls = assets + .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) + .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) + .OrderBy(a => GetCarouselSortIndex(a.Name)) + .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) + .Select(a => a.DownloadUrl!) + .ToList(); + + if (urls.Count == 0) + return null; + + var bitmaps = new List(); + foreach (var url in urls) + { + try { var bytes = await _http.GetByteArrayAsync(url); using var ms = new MemoryStream(bytes); @@ -210,63 +210,63 @@ private async Task SetupCarouselAsync() catch { } } return bitmaps.Count > 0 ? bitmaps : null; - } - catch { return null; } - } - - private static int GetCarouselSortIndex(string name) - { - var match = Regex.Match(name, @"^carousel_(\d+)", RegexOptions.IgnoreCase); - if (match.Success && int.TryParse(match.Groups[1].Value, out var index)) - return index; - return int.MaxValue; - } - - private sealed class GitHubAssetEntry - { - [JsonProperty("name")] - public string Name { get; set; } = string.Empty; - - [JsonProperty("type")] - public string Type { get; set; } = string.Empty; - - [JsonProperty("download_url")] - public string? DownloadUrl { get; set; } - } - - private static Image[] CreateCarouselImages(IReadOnlyList bitmaps) - { - var images = new Image[bitmaps.Count]; - for (int i = 0; i < bitmaps.Count; i++) - { - images[i] = new Image - { - Source = bitmaps[i], - Stretch = Stretch.UniformToFill, - Opacity = 0.0, - Transitions = new Transitions - { - new DoubleTransition - { - Property = Visual.OpacityProperty, - Duration = TimeSpan.FromSeconds(1.5), - Easing = new CubicEaseInOut() - } - } - }; - } - - return images; - } - - private void EnsureZoomSlots(int count) - { - while (_zoomCts.Count < count) - _zoomCts.Add(null); - } - - private static List LoadEmbeddedCarouselImages() - { + } + catch { return null; } + } + + private static int GetCarouselSortIndex(string name) + { + var match = Regex.Match(name, @"^carousel_(\d+)", RegexOptions.IgnoreCase); + if (match.Success && int.TryParse(match.Groups[1].Value, out var index)) + return index; + return int.MaxValue; + } + + private sealed class GitHubAssetEntry + { + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("type")] + public string Type { get; set; } = string.Empty; + + [JsonProperty("download_url")] + public string? DownloadUrl { get; set; } + } + + private static Image[] CreateCarouselImages(IReadOnlyList bitmaps) + { + var images = new Image[bitmaps.Count]; + for (int i = 0; i < bitmaps.Count; i++) + { + images[i] = new Image + { + Source = bitmaps[i], + Stretch = Stretch.UniformToFill, + Opacity = 0.0, + Transitions = new Transitions + { + new DoubleTransition + { + Property = Visual.OpacityProperty, + Duration = TimeSpan.FromSeconds(1.5), + Easing = new CubicEaseInOut() + } + } + }; + } + + return images; + } + + private void EnsureZoomSlots(int count) + { + while (_zoomCts.Count < count) + _zoomCts.Add(null); + } + + private static List LoadEmbeddedCarouselImages() + { var bitmaps = new List(); string[] files = { "carousel_0.png", "carousel_1.png", "carousel_2.png", "carousel_3.png" }; foreach (var file in files) @@ -282,29 +282,29 @@ private static List LoadEmbeddedCarouselImages() return bitmaps; } - private void RotateCarousel() - { - if (_carouselImages.Length == 0) - return; - - // Fade out current image (zoom continues through the crossfade) - _carouselImages[_currentCarouselIndex].Opacity = 0.0; - - // Move to next image - _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; - - // Fade in next image and start fresh zoom-out - StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); - _carouselImages[_currentCarouselIndex].Opacity = 1.0; - } - - private void TeardownCarousel() - { - _carouselTimer?.Stop(); - _carouselTimer = null; - for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); - _carouselImages = Array.Empty(); - } + private void RotateCarousel() + { + if (_carouselImages.Length == 0) + return; + + // Fade out current image (zoom continues through the crossfade) + _carouselImages[_currentCarouselIndex].Opacity = 0.0; + + // Move to next image + _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; + + // Fade in next image and start fresh zoom-out + StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); + _carouselImages[_currentCarouselIndex].Opacity = 1.0; + } + + private void TeardownCarousel() + { + _carouselTimer?.Stop(); + _carouselTimer = null; + for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); + _carouselImages = Array.Empty(); + } private void StartZoomOut(Image img, int slot) { @@ -334,28 +334,28 @@ private void StartZoomOut(Image img, int slot) zoomTimer.Start(); } - private void StopZoom(int slot) - { - if (slot < 0 || slot >= _zoomCts.Count) - return; - - _zoomCts[slot]?.Cancel(); - _zoomCts[slot] = null; - } + private void StopZoom(int slot) + { + if (slot < 0 || slot >= _zoomCts.Count) + return; + + _zoomCts[slot]?.Cancel(); + _zoomCts[slot] = null; + } // ── Server dropdown ─────────────────────────────────────────── - private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) - { - CloseDropdown(); - return; - } - - _dropdownOpen = !_dropdownOpen; - - if (DataContext is MainWindowViewModel vm) - vm.IsDropdownOpen = _dropdownOpen; + private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) + { + CloseDropdown(); + return; + } + + _dropdownOpen = !_dropdownOpen; + + if (DataContext is MainWindowViewModel vm) + vm.IsDropdownOpen = _dropdownOpen; if (_dropdownOpen) { @@ -387,39 +387,39 @@ private void CloseDropdown() } // ── Game launch ─────────────────────────────────────────── - private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - - if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || Volatile.Read(ref _launchInProgress) == 1) - return; - else if (vm.IsNeedingInstall) - _ = InstallGameFromCdnAsync(); - else if (_settings.SkipUpdates) - _ = LaunchGameAsync(); - else if (vm.UpdateAvailable) - { - if (_selfUpdateAvailable) - _ = Button_SelfUpdateAsync(); - else - Button_Update(sender, e); - } - else - _ = LaunchGameAsync(); - } - - private async Task LaunchGameAsync() - { - if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) - return; - - var vm = DataContext as MainWindowViewModel; - try - { - _settings = SettingsWindowViewModel.LoadGlobal(); - + private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || Volatile.Read(ref _launchInProgress) == 1) + return; + else if (vm.IsNeedingInstall) + _ = InstallGameFromCdnAsync(); + else if (_settings.SkipUpdates) + _ = LaunchGameAsync(); + else if (vm.UpdateAvailable) + { + if (_selfUpdateAvailable) + _ = Button_SelfUpdateAsync(); + else + Button_Update(sender, e); + } + else + _ = LaunchGameAsync(); + } + + private async Task LaunchGameAsync() + { + if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) + return; + + var vm = DataContext as MainWindowViewModel; + try + { + _settings = SettingsWindowViewModel.LoadGlobal(); + if (vm != null) vm.GameStatus = "Running"; // Clear any arguments left over from a previous launch before adding new ones. @@ -430,9 +430,9 @@ private async Task LaunchGameAsync() if (!_settings.DiscordRpc) Argument.AddArgument("--disable-rpc"); - if (!string.IsNullOrWhiteSpace(_settings.LaunchOptions)) - foreach (var arg in ParseLaunchOptions(_settings.LaunchOptions)) - Argument.AddArgument(arg); + if (!string.IsNullOrWhiteSpace(_settings.LaunchOptions)) + foreach (var arg in ParseLaunchOptions(_settings.LaunchOptions)) + Argument.AddArgument(arg); var selected = vm?.SelectedServer; if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) @@ -454,16 +454,16 @@ private async Task LaunchGameAsync() await Game.Monitor(); } - catch (Exception ex) - { - Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); - } - finally - { - if (vm != null) vm.GameStatus = "Not Running"; - Interlocked.Exchange(ref _launchInProgress, 0); - } - } + catch (Exception ex) + { + Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); + } + finally + { + if (vm != null) vm.GameStatus = "Not Running"; + Interlocked.Exchange(ref _launchInProgress, 0); + } + } // ── Window chrome ─────────────────────────────────────────── private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) @@ -493,201 +493,201 @@ public void ForceQuit() Close(); } - private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var dir = WauncherDirectory; - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = dir, - UseShellExecute = true - }); - } - - // ── Update ───────────────────────────────────────────────────── - private CancellationTokenSource? _updateCts; - private Patches? _cachedPatches; - private bool _selfUpdateAvailable; - private string _selfUpdateDownloadUrl = string.Empty; - private string _selfUpdateVersion = string.Empty; - private int _autoSelfUpdateTriggered; + private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var dir = WauncherDirectory; + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = dir, + UseShellExecute = true + }); + } + + // ── Update ───────────────────────────────────────────────────── + private CancellationTokenSource? _updateCts; + private Patches? _cachedPatches; + private bool _selfUpdateAvailable; + private string _selfUpdateDownloadUrl = string.Empty; + private string _selfUpdateVersion = string.Empty; + private int _autoSelfUpdateTriggered; /// /// Called on window open. If csgo.exe is missing, triggers a full CDN install. /// Otherwise runs the normal patch update check. /// - private async Task StartupAsync() - { - // Yield to let Avalonia finish its initial layout/styling pass - // (Loaded sets the button disabled/gray; we need that to settle before overriding) - await Task.Delay(50); - - LaunchUpdateButton.IsEnabled = true; - - string csgoExe = Path.Combine(WauncherDirectory, "csgo.exe"); - if (DataContext is not MainWindowViewModel vm) - return; - - if (!File.Exists(csgoExe)) - { - vm.IsNeedingInstall = true; - var blue = new SolidColorBrush(Color.Parse("#2196F3")); - LaunchUpdateButton.Background = blue; - ArrowButton.Background = blue; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - if (vm?.IsOfflineMode == true) - { - vm.IsNeedingInstall = false; - vm.UpdateAvailable = false; - var green = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = green; - ArrowButton.Background = green; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - _settings = SettingsWindowViewModel.LoadGlobal(); - if (_settings.SkipUpdates) - { - vm!.IsNeedingInstall = false; - vm.UpdateAvailable = false; - vm.IsCheckingUpdates = false; - var green = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = green; - ArrowButton.Background = green; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - await CheckForUpdatesAsync(); - } - - private async Task InstallGameFromCdnAsync() - { - if (DataContext is not MainWindowViewModel vm) return; - if (Interlocked.Exchange(ref _installInProgress, 1) == 1) - return; - - bool installSucceeded = false; - vm.IsNeedingInstall = false; - vm.IsInstalling = true; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = "Connecting..."; + private async Task StartupAsync() + { + // Yield to let Avalonia finish its initial layout/styling pass + // (Loaded sets the button disabled/gray; we need that to settle before overriding) + await Task.Delay(50); + + LaunchUpdateButton.IsEnabled = true; + + string csgoExe = Path.Combine(WauncherDirectory, "csgo.exe"); + if (DataContext is not MainWindowViewModel vm) + return; + + if (!File.Exists(csgoExe)) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + if (vm?.IsOfflineMode == true) + { + vm.IsNeedingInstall = false; + vm.UpdateAvailable = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + _settings = SettingsWindowViewModel.LoadGlobal(); + if (_settings.SkipUpdates) + { + vm!.IsNeedingInstall = false; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + await CheckForUpdatesAsync(); + } + + private async Task InstallGameFromCdnAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + if (Interlocked.Exchange(ref _installInProgress, 1) == 1) + return; + + bool installSucceeded = false; + vm.IsNeedingInstall = false; + vm.IsInstalling = true; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Connecting..."; vm.UpdateStatusSpeed = ""; - try - { - await DownloadManager.InstallFullGame( - onProgress: (file, speed, percent) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = $"Installing {ShortFileName(file)} {percent:F0}%"; - vm.UpdateStatusSpeed = string.IsNullOrWhiteSpace(speed) ? "" : speed; - vm.UpdateProgress = percent; - vm.UpdateIndeterminate = false; - }); - }, - onStatus: status => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = status; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); - if (!vm.UpdateIndeterminate) - vm.UpdateProgress = 0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = extractPercent; - }); - }); - - // Immediately apply any post-install patches so first-time installs - // end in a launch-ready state without requiring a second manual update. - Dispatcher.UIThread.Post(() => - { - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = "Checking post-install patches..."; - vm.UpdateStatusSpeed = ""; - }); - - var patches = await Task.Run(() => PatchManager.ValidatePatches()); - var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); - if (allPatches.Count > 0) - { - int totalFiles = allPatches.Count; - int completed = 0; - - foreach (var patch in allPatches) - { - var extractWatch = new System.Diagnostics.Stopwatch(); - await DownloadManager.DownloadPatch( - patch, - onProgress: (p) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; - vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); - vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; - }); - }, - onExtract: () => - { - extractWatch.Restart(); - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); - vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; - }); - }); - - completed++; - vm.UpdateProgress = (double)completed / totalFiles * 100.0; - } - } - - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = "Game installed and fully updated!"; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = false; - vm.UpdateProgress = 100; - }); - installSucceeded = true; - await Task.Delay(1500); - } - catch (Exception ex) - { - vm.UpdateStatusFile = $"Install error: {ex.Message}"; + try + { + await DownloadManager.InstallFullGame( + onProgress: (file, speed, percent) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = $"Installing {ShortFileName(file)} {percent:F0}%"; + vm.UpdateStatusSpeed = string.IsNullOrWhiteSpace(speed) ? "" : speed; + vm.UpdateProgress = percent; + vm.UpdateIndeterminate = false; + }); + }, + onStatus: status => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = status; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); + if (!vm.UpdateIndeterminate) + vm.UpdateProgress = 0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = extractPercent; + }); + }); + + // Immediately apply any post-install patches so first-time installs + // end in a launch-ready state without requiring a second manual update. + Dispatcher.UIThread.Post(() => + { + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Checking post-install patches..."; + vm.UpdateStatusSpeed = ""; + }); + + var patches = await Task.Run(() => PatchManager.ValidatePatches()); + var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); + if (allPatches.Count > 0) + { + int totalFiles = allPatches.Count; + int completed = 0; + + foreach (var patch in allPatches) + { + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); + + completed++; + vm.UpdateProgress = (double)completed / totalFiles * 100.0; + } + } + + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = "Game installed and fully updated!"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + vm.UpdateProgress = 100; + }); + installSucceeded = true; + await Task.Delay(1500); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Install error: {ex.Message}"; vm.UpdateStatusSpeed = ""; await Task.Delay(4000); } @@ -695,196 +695,196 @@ await DownloadManager.DownloadPatch( { vm.IsInstalling = false; vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - - _cachedPatches = null; - - if (!installSucceeded && !File.Exists(Path.Combine(WauncherDirectory, "csgo.exe"))) - { - vm.IsNeedingInstall = true; - var blue = new SolidColorBrush(Color.Parse("#2196F3")); - LaunchUpdateButton.Background = blue; - ArrowButton.Background = blue; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); - } - else - { - try - { - await CheckForUpdatesAsync(); - } - catch { } - } - - LaunchUpdateButton.IsEnabled = true; - Interlocked.Exchange(ref _installInProgress, 0); - } - } - - private async Task LoadPatchNotesAsync() - { - try - { - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Loading latest patch notes..."; - PatchNotesVersion.IsVisible = true; - }); - - if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) - { - Dispatcher.UIThread.Post(() => - { - var cachedItems = LoadCachedPatchNotes(); - if (cachedItems.Count > 0) - { - PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; - PatchNotesList.ItemsSource = cachedItems; - } - else - { - PatchNotesVersion.Text = "Patch notes are unavailable offline."; - PatchNotesList.ItemsSource = new List(); - } - - PatchNotesVersion.IsVisible = true; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - return; - } - - var md = await Api.GitHub.GetPatchNotesWauncher(); - var items = ParsePatchNotes(md); - SavePatchNotesCache(md); - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = items.Count > 0 - ? $"Updated {DateTime.Now:MMM d, h:mm tt}" - : "Patch notes are currently empty."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - } - catch - { - var items = LoadCachedPatchNotes(); - if (items.Count == 0) - { - items = BuildFallbackPatchNotes(); - } - - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Using fallback patch notes."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - } - } - - private static void SavePatchNotesCache(string markdown) - { - try - { - var directory = Path.GetDirectoryName(PatchNotesCachePath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(PatchNotesCachePath, markdown); - } - catch - { - // Caching is best-effort; keep patch notes functional if disk write fails. - } - } - - private static List LoadCachedPatchNotes() - { - try - { - if (!File.Exists(PatchNotesCachePath)) - { - return new List(); - } - - var markdown = File.ReadAllText(PatchNotesCachePath); - return ParsePatchNotes(markdown); - } - catch - { - return new List(); - } - } - - private static List BuildFallbackPatchNotes() - { - return new List - { - new() { Text = "Anniversary Update", IsMajorHeader = true }, - new() { Text = "What's Changed", IsHeader = true }, - new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, - new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, - new() { Text = "Bug fixes and security improvements.", IsBullet = true }, - }; - } - - private static List ParsePatchNotes(string markdown) - { - var items = new List(); - foreach (var raw in markdown.Split('\n')) - { - var line = raw.TrimEnd(); - if (string.IsNullOrWhiteSpace(line)) continue; - - line = line.Trim(); - line = line.Replace("**", "").Replace("__", ""); - line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); - line = Regex.Replace(line, @"`([^`]*)`", "$1"); - - if (line.StartsWith("# ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsMajorHeader = true - }); - } - else if (line.StartsWith("## ") || line.StartsWith("### ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsHeader = true + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + + _cachedPatches = null; + + if (!installSucceeded && !File.Exists(Path.Combine(WauncherDirectory, "csgo.exe"))) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + } + else + { + try + { + await CheckForUpdatesAsync(); + } + catch { } + } + + LaunchUpdateButton.IsEnabled = true; + Interlocked.Exchange(ref _installInProgress, 0); + } + } + + private async Task LoadPatchNotesAsync() + { + try + { + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Loading latest patch notes..."; + PatchNotesVersion.IsVisible = true; + }); + + if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) + { + Dispatcher.UIThread.Post(() => + { + var cachedItems = LoadCachedPatchNotes(); + if (cachedItems.Count > 0) + { + PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; + PatchNotesList.ItemsSource = cachedItems; + } + else + { + PatchNotesVersion.Text = "Patch notes are unavailable offline."; + PatchNotesList.ItemsSource = new List(); + } + + PatchNotesVersion.IsVisible = true; + PatchNotesScroll.Offset = new Vector(0, 0); }); + return; + } + + var md = await Api.GitHub.GetPatchNotesWauncher(); + var items = ParsePatchNotes(md); + SavePatchNotesCache(md); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = items.Count > 0 + ? $"Updated {DateTime.Now:MMM d, h:mm tt}" + : "Patch notes are currently empty."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + catch + { + var items = LoadCachedPatchNotes(); + if (items.Count == 0) + { + items = BuildFallbackPatchNotes(); } - else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Substring(2).Trim(), - IsBullet = true - }); - } - else if (Regex.IsMatch(line, @"^\d+\.\s+")) - { - var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); - items.Add(new ViewModels.PatchNoteItem - { - Text = bulletText, - IsBullet = true - }); - } - else if (line.StartsWith("**") && line.EndsWith("**")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Trim('*', ' '), + + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Using fallback patch notes."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + } + + private static void SavePatchNotesCache(string markdown) + { + try + { + var directory = Path.GetDirectoryName(PatchNotesCachePath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(PatchNotesCachePath, markdown); + } + catch + { + // Caching is best-effort; keep patch notes functional if disk write fails. + } + } + + private static List LoadCachedPatchNotes() + { + try + { + if (!File.Exists(PatchNotesCachePath)) + { + return new List(); + } + + var markdown = File.ReadAllText(PatchNotesCachePath); + return ParsePatchNotes(markdown); + } + catch + { + return new List(); + } + } + + private static List BuildFallbackPatchNotes() + { + return new List + { + new() { Text = "Anniversary Update", IsMajorHeader = true }, + new() { Text = "What's Changed", IsHeader = true }, + new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, + new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, + new() { Text = "Bug fixes and security improvements.", IsBullet = true }, + }; + } + + private static List ParsePatchNotes(string markdown) + { + var items = new List(); + foreach (var raw in markdown.Split('\n')) + { + var line = raw.TrimEnd(); + if (string.IsNullOrWhiteSpace(line)) continue; + + line = line.Trim(); + line = line.Replace("**", "").Replace("__", ""); + line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); + line = Regex.Replace(line, @"`([^`]*)`", "$1"); + + if (line.StartsWith("# ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsMajorHeader = true + }); + } + else if (line.StartsWith("## ") || line.StartsWith("### ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsHeader = true + }); + } + else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Substring(2).Trim(), + IsBullet = true + }); + } + else if (Regex.IsMatch(line, @"^\d+\.\s+")) + { + var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); + items.Add(new ViewModels.PatchNoteItem + { + Text = bulletText, + IsBullet = true + }); + } + else if (line.StartsWith("**") && line.EndsWith("**")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Trim('*', ' '), IsHeader = true }); } @@ -897,329 +897,329 @@ private static void SavePatchNotesCache(string markdown) }); } } - return items; - } - - private sealed class GitHubRelease - { - [JsonProperty("tag_name")] - public string? TagName { get; set; } - - [JsonProperty("assets")] - public List? Assets { get; set; } - } - - private sealed class GitHubReleaseAsset - { - [JsonProperty("name")] - public string Name { get; set; } = string.Empty; - - [JsonProperty("browser_download_url")] - public string DownloadUrl { get; set; } = string.Empty; - } - - private static string NormalizeVersionToken(string? version) - { - if (string.IsNullOrWhiteSpace(version)) - return "0.0.0"; - - var cleaned = version.Trim(); - if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase)) - cleaned = cleaned[1..]; - - cleaned = Regex.Replace(cleaned, @"[^0-9\.]", string.Empty); - return string.IsNullOrWhiteSpace(cleaned) ? "0.0.0" : cleaned; - } - - private static bool TryParseVersion(string value, out global::System.Version parsed) - { - if (global::System.Version.TryParse(value, out parsed!)) - return true; - - var tokens = value.Split('.', StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length == 0) - { - parsed = new global::System.Version(0, 0, 0); - return false; - } - - while (tokens.Length < 3) - tokens = tokens.Append("0").ToArray(); - - return global::System.Version.TryParse(string.Join('.', tokens.Take(4)), out parsed!); - } - - private async Task CheckForSelfUpdateAsync() - { - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - - try - { - var latestReleaseJson = await Api.GitHub.GetLatestRelease(); - var release = JsonConvert.DeserializeObject(latestReleaseJson); - if (release == null) - return false; - - var currentVersion = NormalizeVersionToken(Wauncher.Utils.Version.Current); - var latestVersion = NormalizeVersionToken(release.TagName); - if (!TryParseVersion(currentVersion, out var current) || !TryParseVersion(latestVersion, out var latest)) - return false; - - if (latest <= current) - return false; - - var assets = release.Assets ?? new List(); - var preferred = assets.FirstOrDefault(a => - !string.IsNullOrWhiteSpace(a.DownloadUrl) && - string.Equals(a.Name, "wauncher.exe", StringComparison.OrdinalIgnoreCase)); - - preferred ??= assets.FirstOrDefault(a => - !string.IsNullOrWhiteSpace(a.DownloadUrl) && - a.Name.Contains("wauncher", StringComparison.OrdinalIgnoreCase) && - a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); - - if (preferred == null) - return false; - - _selfUpdateAvailable = true; - _selfUpdateDownloadUrl = preferred.DownloadUrl; - _selfUpdateVersion = latestVersion; - return true; - } - catch - { - return false; - } - } - - private async Task DownloadFileWithProgressAsync(string url, string destination, Action? onProgress, CancellationToken token) - { - using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); - response.EnsureSuccessStatusCode(); - - var totalBytes = response.Content.Headers.ContentLength; - await using var input = await response.Content.ReadAsStreamAsync(token); - await using var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true); - - var buffer = new byte[81920]; - long received = 0; - while (true) - { - int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), token); - if (read == 0) - break; - - await output.WriteAsync(buffer.AsMemory(0, read), token); - received += read; - if (totalBytes.HasValue && totalBytes.Value > 0) - { - onProgress?.Invoke((double)received / totalBytes.Value * 100.0); - } - } - - onProgress?.Invoke(100.0); - } - - private static string BuildSelfUpdateScript(string stagedExePath, string currentExePath) - { - return -$@"@echo off -setlocal -set ""SRC={stagedExePath}"" -set ""DST={currentExePath}"" - -for /L %%i in (1,1,60) do ( - copy /Y ""%SRC%"" ""%DST%"" >nul 2>nul && goto copied - timeout /t 1 /nobreak >nul -) - -exit /b 1 - -:copied -start """" ""%DST%"" -del /Q ""%SRC%"" >nul 2>nul -del /Q ""%~f0"" >nul 2>nul -exit /b 0 -"; - } - - private async Task Button_SelfUpdateAsync() - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) - return; - - if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) - return; - - _updateCts?.Dispose(); - _updateCts = new CancellationTokenSource(); - var token = _updateCts.Token; - - vm.IsUpdating = true; - vm.UpdateAvailable = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = "Downloading Wauncher update..."; - vm.UpdateStatusSpeed = ""; - - try - { - if (string.IsNullOrWhiteSpace(_selfUpdateDownloadUrl)) - throw new Exception("No self-update package URL found."); - - var updatesDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "self-update"); - Directory.CreateDirectory(updatesDir); - - var safeVersion = Regex.Replace(_selfUpdateVersion, @"[^0-9A-Za-z\.\-_]", string.Empty); - if (string.IsNullOrWhiteSpace(safeVersion)) - safeVersion = "latest"; - - var stagedExePath = Path.Combine(updatesDir, $"wauncher_{safeVersion}.exe"); - await DownloadFileWithProgressAsync(_selfUpdateDownloadUrl, stagedExePath, percent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateProgress = percent; - vm.UpdateStatusFile = $"Downloading Wauncher update... {percent:F0}%"; - }); - }, token); - - var currentExePath = Services.GetExePath(); - if (string.IsNullOrWhiteSpace(currentExePath)) - throw new Exception("Could not locate current Wauncher executable."); - - var scriptPath = Path.Combine(updatesDir, "apply_wauncher_update.cmd"); - var script = BuildSelfUpdateScript(stagedExePath, currentExePath); - File.WriteAllText(scriptPath, script, Encoding.ASCII); - - Process.Start(new ProcessStartInfo - { - FileName = "cmd.exe", - Arguments = $"/c \"{scriptPath}\"", - WorkingDirectory = updatesDir, - CreateNoWindow = true, - UseShellExecute = false, - }); - - vm.UpdateStatusFile = "Restarting Wauncher to apply update..."; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = 100; - await Task.Delay(500, token); - ForceQuit(); - } - catch (OperationCanceledException) - { - vm.UpdateStatusFile = "Update cancelled."; - vm.UpdateStatusSpeed = ""; - await Task.Delay(800); - } - catch (Exception ex) - { - vm.UpdateStatusFile = $"Self-update failed: {ex.Message}"; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = false; - await Task.Delay(2500); - } - finally - { - if (!_forceClose) - { - vm.IsUpdating = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - _updateCts?.Dispose(); - _updateCts = null; - - try - { - await CheckForUpdatesAsync(); - } - catch - { - // keep UI responsive even if refresh fails - } - } - - Interlocked.Exchange(ref _updateInProgress, 0); - } - } - - private async Task CheckForUpdatesAsync() - { - if (DataContext is not MainWindowViewModel vm) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - - if (vm.IsOfflineMode) - { - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - _cachedPatches = null; - vm.UpdateAvailable = false; - vm.IsCheckingUpdates = false; - var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = launchColor; - ArrowButton.Background = launchColor; - LaunchUpdateButton.IsEnabled = true; - return; - } - - vm.IsCheckingUpdates = true; - LaunchUpdateButton.Background = new SolidColorBrush(Color.Parse("#555555")); - ArrowButton.Background = new SolidColorBrush(Color.Parse("#555555")); - LaunchUpdateButton.IsEnabled = false; - try - { - bool hasSelfUpdate = await CheckForSelfUpdateAsync(); - if (hasSelfUpdate) - { - _cachedPatches = null; - vm.UpdateAvailable = true; - var selfUpdateColor = new SolidColorBrush(Color.Parse("#FFC107")); - LaunchUpdateButton.Background = selfUpdateColor; - ArrowButton.Background = selfUpdateColor; - - if (Interlocked.Exchange(ref _autoSelfUpdateTriggered, 1) == 0) - { - _ = Button_SelfUpdateAsync(); - } - - return; - } - - if (_settings.SkipUpdates) - { - _cachedPatches = null; - vm.UpdateAvailable = false; - var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = launchColor; - ArrowButton.Background = launchColor; - return; - } - - var patches = await Task.Run(() => PatchManager.ValidatePatches(deleteOutdatedFiles: false)); - bool hasUpdates = patches.Missing.Count > 0 || patches.Outdated.Count > 0; - - // Cache the result so Button_Update can consume it without re-validating. - _cachedPatches = patches; - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - vm.UpdateAvailable = hasUpdates; - var buttonColor = new SolidColorBrush( - Color.Parse(hasUpdates ? "#FFC107" : "#4CAF50")); + return items; + } + + private sealed class GitHubRelease + { + [JsonProperty("tag_name")] + public string? TagName { get; set; } + + [JsonProperty("assets")] + public List? Assets { get; set; } + } + + private sealed class GitHubReleaseAsset + { + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("browser_download_url")] + public string DownloadUrl { get; set; } = string.Empty; + } + + private static string NormalizeVersionToken(string? version) + { + if (string.IsNullOrWhiteSpace(version)) + return "0.0.0"; + + var cleaned = version.Trim(); + if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + cleaned = cleaned[1..]; + + cleaned = Regex.Replace(cleaned, @"[^0-9\.]", string.Empty); + return string.IsNullOrWhiteSpace(cleaned) ? "0.0.0" : cleaned; + } + + private static bool TryParseVersion(string value, out global::System.Version parsed) + { + if (global::System.Version.TryParse(value, out parsed!)) + return true; + + var tokens = value.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 0) + { + parsed = new global::System.Version(0, 0, 0); + return false; + } + + while (tokens.Length < 3) + tokens = tokens.Append("0").ToArray(); + + return global::System.Version.TryParse(string.Join('.', tokens.Take(4)), out parsed!); + } + + private async Task CheckForSelfUpdateAsync() + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + + try + { + var latestReleaseJson = await Api.GitHub.GetLatestRelease(); + var release = JsonConvert.DeserializeObject(latestReleaseJson); + if (release == null) + return false; + + var currentVersion = NormalizeVersionToken(Wauncher.Utils.Version.Current); + var latestVersion = NormalizeVersionToken(release.TagName); + if (!TryParseVersion(currentVersion, out var current) || !TryParseVersion(latestVersion, out var latest)) + return false; + + if (latest <= current) + return false; + + var assets = release.Assets ?? new List(); + var preferred = assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + string.Equals(a.Name, "wauncher.exe", StringComparison.OrdinalIgnoreCase)); + + preferred ??= assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + a.Name.Contains("wauncher", StringComparison.OrdinalIgnoreCase) && + a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); + + if (preferred == null) + return false; + + _selfUpdateAvailable = true; + _selfUpdateDownloadUrl = preferred.DownloadUrl; + _selfUpdateVersion = latestVersion; + return true; + } + catch + { + return false; + } + } + + private async Task DownloadFileWithProgressAsync(string url, string destination, Action? onProgress, CancellationToken token) + { + using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); + response.EnsureSuccessStatusCode(); + + var totalBytes = response.Content.Headers.ContentLength; + await using var input = await response.Content.ReadAsStreamAsync(token); + await using var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true); + + var buffer = new byte[81920]; + long received = 0; + while (true) + { + int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), token); + if (read == 0) + break; + + await output.WriteAsync(buffer.AsMemory(0, read), token); + received += read; + if (totalBytes.HasValue && totalBytes.Value > 0) + { + onProgress?.Invoke((double)received / totalBytes.Value * 100.0); + } + } + + onProgress?.Invoke(100.0); + } + + private static string BuildSelfUpdateScript(string stagedExePath, string currentExePath) + { + return +$@"@echo off +setlocal +set ""SRC={stagedExePath}"" +set ""DST={currentExePath}"" + +for /L %%i in (1,1,60) do ( + copy /Y ""%SRC%"" ""%DST%"" >nul 2>nul && goto copied + timeout /t 1 /nobreak >nul +) + +exit /b 1 + +:copied +start """" ""%DST%"" +del /Q ""%SRC%"" >nul 2>nul +del /Q ""%~f0"" >nul 2>nul +exit /b 0 +"; + } + + private async Task Button_SelfUpdateAsync() + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) + return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = "Downloading Wauncher update..."; + vm.UpdateStatusSpeed = ""; + + try + { + if (string.IsNullOrWhiteSpace(_selfUpdateDownloadUrl)) + throw new Exception("No self-update package URL found."); + + var updatesDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "self-update"); + Directory.CreateDirectory(updatesDir); + + var safeVersion = Regex.Replace(_selfUpdateVersion, @"[^0-9A-Za-z\.\-_]", string.Empty); + if (string.IsNullOrWhiteSpace(safeVersion)) + safeVersion = "latest"; + + var stagedExePath = Path.Combine(updatesDir, $"wauncher_{safeVersion}.exe"); + await DownloadFileWithProgressAsync(_selfUpdateDownloadUrl, stagedExePath, percent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateProgress = percent; + vm.UpdateStatusFile = $"Downloading Wauncher update... {percent:F0}%"; + }); + }, token); + + var currentExePath = Services.GetExePath(); + if (string.IsNullOrWhiteSpace(currentExePath)) + throw new Exception("Could not locate current Wauncher executable."); + + var scriptPath = Path.Combine(updatesDir, "apply_wauncher_update.cmd"); + var script = BuildSelfUpdateScript(stagedExePath, currentExePath); + File.WriteAllText(scriptPath, script, Encoding.ASCII); + + Process.Start(new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/c \"{scriptPath}\"", + WorkingDirectory = updatesDir, + CreateNoWindow = true, + UseShellExecute = false, + }); + + vm.UpdateStatusFile = "Restarting Wauncher to apply update..."; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 100; + await Task.Delay(500, token); + ForceQuit(); + } + catch (OperationCanceledException) + { + vm.UpdateStatusFile = "Update cancelled."; + vm.UpdateStatusSpeed = ""; + await Task.Delay(800); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Self-update failed: {ex.Message}"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + await Task.Delay(2500); + } + finally + { + if (!_forceClose) + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch + { + // keep UI responsive even if refresh fails + } + } + + Interlocked.Exchange(ref _updateInProgress, 0); + } + } + + private async Task CheckForUpdatesAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsOfflineMode) + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + _cachedPatches = null; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + LaunchUpdateButton.IsEnabled = true; + return; + } + + vm.IsCheckingUpdates = true; + LaunchUpdateButton.Background = new SolidColorBrush(Color.Parse("#555555")); + ArrowButton.Background = new SolidColorBrush(Color.Parse("#555555")); + LaunchUpdateButton.IsEnabled = false; + try + { + bool hasSelfUpdate = await CheckForSelfUpdateAsync(); + if (hasSelfUpdate) + { + _cachedPatches = null; + vm.UpdateAvailable = true; + var selfUpdateColor = new SolidColorBrush(Color.Parse("#FFC107")); + LaunchUpdateButton.Background = selfUpdateColor; + ArrowButton.Background = selfUpdateColor; + + if (Interlocked.Exchange(ref _autoSelfUpdateTriggered, 1) == 0) + { + _ = Button_SelfUpdateAsync(); + } + + return; + } + + if (_settings.SkipUpdates) + { + _cachedPatches = null; + vm.UpdateAvailable = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + return; + } + + var patches = await Task.Run(() => PatchManager.ValidatePatches(deleteOutdatedFiles: false)); + bool hasUpdates = patches.Missing.Count > 0 || patches.Outdated.Count > 0; + + // Cache the result so Button_Update can consume it without re-validating. + _cachedPatches = patches; + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + vm.UpdateAvailable = hasUpdates; + var buttonColor = new SolidColorBrush( + Color.Parse(hasUpdates ? "#FFC107" : "#4CAF50")); LaunchUpdateButton.Background = buttonColor; ArrowButton.Background = buttonColor; } @@ -1236,21 +1236,21 @@ private async Task CheckForUpdatesAsync() } } - private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) return; - - if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) - return; - - _updateCts?.Dispose(); - _updateCts = new CancellationTokenSource(); - var token = _updateCts.Token; - vm.IsUpdating = true; - vm.UpdateAvailable = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; + private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; vm.UpdateStatus = ""; try @@ -1275,44 +1275,44 @@ private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEv int totalFiles = allPatches.Count; int completed = 0; - foreach (var patch in allPatches) - { - if (token.IsCancellationRequested) break; - - var extractWatch = new System.Diagnostics.Stopwatch(); - await DownloadManager.DownloadPatch( - patch, - onProgress: (p) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; - vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); - vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; - }); - }, - onExtract: () => - { - extractWatch.Restart(); - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); - vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; - }); - }); + foreach (var patch in allPatches) + { + if (token.IsCancellationRequested) break; + + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); completed++; vm.UpdateProgress = (double)completed / totalFiles * 100.0; @@ -1339,27 +1339,27 @@ await DownloadManager.DownloadPatch( vm.UpdateIndeterminate = false; await Task.Delay(3000); } - finally - { - vm.IsUpdating = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - _cachedPatches = null; - _updateCts?.Dispose(); - _updateCts = null; - - try - { - await CheckForUpdatesAsync(); - } - catch { } - - Interlocked.Exchange(ref _updateInProgress, 0); - } - } + finally + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _cachedPatches = null; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch { } + + Interlocked.Exchange(ref _updateInProgress, 0); + } + } private void Button_CancelUpdate(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { @@ -1372,79 +1372,79 @@ private void FriendsTab_Click(object? sender, Avalonia.Interactivity.RoutedEvent if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "Friends"; } - private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "PatchNotes"; - PatchNotesScroll.Offset = new Vector(0, 0); - } - - private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (sender is not MenuItem { Tag: FriendInfo friend }) - return; - - var profileId = ResolveProfileSteamId(friend.SteamId); - if (string.IsNullOrWhiteSpace(profileId)) - return; - - try - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = $"https://eddies.cc/profiles/{profileId}", - UseShellExecute = true - }); - } - catch - { - // Best-effort open. - } - } - - private static string ResolveProfileSteamId(string? steamId) - { - if (string.IsNullOrWhiteSpace(steamId)) - return string.Empty; - - var value = steamId.Trim(); - if (ulong.TryParse(value, out _)) - return value; - - if (TryConvertSteamId2To64(value, out var steamId64)) - return steamId64.ToString(); - - return string.Empty; - } - - private static bool TryConvertSteamId2To64(string steamId2, out ulong steamId64) - { - steamId64 = 0; - var match = Regex.Match(steamId2, @"^STEAM_[0-5]:([0-1]):(\d+)$", RegexOptions.IgnoreCase); - if (!match.Success) - return false; - - if (!ulong.TryParse(match.Groups[1].Value, out var y)) - return false; - if (!ulong.TryParse(match.Groups[2].Value, out var z)) - return false; - - steamId64 = 76561197960265728UL + (z * 2UL) + y; - return true; - } - - private void Button_Settings(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { + private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "PatchNotes"; + PatchNotesScroll.Offset = new Vector(0, 0); + } + + private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: FriendInfo friend }) + return; + + var profileId = ResolveProfileSteamId(friend.SteamId); + if (string.IsNullOrWhiteSpace(profileId)) + return; + + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = $"https://eddies.cc/profiles/{profileId}", + UseShellExecute = true + }); + } + catch + { + // Best-effort open. + } + } + + private static string ResolveProfileSteamId(string? steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) + return string.Empty; + + var value = steamId.Trim(); + if (ulong.TryParse(value, out _)) + return value; + + if (TryConvertSteamId2To64(value, out var steamId64)) + return steamId64.ToString(); + + return string.Empty; + } + + private static bool TryConvertSteamId2To64(string steamId2, out ulong steamId64) + { + steamId64 = 0; + var match = Regex.Match(steamId2, @"^STEAM_[0-5]:([0-1]):(\d+)$", RegexOptions.IgnoreCase); + if (!match.Success) + return false; + + if (!ulong.TryParse(match.Groups[1].Value, out var y)) + return false; + if (!ulong.TryParse(match.Groups[2].Value, out var z)) + return false; + + steamId64 = 76561197960265728UL + (z * 2UL) + y; + return true; + } + + private void Button_Settings(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { if (_settingsWindow == null) { - _settingsWindow = new SettingsWindow(); - _settingsWindow.Closed += (s, e) => - { - _settingsWindow = null; - _settings = SettingsWindowViewModel.LoadGlobal(); - _ = CheckForUpdatesAsync(); - }; - _settingsWindow.Show(this); - } + _settingsWindow = new SettingsWindow(); + _settingsWindow.Closed += (s, e) => + { + _settingsWindow = null; + _settings = SettingsWindowViewModel.LoadGlobal(); + _ = CheckForUpdatesAsync(); + }; + _settingsWindow.Show(this); + } else _settingsWindow.Activate(); } @@ -1460,148 +1460,148 @@ private void Button_Info(object? sender, Avalonia.Interactivity.RoutedEventArgs } // ── Launch button glow + color ──────────────────────────────────────────── - private void SetLaunchGlow(bool updating) - { - var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); - LaunchUpdateButton.Background = brush; - ArrowButton.Background = brush; - LaunchButtonGlow.BoxShadow = updating - ? BoxShadows.Parse("0 0 18 2 #55FFC107") - : BoxShadows.Parse("0 0 18 2 #554CAF50"); - } - - private static string ShortFileName(string path) - { - if (string.IsNullOrWhiteSpace(path)) - return path; - - var normalized = path.Replace('\\', '/'); - if (normalized.Length <= 42) - return normalized; - - var fileName = Path.GetFileName(normalized); - if (fileName.Length <= 30) - return fileName; - - return fileName[..27] + "..."; - } - - private static string FormatDownloadSpeedAndEta(object progressArgs) - { - double speedBytes = 0; - if (TryGetDoubleProperty(progressArgs, "AverageBytesPerSecondSpeed", out var avg) && avg > 0) - speedBytes = avg; - else if (TryGetDoubleProperty(progressArgs, "BytesPerSecondSpeed", out var cur) && cur > 0) - speedBytes = cur; - - var speedText = speedBytes > 0 - ? $"{speedBytes / 1024.0 / 1024.0:F1} MB/s" - : ""; - - if (speedBytes <= 0 || - !TryGetLongProperty(progressArgs, "TotalBytesToReceive", out var totalBytes) || - !TryGetLongProperty(progressArgs, "ReceivedBytesSize", out var receivedBytes) || - totalBytes <= 0 || receivedBytes < 0 || receivedBytes >= totalBytes) - { - return speedText; - } - - var remainingBytes = totalBytes - receivedBytes; - var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); - var etaText = $"ETA {FormatEta(eta)}"; - - return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; - } - - private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) - { - if (watch == null || !watch.IsRunning || percent <= 1.0) - return ""; - - var elapsed = watch.Elapsed.TotalSeconds; - var total = elapsed / (percent / 100.0); - var remaining = Math.Max(0, total - elapsed); - return $"ETA {FormatEta(TimeSpan.FromSeconds(remaining))}"; - } - - private static string FormatEta(TimeSpan eta) - { - if (eta.TotalHours >= 1) - return eta.ToString(@"hh\:mm\:ss"); - return eta.ToString(@"mm\:ss"); - } - - private static bool TryGetDoubleProperty(object obj, string propertyName, out double value) - { - value = 0; - var prop = obj.GetType().GetProperty(propertyName); - if (prop == null) return false; - var raw = prop.GetValue(obj); - if (raw == null) return false; - try - { - value = Convert.ToDouble(raw); - return true; - } - catch - { - return false; - } - } - - private static bool TryGetLongProperty(object obj, string propertyName, out long value) - { - value = 0; - var prop = obj.GetType().GetProperty(propertyName); - if (prop == null) return false; - var raw = prop.GetValue(obj); - if (raw == null) return false; - try - { - value = Convert.ToInt64(raw); - return true; - } - catch - { - return false; - } - } - - // Minimal parser for launch options that supports quoted values. - private static IEnumerable ParseLaunchOptions(string options) - { - if (string.IsNullOrWhiteSpace(options)) - yield break; - - var current = new StringBuilder(); - bool inQuotes = false; - - foreach (var ch in options) - { - if (ch == '"') - { - inQuotes = !inQuotes; - continue; - } - - if (char.IsWhiteSpace(ch) && !inQuotes) - { - if (current.Length > 0) - { - yield return current.ToString(); - current.Clear(); - } - continue; - } - - current.Append(ch); - } - - if (current.Length > 0) - yield return current.ToString(); - } - - } -} + private void SetLaunchGlow(bool updating) + { + var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); + LaunchUpdateButton.Background = brush; + ArrowButton.Background = brush; + LaunchButtonGlow.BoxShadow = updating + ? BoxShadows.Parse("0 0 18 2 #55FFC107") + : BoxShadows.Parse("0 0 18 2 #554CAF50"); + } + + private static string ShortFileName(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return path; + + var normalized = path.Replace('\\', '/'); + if (normalized.Length <= 42) + return normalized; + + var fileName = Path.GetFileName(normalized); + if (fileName.Length <= 30) + return fileName; + + return fileName[..27] + "..."; + } + + private static string FormatDownloadSpeedAndEta(object progressArgs) + { + double speedBytes = 0; + if (TryGetDoubleProperty(progressArgs, "AverageBytesPerSecondSpeed", out var avg) && avg > 0) + speedBytes = avg; + else if (TryGetDoubleProperty(progressArgs, "BytesPerSecondSpeed", out var cur) && cur > 0) + speedBytes = cur; + + var speedText = speedBytes > 0 + ? $"{speedBytes / 1024.0 / 1024.0:F1} MB/s" + : ""; + + if (speedBytes <= 0 || + !TryGetLongProperty(progressArgs, "TotalBytesToReceive", out var totalBytes) || + !TryGetLongProperty(progressArgs, "ReceivedBytesSize", out var receivedBytes) || + totalBytes <= 0 || receivedBytes < 0 || receivedBytes >= totalBytes) + { + return speedText; + } + + var remainingBytes = totalBytes - receivedBytes; + var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); + var etaText = $"ETA {FormatEta(eta)}"; + + return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; + } + + private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) + { + if (watch == null || !watch.IsRunning || percent <= 1.0) + return ""; + + var elapsed = watch.Elapsed.TotalSeconds; + var total = elapsed / (percent / 100.0); + var remaining = Math.Max(0, total - elapsed); + return $"ETA {FormatEta(TimeSpan.FromSeconds(remaining))}"; + } + + private static string FormatEta(TimeSpan eta) + { + if (eta.TotalHours >= 1) + return eta.ToString(@"hh\:mm\:ss"); + return eta.ToString(@"mm\:ss"); + } + + private static bool TryGetDoubleProperty(object obj, string propertyName, out double value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToDouble(raw); + return true; + } + catch + { + return false; + } + } + + private static bool TryGetLongProperty(object obj, string propertyName, out long value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToInt64(raw); + return true; + } + catch + { + return false; + } + } + + // Minimal parser for launch options that supports quoted values. + private static IEnumerable ParseLaunchOptions(string options) + { + if (string.IsNullOrWhiteSpace(options)) + yield break; + + var current = new StringBuilder(); + bool inQuotes = false; + + foreach (var ch in options) + { + if (ch == '"') + { + inQuotes = !inQuotes; + continue; + } + + if (char.IsWhiteSpace(ch) && !inQuotes) + { + if (current.Length > 0) + { + yield return current.ToString(); + current.Clear(); + } + continue; + } + + current.Append(ch); + } + + if (current.Length > 0) + yield return current.ToString(); + } + + } +} From 9e6411d9998580476fd1cc75c70ac1107744087b Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 04:38:22 -0400 Subject: [PATCH 08/51] move verify files to launch menu and remove cli-style options --- Wauncher/Utils/Api.cs | 448 ++- Wauncher/Utils/Argument.cs | 118 +- Wauncher/Utils/Debug.cs | 5 +- Wauncher/Utils/Game.cs | 12 +- Wauncher/Utils/Patch.cs | 8 +- Wauncher/Utils/ProtocolManager.cs | 2 +- .../ViewModels/SettingsWindowViewModel.cs | 6 - Wauncher/Views/MainWindow.axaml | 13 +- Wauncher/Views/MainWindow.axaml.cs | 2559 ++++++++--------- Wauncher/Views/SettingsWindow.axaml | 23 - 10 files changed, 1543 insertions(+), 1651 deletions(-) diff --git a/Wauncher/Utils/Api.cs b/Wauncher/Utils/Api.cs index 813c217..976c7dc 100644 --- a/Wauncher/Utils/Api.cs +++ b/Wauncher/Utils/Api.cs @@ -1,230 +1,220 @@ -using Refit; -using Newtonsoft.Json; - -namespace Wauncher.Utils -{ - public class FullGameDownload - { - public required string File { get; set; } - public required string Link { get; set; } - public required string Hash { get; set; } - } - - public class FullGameDownloadResponse - { - public List? Files { get; set; } - public string? Error { get; set; } - } - - public interface IGitHub - { - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/repos/ClassicCounter/launcher/releases/latest")] - Task GetLatestRelease(); - - [Headers("User-Agent: ClassicCounter Wauncher", - "Accept: application/vnd.github.raw+json")] - [Get("/repos/ClassicCounter/launcher/contents/dependencies.json")] - Task GetDependencies(); - - [Headers("User-Agent: ClassicCounter Wauncher", - "Accept: application/vnd.github.raw+json")] - [Get("/repos/ClassicCounter/launcher/contents/carousel.json")] - Task GetCarouselManifest(); - - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/repos/ClassicCounter/launcher/contents/Wauncher/Assets")] - Task GetCarouselAssetsWauncher(); - - [Headers("User-Agent: ClassicCounter Wauncher", - "Accept: application/vnd.github.raw+json")] - [Get("/repos/ClassicCounter/launcher/contents/Wauncher/patchnotes.md")] - Task GetPatchNotesWauncher(); - } - - public class FriendInfo - { - [JsonProperty("steamid")] - public string SteamId { get; set; } = ""; - - [JsonProperty("steamid2")] - public string? SteamId2 - { - set - { - if (!string.IsNullOrWhiteSpace(value) && string.IsNullOrWhiteSpace(SteamId)) - SteamId = value; - } - } - - [JsonProperty("username")] - public string Username { get; set; } = ""; - - [JsonProperty("avatar_url")] - public string AvatarUrl { get; set; } = ""; - - [JsonProperty("avatar")] - public string? Avatar - { - set - { - if (!string.IsNullOrWhiteSpace(value)) - AvatarUrl = value; - } - } - - [JsonProperty("custom_username")] - public string? CustomUsername - { - set - { - if (!string.IsNullOrWhiteSpace(value)) - Username = value; - } - } - - [JsonProperty("custom_avatar")] - public string? CustomAvatar - { - set - { - if (!string.IsNullOrWhiteSpace(value)) - AvatarUrl = value; - } - } - - [JsonProperty("status")] - public string Status { get; set; } = "Offline"; // "Online" | "Offline" - - public string DotColor => Status == "Online" ? "#4CAF50" : "#888888"; - public bool IsOffline => Status == "Offline"; - public double AvatarOpacity => IsOffline ? 0.35 : 1.0; - public string StatusText => IsOffline ? "Offline" : "In Game"; - public string StatusColor => IsOffline ? "#666666" : "#999999"; - } - - public class FriendsResponse - { - public List? Friends { get; set; } - } - - public interface IEddies - { - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/friendsapi.php")] - Task GetFriends([AliasAs("steamid64")] string steamId64); - - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/friendsapi.php")] - Task GetFriendsBySteamId2([AliasAs("steamid2")] string steamId2); - - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/selfinfo.php")] - Task GetSelfInfo([AliasAs("steamid64")] string steamId64); - } - - public interface IClassicCounter - { - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/patch/get")] - Task GetPatches(); - - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/game/get")] - Task GetFullGameValidate(); - - [Headers("User-Agent: ClassicCounter Wauncher")] - [Get("/game/full")] - Task GetFullGameDownload([Query] string steam_id); - } - - public static class Api - { - private static HttpClientHandler _httpClientHandler = new HttpClientHandler() - { - ServerCertificateCustomValidationCallback = (message, cert, chain, sslErrors) => true - }; - private static HttpClient ClassicCounterApiHttpClient = new HttpClient(_httpClientHandler) - { - BaseAddress = new Uri("https://classiccounter.cc/api") - }; - private static RefitSettings _settings = new RefitSettings(new NewtonsoftJsonContentSerializer()); - public static IGitHub GitHub = RestService.For("https://api.github.com", _settings); - public static IClassicCounter ClassicCounter = Argument.Exists("--ssl-bypass") - ? RestService.For(ClassicCounterApiHttpClient, _settings) - : RestService.For("https://classiccounter.cc/api", _settings); // THIS IS NOT IDEAL, CHANGE THE WORLD OF TRUSTED CERTIFICATES - public static IEddies Eddies = RestService.For("https://eddies.cc/api", _settings); - - public static List ParseFriendsPayload(string? json) - { - if (string.IsNullOrWhiteSpace(json)) - return new List(); - - try - { - var wrapped = JsonConvert.DeserializeObject(json); - if (wrapped?.Friends != null && wrapped.Friends.Count > 0) - return NormalizeFriends(wrapped.Friends); - } - catch - { - // Fall through to array parse. - } - - try - { - var flat = JsonConvert.DeserializeObject>(json); - if (flat != null) - return NormalizeFriends(flat); - } - catch - { - // Ignore and return empty. - } - - return new List(); - } - - public static FriendInfo? ParseSelfInfoPayload(string? json) - { - if (string.IsNullOrWhiteSpace(json)) - return null; - - try - { - var parsed = JsonConvert.DeserializeObject(json); - if (parsed == null) - return null; - - return NormalizeFriends(new[] { parsed }).FirstOrDefault(); - } - catch - { - return null; - } - } - - private static List NormalizeFriends(IEnumerable friends) - { - var normalized = new List(); - foreach (var f in friends) - { - var username = string.IsNullOrWhiteSpace(f.Username) ? "Unknown" : f.Username; - var status = string.Equals(f.Status, "Online", StringComparison.OrdinalIgnoreCase) - ? "Online" - : "Offline"; - - normalized.Add(new FriendInfo - { - SteamId = f.SteamId ?? string.Empty, - Username = username, - AvatarUrl = f.AvatarUrl ?? string.Empty, - Status = status - }); - } - - return normalized; - } - } -} +using Refit; +using Newtonsoft.Json; + +namespace Wauncher.Utils +{ + public class FullGameDownload + { + public required string File { get; set; } + public required string Link { get; set; } + public required string Hash { get; set; } + } + + public class FullGameDownloadResponse + { + public List? Files { get; set; } + public string? Error { get; set; } + } + + public interface IGitHub + { + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/repos/ClassicCounter/launcher/releases/latest")] + Task GetLatestRelease(); + + [Headers("User-Agent: ClassicCounter Wauncher", + "Accept: application/vnd.github.raw+json")] + [Get("/repos/ClassicCounter/launcher/contents/dependencies.json")] + Task GetDependencies(); + + [Headers("User-Agent: ClassicCounter Wauncher", + "Accept: application/vnd.github.raw+json")] + [Get("/repos/ClassicCounter/launcher/contents/carousel.json")] + Task GetCarouselManifest(); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/repos/ClassicCounter/launcher/contents/Wauncher/Assets")] + Task GetCarouselAssetsWauncher(); + + [Headers("User-Agent: ClassicCounter Wauncher", + "Accept: application/vnd.github.raw+json")] + [Get("/repos/ClassicCounter/launcher/contents/Wauncher/patchnotes.md")] + Task GetPatchNotesWauncher(); + } + + public class FriendInfo + { + [JsonProperty("steamid")] + public string SteamId { get; set; } = ""; + + [JsonProperty("steamid2")] + public string? SteamId2 + { + set + { + if (!string.IsNullOrWhiteSpace(value) && string.IsNullOrWhiteSpace(SteamId)) + SteamId = value; + } + } + + [JsonProperty("username")] + public string Username { get; set; } = ""; + + [JsonProperty("avatar_url")] + public string AvatarUrl { get; set; } = ""; + + [JsonProperty("avatar")] + public string? Avatar + { + set + { + if (!string.IsNullOrWhiteSpace(value)) + AvatarUrl = value; + } + } + + [JsonProperty("custom_username")] + public string? CustomUsername + { + set + { + if (!string.IsNullOrWhiteSpace(value)) + Username = value; + } + } + + [JsonProperty("custom_avatar")] + public string? CustomAvatar + { + set + { + if (!string.IsNullOrWhiteSpace(value)) + AvatarUrl = value; + } + } + + [JsonProperty("status")] + public string Status { get; set; } = "Offline"; // "Online" | "Offline" + + public string DotColor => Status == "Online" ? "#4CAF50" : "#888888"; + public bool IsOffline => Status == "Offline"; + public double AvatarOpacity => IsOffline ? 0.35 : 1.0; + public string StatusText => IsOffline ? "Offline" : "In Game"; + public string StatusColor => IsOffline ? "#666666" : "#999999"; + } + + public class FriendsResponse + { + public List? Friends { get; set; } + } + + public interface IEddies + { + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/friendsapi.php")] + Task GetFriends([AliasAs("steamid64")] string steamId64); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/friendsapi.php")] + Task GetFriendsBySteamId2([AliasAs("steamid2")] string steamId2); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/selfinfo.php")] + Task GetSelfInfo([AliasAs("steamid64")] string steamId64); + } + + public interface IClassicCounter + { + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/patch/get")] + Task GetPatches(); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/game/get")] + Task GetFullGameValidate(); + + [Headers("User-Agent: ClassicCounter Wauncher")] + [Get("/game/full")] + Task GetFullGameDownload([Query] string steam_id); + } + + public static class Api + { + private static RefitSettings _settings = new RefitSettings(new NewtonsoftJsonContentSerializer()); + public static IGitHub GitHub = RestService.For("https://api.github.com", _settings); + public static IClassicCounter ClassicCounter = RestService.For("https://classiccounter.cc/api", _settings); + public static IEddies Eddies = RestService.For("https://eddies.cc/api", _settings); + + public static List ParseFriendsPayload(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return new List(); + + try + { + var wrapped = JsonConvert.DeserializeObject(json); + if (wrapped?.Friends != null && wrapped.Friends.Count > 0) + return NormalizeFriends(wrapped.Friends); + } + catch + { + // Fall through to array parse. + } + + try + { + var flat = JsonConvert.DeserializeObject>(json); + if (flat != null) + return NormalizeFriends(flat); + } + catch + { + // Ignore and return empty. + } + + return new List(); + } + + public static FriendInfo? ParseSelfInfoPayload(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return null; + + try + { + var parsed = JsonConvert.DeserializeObject(json); + if (parsed == null) + return null; + + return NormalizeFriends(new[] { parsed }).FirstOrDefault(); + } + catch + { + return null; + } + } + + private static List NormalizeFriends(IEnumerable friends) + { + var normalized = new List(); + foreach (var f in friends) + { + var username = string.IsNullOrWhiteSpace(f.Username) ? "Unknown" : f.Username; + var status = string.Equals(f.Status, "Online", StringComparison.OrdinalIgnoreCase) + ? "Online" + : "Offline"; + + normalized.Add(new FriendInfo + { + SteamId = f.SteamId ?? string.Empty, + Username = username, + AvatarUrl = f.AvatarUrl ?? string.Empty, + Status = status + }); + } + + return normalized; + } + } +} diff --git a/Wauncher/Utils/Argument.cs b/Wauncher/Utils/Argument.cs index a053c27..e64e917 100644 --- a/Wauncher/Utils/Argument.cs +++ b/Wauncher/Utils/Argument.cs @@ -1,68 +1,50 @@ -namespace Wauncher.Utils -{ - public static class Argument - { - private static List _launcherArguments = new() - { - "--debug-mode", - "--skip-updates", - "--skip-validating", - "--validate-all", - "--patch-only", - "--gc", - "--disable-rpc", - "--install-dependencies", - "--protocol-command", - "--ssl-bypass" - }; - - private static List _additionalArguments = new(); - public static void AddArgument(string argument) - { - if (!_additionalArguments.Any(a => string.Equals(a, argument, StringComparison.OrdinalIgnoreCase))) - _additionalArguments.Add(argument); - } - - public static void ClearAdditionalArguments() - { - _additionalArguments.Clear(); - } - - public static bool Exists(string argument) - { - IEnumerable arguments = Environment.GetCommandLineArgs(); - - foreach (string arg in arguments) - if (arg.ToLowerInvariant() == argument) return true; - - return false; - } - - public static List GenerateGameArguments(bool passLauncherArguments = false) - { - IEnumerable launcherArguments = Environment.GetCommandLineArgs(); - List gameArguments = new(); - - foreach (string arg in launcherArguments) - if (arg.StartsWith("cc://")) - { - string protocolArgument = arg.Replace("cc://", ""); - string[] protocolArguments = protocolArgument.Split('/'); - switch (protocolArguments[0]) - { - case "connect": - gameArguments.Add("+" + protocolArguments[0]); - gameArguments.Add(protocolArguments[1]); - break; - } - } - else if ((passLauncherArguments || !_launcherArguments.Contains(arg.ToLowerInvariant())) - && !arg.EndsWith(".exe")) - gameArguments.Add(arg.ToLowerInvariant()); - - gameArguments.AddRange(_additionalArguments); - return gameArguments; - } - } -} - +namespace Wauncher.Utils +{ + public static class Argument + { + private static readonly List _additionalArguments = new(); + + public static void AddArgument(string argument) + { + if (!_additionalArguments.Any(a => string.Equals(a, argument, StringComparison.OrdinalIgnoreCase))) + _additionalArguments.Add(argument); + } + + public static void ClearAdditionalArguments() + { + _additionalArguments.Clear(); + } + + public static bool HasProtocolCommand() => + Environment.GetCommandLineArgs().Any(arg => + arg.StartsWith("cc://", StringComparison.OrdinalIgnoreCase)); + + public static List GenerateGameArguments() + { + IEnumerable launcherArguments = Environment.GetCommandLineArgs(); + List gameArguments = new(); + + foreach (string arg in launcherArguments) + { + if (!arg.StartsWith("cc://", StringComparison.OrdinalIgnoreCase)) + continue; + + string protocolArgument = arg.Replace("cc://", "", StringComparison.OrdinalIgnoreCase); + string[] protocolArguments = protocolArgument.Split('/'); + if (protocolArguments.Length < 2) + continue; + + switch (protocolArguments[0]) + { + case "connect": + gameArguments.Add("+connect"); + gameArguments.Add(protocolArguments[1]); + break; + } + } + + gameArguments.AddRange(_additionalArguments); + return gameArguments; + } + } +} diff --git a/Wauncher/Utils/Debug.cs b/Wauncher/Utils/Debug.cs index 859b2de..e5dd059 100644 --- a/Wauncher/Utils/Debug.cs +++ b/Wauncher/Utils/Debug.cs @@ -1,8 +1,7 @@ -namespace Wauncher.Utils +namespace Wauncher.Utils { public static class Debug { - public static bool Enabled() => Argument.Exists("--debug-mode"); + public static bool Enabled() => false; } } - diff --git a/Wauncher/Utils/Game.cs b/Wauncher/Utils/Game.cs index 7d27a67..6541ca2 100644 --- a/Wauncher/Utils/Game.cs +++ b/Wauncher/Utils/Game.cs @@ -20,19 +20,17 @@ public static async Task Launch() { List arguments = Argument.GenerateGameArguments(); if (arguments.Count > 0) Terminal.Print($"Arguments: {string.Join(" ", arguments)}"); + var settings = ViewModels.SettingsWindowViewModel.LoadGlobal(); string directory = Directory.GetCurrentDirectory(); Terminal.Print($"Directory: {directory}"); string gameStatePath = $"{directory}/csgo/cfg/gamestate_integration_cc.cfg"; - if (!Argument.Exists("--disable-rpc")) + if (settings.DiscordRpc) { _port = GeneratePort(); - if (Argument.Exists("--debug-mode")) - Terminal.Debug($"Starting Game State Integration with TCP port {_port}."); - _listener = new($"http://localhost:{_port}/"); _listener.NewGameState += OnNewGameState; _listener.Start(); @@ -71,12 +69,8 @@ await File.WriteAllTextAsync(gameStatePath, else if (File.Exists(gameStatePath)) File.Delete(gameStatePath); _process = new Process(); - bool useGC = Argument.Exists("--gc"); - - if (Argument.Exists("--debug-mode")) - Terminal.Debug($"Launching the game with{(useGC ? "" : "out")} Game Coordinator..."); - string gameExe = useGC ? "cc.exe" : "csgo.exe"; + string gameExe = "csgo.exe"; _process.StartInfo.FileName = $"{directory}\\{gameExe}"; _process.StartInfo.Arguments = string.Join(" ", arguments); diff --git a/Wauncher/Utils/Patch.cs b/Wauncher/Utils/Patch.cs index 9f48833..76d8175 100644 --- a/Wauncher/Utils/Patch.cs +++ b/Wauncher/Utils/Patch.cs @@ -133,7 +133,7 @@ public static async Task ValidatePatches(bool validateAll = false, bool outdated.Add(dirPatch); needPak01Update = true; } - else if (!Argument.Exists("--validate-all")) + else if (!validateAll) { if (Debug.Enabled()) Terminal.Debug("csgo/pak01_dir.vpk is up to date - will skip pak01 files"); @@ -141,7 +141,7 @@ public static async Task ValidatePatches(bool validateAll = false, bool else { if (Debug.Enabled()) - Terminal.Debug("csgo/pak01_dir.vpk is up to date - checking all files anyway due to --validate-all"); + Terminal.Debug("csgo/pak01_dir.vpk is up to date - checking all files anyway due to full validation mode"); } } else @@ -179,7 +179,7 @@ await Parallel.ForEachAsync(patches, parallelOptions, async (patch, cancellation bool isPak01File = originalFileName.Contains("pak01_"); string path = Path.Combine(Directory.GetCurrentDirectory(), originalFileName); - if (isPak01File && !needPak01Update && !Argument.Exists("--validate-all")) + if (isPak01File && !needPak01Update && !validateAll) { if (!File.Exists(path)) { @@ -206,7 +206,7 @@ await Parallel.ForEachAsync(patches, parallelOptions, async (patch, cancellation } if (Debug.Enabled()) - Terminal.Debug($"Checking hash for: {originalFileName}{(isPak01File && Argument.Exists("--validate-all") ? " (--validate-all)" : "")}"); + Terminal.Debug($"Checking hash for: {originalFileName}{(isPak01File && validateAll ? " (full validation)" : "")}"); string hash = await GetHash(path); if (hash != patch.Hash) diff --git a/Wauncher/Utils/ProtocolManager.cs b/Wauncher/Utils/ProtocolManager.cs index eba8ed5..efad942 100644 --- a/Wauncher/Utils/ProtocolManager.cs +++ b/Wauncher/Utils/ProtocolManager.cs @@ -13,7 +13,7 @@ public static void RegisterURIHandler() EnsureKeyExists(Registry.CurrentUser, "Software/Classes/cc", "ClassicCounter"); SetValue(Registry.CurrentUser, "Software/Classes/cc", "URL Protocol", string.Empty); EnsureKeyExists(Registry.CurrentUser, "Software/Classes/cc/DefaultIcon", $"{appCurrentLocation},1"); - EnsureKeyExists(Registry.CurrentUser, "Software/Classes/cc/shell/open/command", $"\"{appCurrentLocation}\" --protocol-command \"%1\""); + EnsureKeyExists(Registry.CurrentUser, "Software/Classes/cc/shell/open/command", $"\"{appCurrentLocation}\" \"%1\""); } private static void SetValue(RegistryKey rootKey, string keys, string valueName, string value) diff --git a/Wauncher/ViewModels/SettingsWindowViewModel.cs b/Wauncher/ViewModels/SettingsWindowViewModel.cs index fe256b2..d2f10e2 100644 --- a/Wauncher/ViewModels/SettingsWindowViewModel.cs +++ b/Wauncher/ViewModels/SettingsWindowViewModel.cs @@ -16,9 +16,6 @@ public partial class SettingsWindowViewModel : ViewModelBase [ObservableProperty] private bool _skipUpdates = false; - [ObservableProperty] - private string _launchOptions = string.Empty; - public SettingsWindowViewModel() { Load(); @@ -26,7 +23,6 @@ public SettingsWindowViewModel() partial void OnMinimizeToTrayChanged(bool value) => Save(); partial void OnSkipUpdatesChanged(bool value) => Save(); - partial void OnLaunchOptionsChanged(string value) => Save(); partial void OnDiscordRpcChanged(bool value) { @@ -55,7 +51,6 @@ private void Load() case "MinimizeToTray": MinimizeToTray = value.Trim() == "true"; break; case "DiscordRpc": DiscordRpc = value.Trim() == "true"; break; case "SkipUpdates": SkipUpdates = value.Trim() == "true"; break; - case "LaunchOptions": LaunchOptions = value; break; } } } @@ -71,7 +66,6 @@ public void Save() $"MinimizeToTray={MinimizeToTray.ToString().ToLower()}", $"DiscordRpc={DiscordRpc.ToString().ToLower()}", $"SkipUpdates={SkipUpdates.ToString().ToLower()}", - $"LaunchOptions={LaunchOptions}", }); } catch { } diff --git a/Wauncher/Views/MainWindow.axaml b/Wauncher/Views/MainWindow.axaml index c4bcf1a..c2d0d61 100644 --- a/Wauncher/Views/MainWindow.axaml +++ b/Wauncher/Views/MainWindow.axaml @@ -562,12 +562,13 @@ - diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index fa6ed69..e452260 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -1,64 +1,64 @@ -using System.IO; -using System.Net.Http; -using System.Linq; -using System.Net.NetworkInformation; -using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Text.Json; -using System.Text; -using System.Text.RegularExpressions; -using Avalonia.Animation; -using Avalonia.Animation.Easings; -using Avalonia; +using System.IO; +using System.Net.Http; +using System.Linq; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Text.Json; +using System.Text; +using System.Text.RegularExpressions; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Threading; -using Wauncher.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Wauncher.ViewModels; -using Wauncher.Views; +using Avalonia.Platform; +using Avalonia.Threading; +using Wauncher.Utils; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Wauncher.ViewModels; +using Wauncher.Views; namespace Wauncher.Views { - public partial class MainWindow : Window - { - private InfoWindow? _infoWindow = null; - private SettingsWindow? _settingsWindow = null; - private SettingsWindowViewModel _settings; - private int _launchInProgress; - private int _updateInProgress; - private int _installInProgress; - - private bool _dropdownOpen = false; + public partial class MainWindow : Window + { + private InfoWindow? _infoWindow = null; + private SettingsWindow? _settingsWindow = null; + private SettingsWindowViewModel _settings; + private int _launchInProgress; + private int _updateInProgress; + private int _installInProgress; + + private bool _dropdownOpen = false; private const double HeightClosed = 720; private const double HeightOpen = 720; // ── Image carousel (center content area) ────────────────────────────────── - private Image[] _carouselImages = Array.Empty(); - private DispatcherTimer? _carouselTimer; - private int _currentCarouselIndex = 0; - private const int CarouselRotationIntervalSeconds = 5; - private readonly List _zoomCts = new(); - private static string WauncherDirectory => - Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); + private Image[] _carouselImages = Array.Empty(); + private DispatcherTimer? _carouselTimer; + private int _currentCarouselIndex = 0; + private const int CarouselRotationIntervalSeconds = 5; + private readonly List _zoomCts = new(); + private static string WauncherDirectory => + Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); public MainWindow() { InitializeComponent(); _settings = SettingsWindowViewModel.LoadGlobal(); - this.Loaded += (_, _) => - { - var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = buttonColor; - ArrowButton.Background = buttonColor; - LaunchUpdateButton.IsEnabled = true; - }; + this.Loaded += (_, _) => + { + var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = buttonColor; + ArrowButton.Background = buttonColor; + LaunchUpdateButton.IsEnabled = true; + }; this.Opened += (_, _) => { @@ -93,75 +93,72 @@ public MainWindow() this.Closed += (_, _) => TeardownCarousel(); } - // ── Image carousel (center content area) ────────────────────────────────── - private static readonly HttpClient _http = new(); - private static string PatchNotesCachePath => - Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "cache", - "patchnotes.md"); - - private async Task SetupCarouselAsync() - { - try - { - TeardownCarousel(); - - var carouselContainer = this.FindControl("CarouselContainer"); - var offlinePanel = this.FindControl("CarouselOfflinePanel"); - var offlineSubText = this.FindControl("CarouselOfflineSubText"); - if (carouselContainer == null) - return; - - bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); - var bitmaps = hasInternet - ? await LoadCarouselFromGitHubAsync() - : null; - - if (bitmaps == null || bitmaps.Count == 0) - bitmaps = LoadEmbeddedCarouselImages(); - - if (bitmaps.Count == 0) - { - if (offlinePanel != null) - offlinePanel.IsVisible = true; - if (offlineSubText != null) - { - offlineSubText.Text = hasInternet - ? "Carousel is temporarily unavailable." - : "Connect to Wi-Fi or Ethernet to load the carousel."; - } - return; - } - - if (offlinePanel != null) - offlinePanel.IsVisible = false; - - _carouselImages = CreateCarouselImages(bitmaps); - EnsureZoomSlots(_carouselImages.Length); - - foreach (var existingImage in carouselContainer.Children.OfType().ToList()) - carouselContainer.Children.Remove(existingImage); - - int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; - for (int i = 0; i < _carouselImages.Length; i++) - { - if (overlayIndex >= 0) - { - carouselContainer.Children.Insert(overlayIndex, _carouselImages[i]); - overlayIndex++; - } - else - { - carouselContainer.Children.Add(_carouselImages[i]); - } - } - - _currentCarouselIndex = 0; - _carouselImages[0].Opacity = 1.0; - StartZoomOut(_carouselImages[0], 0); + // ── Image carousel (center content area) ────────────────────────────────── + private static readonly HttpClient _http = new(); + private static string PatchNotesCachePath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache", + "patchnotes.md"); + + private async Task SetupCarouselAsync() + { + try + { + TeardownCarousel(); + + var carouselContainer = this.FindControl("CarouselContainer"); + var offlinePanel = this.FindControl("CarouselOfflinePanel"); + var offlineSubText = this.FindControl("CarouselOfflineSubText"); + if (carouselContainer == null) + return; + + bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); + var bitmaps = hasInternet + ? await LoadCarouselFromGitHubAsync() + : null; + + if (bitmaps == null || bitmaps.Count == 0) + { + if (offlinePanel != null) + offlinePanel.IsVisible = true; + if (offlineSubText != null) + { + offlineSubText.Text = hasInternet + ? "Carousel is temporarily unavailable." + : "Connect to Wi-Fi or Ethernet to load the carousel."; + } + return; + } + + if (offlinePanel != null) + offlinePanel.IsVisible = false; + + _carouselImages = CreateCarouselImages(bitmaps); + EnsureZoomSlots(_carouselImages.Length); + + foreach (var existingImage in carouselContainer.Children.OfType().ToList()) + carouselContainer.Children.Remove(existingImage); + + int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; + for (int i = 0; i < _carouselImages.Length; i++) + { + if (overlayIndex >= 0) + { + carouselContainer.Children.Insert(overlayIndex, _carouselImages[i]); + overlayIndex++; + } + else + { + carouselContainer.Children.Add(_carouselImages[i]); + } + } + + _currentCarouselIndex = 0; + _carouselImages[0].Opacity = 1.0; + StartZoomOut(_carouselImages[0], 0); _carouselTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CarouselRotationIntervalSeconds) }; _carouselTimer.Tick += (_, _) => RotateCarousel(); @@ -173,35 +170,35 @@ private async Task SetupCarouselAsync() } } - private async Task?> LoadCarouselFromGitHubAsync() - { - try - { - var json = await Api.GitHub.GetCarouselAssetsWauncher(); - var assets = JsonConvert.DeserializeObject>(json); - if (assets == null || assets.Count == 0) - return null; - - var urls = assets - .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) - .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) - .OrderBy(a => GetCarouselSortIndex(a.Name)) - .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) - .Select(a => a.DownloadUrl!) - .ToList(); - - if (urls.Count == 0) - return null; - - var bitmaps = new List(); - foreach (var url in urls) - { - try + private async Task?> LoadCarouselFromGitHubAsync() + { + try + { + var json = await Api.GitHub.GetCarouselAssetsWauncher(); + var assets = JsonConvert.DeserializeObject>(json); + if (assets == null || assets.Count == 0) + return null; + + var urls = assets + .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) + .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) + .OrderBy(a => GetCarouselSortIndex(a.Name)) + .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) + .Select(a => a.DownloadUrl!) + .ToList(); + + if (urls.Count == 0) + return null; + + var bitmaps = new List(); + foreach (var url in urls) + { + try { var bytes = await _http.GetByteArrayAsync(url); using var ms = new MemoryStream(bytes); @@ -210,101 +207,84 @@ private async Task SetupCarouselAsync() catch { } } return bitmaps.Count > 0 ? bitmaps : null; - } - catch { return null; } - } - - private static int GetCarouselSortIndex(string name) - { - var match = Regex.Match(name, @"^carousel_(\d+)", RegexOptions.IgnoreCase); - if (match.Success && int.TryParse(match.Groups[1].Value, out var index)) - return index; - return int.MaxValue; - } - - private sealed class GitHubAssetEntry - { - [JsonProperty("name")] - public string Name { get; set; } = string.Empty; - - [JsonProperty("type")] - public string Type { get; set; } = string.Empty; - - [JsonProperty("download_url")] - public string? DownloadUrl { get; set; } - } - - private static Image[] CreateCarouselImages(IReadOnlyList bitmaps) - { - var images = new Image[bitmaps.Count]; - for (int i = 0; i < bitmaps.Count; i++) - { - images[i] = new Image - { - Source = bitmaps[i], - Stretch = Stretch.UniformToFill, - Opacity = 0.0, - Transitions = new Transitions - { - new DoubleTransition - { - Property = Visual.OpacityProperty, - Duration = TimeSpan.FromSeconds(1.5), - Easing = new CubicEaseInOut() - } - } - }; - } - - return images; - } - - private void EnsureZoomSlots(int count) - { - while (_zoomCts.Count < count) - _zoomCts.Add(null); - } - - private static List LoadEmbeddedCarouselImages() - { - var bitmaps = new List(); - string[] files = { "carousel_0.png", "carousel_1.png", "carousel_2.png", "carousel_3.png" }; - foreach (var file in files) - { - try - { - var uri = new Uri("avares://Wauncher/Assets/" + file); - using var stream = AssetLoader.Open(uri); - bitmaps.Add(new Bitmap(stream)); - } - catch { } - } - return bitmaps; - } - - private void RotateCarousel() - { - if (_carouselImages.Length == 0) - return; - - // Fade out current image (zoom continues through the crossfade) - _carouselImages[_currentCarouselIndex].Opacity = 0.0; - - // Move to next image - _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; - - // Fade in next image and start fresh zoom-out - StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); - _carouselImages[_currentCarouselIndex].Opacity = 1.0; - } - - private void TeardownCarousel() - { - _carouselTimer?.Stop(); - _carouselTimer = null; - for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); - _carouselImages = Array.Empty(); - } + } + catch { return null; } + } + + private static int GetCarouselSortIndex(string name) + { + var match = Regex.Match(name, @"^carousel_(\d+)", RegexOptions.IgnoreCase); + if (match.Success && int.TryParse(match.Groups[1].Value, out var index)) + return index; + return int.MaxValue; + } + + private sealed class GitHubAssetEntry + { + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("type")] + public string Type { get; set; } = string.Empty; + + [JsonProperty("download_url")] + public string? DownloadUrl { get; set; } + } + + private static Image[] CreateCarouselImages(IReadOnlyList bitmaps) + { + var images = new Image[bitmaps.Count]; + for (int i = 0; i < bitmaps.Count; i++) + { + images[i] = new Image + { + Source = bitmaps[i], + Stretch = Stretch.UniformToFill, + Opacity = 0.0, + Transitions = new Transitions + { + new DoubleTransition + { + Property = Visual.OpacityProperty, + Duration = TimeSpan.FromSeconds(1.5), + Easing = new CubicEaseInOut() + } + } + }; + } + + return images; + } + + private void EnsureZoomSlots(int count) + { + while (_zoomCts.Count < count) + _zoomCts.Add(null); + } + + private void RotateCarousel() + { + if (_carouselImages.Length == 0) + return; + + // Fade out current image (zoom continues through the crossfade) + _carouselImages[_currentCarouselIndex].Opacity = 0.0; + + // Move to next image + _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; + + // Fade in next image and start fresh zoom-out + StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); + _carouselImages[_currentCarouselIndex].Opacity = 1.0; + } + + private void TeardownCarousel() + { + _carouselTimer?.Stop(); + _carouselTimer = null; + for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); + _carouselImages = Array.Empty(); + } private void StartZoomOut(Image img, int slot) { @@ -334,28 +314,28 @@ private void StartZoomOut(Image img, int slot) zoomTimer.Start(); } - private void StopZoom(int slot) - { - if (slot < 0 || slot >= _zoomCts.Count) - return; - - _zoomCts[slot]?.Cancel(); - _zoomCts[slot] = null; - } + private void StopZoom(int slot) + { + if (slot < 0 || slot >= _zoomCts.Count) + return; + + _zoomCts[slot]?.Cancel(); + _zoomCts[slot] = null; + } // ── Server dropdown ─────────────────────────────────────────── - private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) - { - CloseDropdown(); - return; - } - - _dropdownOpen = !_dropdownOpen; - - if (DataContext is MainWindowViewModel vm) - vm.IsDropdownOpen = _dropdownOpen; + private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) + { + CloseDropdown(); + return; + } + + _dropdownOpen = !_dropdownOpen; + + if (DataContext is MainWindowViewModel vm) + vm.IsDropdownOpen = _dropdownOpen; if (_dropdownOpen) { @@ -387,56 +367,49 @@ private void CloseDropdown() } // ── Game launch ─────────────────────────────────────────── - private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - - if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || Volatile.Read(ref _launchInProgress) == 1) - return; - else if (vm.IsNeedingInstall) - _ = InstallGameFromCdnAsync(); - else if (_settings.SkipUpdates) - _ = LaunchGameAsync(); - else if (vm.UpdateAvailable) - { - if (_selfUpdateAvailable) - _ = Button_SelfUpdateAsync(); - else - Button_Update(sender, e); - } - else - _ = LaunchGameAsync(); - } - - private async Task LaunchGameAsync() - { - if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) - return; - - var vm = DataContext as MainWindowViewModel; - try - { - _settings = SettingsWindowViewModel.LoadGlobal(); - + private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || Volatile.Read(ref _launchInProgress) == 1) + return; + else if (vm.IsNeedingInstall) + _ = InstallGameFromCdnAsync(); + else if (_settings.SkipUpdates) + _ = LaunchGameAsync(); + else if (vm.UpdateAvailable) + { + if (_selfUpdateAvailable) + _ = Button_SelfUpdateAsync(); + else + Button_Update(sender, e); + } + else + _ = LaunchGameAsync(); + } + + private async Task LaunchGameAsync() + { + if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) + return; + + var vm = DataContext as MainWindowViewModel; + try + { + _settings = SettingsWindowViewModel.LoadGlobal(); + if (vm != null) vm.GameStatus = "Running"; - // Clear any arguments left over from a previous launch before adding new ones. - Argument.ClearAdditionalArguments(); - - Argument.AddArgument("-novid"); - - if (!_settings.DiscordRpc) - Argument.AddArgument("--disable-rpc"); - - if (!string.IsNullOrWhiteSpace(_settings.LaunchOptions)) - foreach (var arg in ParseLaunchOptions(_settings.LaunchOptions)) - Argument.AddArgument(arg); - - var selected = vm?.SelectedServer; - if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) - { + // Clear any arguments left over from a previous launch before adding new ones. + Argument.ClearAdditionalArguments(); + + Argument.AddArgument("-novid"); + + var selected = vm?.SelectedServer; + if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) + { Argument.AddArgument("+connect"); Argument.AddArgument(selected.IpPort); } @@ -454,16 +427,16 @@ private async Task LaunchGameAsync() await Game.Monitor(); } - catch (Exception ex) - { - Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); - } - finally - { - if (vm != null) vm.GameStatus = "Not Running"; - Interlocked.Exchange(ref _launchInProgress, 0); - } - } + catch (Exception ex) + { + Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); + } + finally + { + if (vm != null) vm.GameStatus = "Not Running"; + Interlocked.Exchange(ref _launchInProgress, 0); + } + } // ── Window chrome ─────────────────────────────────────────── private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) @@ -493,201 +466,215 @@ public void ForceQuit() Close(); } - private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var dir = WauncherDirectory; - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = dir, - UseShellExecute = true - }); - } - - // ── Update ───────────────────────────────────────────────────── - private CancellationTokenSource? _updateCts; - private Patches? _cachedPatches; - private bool _selfUpdateAvailable; - private string _selfUpdateDownloadUrl = string.Empty; - private string _selfUpdateVersion = string.Empty; - private int _autoSelfUpdateTriggered; + private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var dir = WauncherDirectory; + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = dir, + UseShellExecute = true + }); + } + + private void VerifyGameFiles_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is not MainWindowViewModel vm) + return; + + if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || vm.IsNeedingInstall) + return; + + _forceValidateAllOnce = true; + _cachedPatches = null; + Button_Update(sender, e); + } + + // ── Update ───────────────────────────────────────────────────── + private CancellationTokenSource? _updateCts; + private Patches? _cachedPatches; + private bool _selfUpdateAvailable; + private string _selfUpdateDownloadUrl = string.Empty; + private string _selfUpdateVersion = string.Empty; + private int _autoSelfUpdateTriggered; + private bool _forceValidateAllOnce; /// /// Called on window open. If csgo.exe is missing, triggers a full CDN install. /// Otherwise runs the normal patch update check. /// - private async Task StartupAsync() - { - // Yield to let Avalonia finish its initial layout/styling pass - // (Loaded sets the button disabled/gray; we need that to settle before overriding) - await Task.Delay(50); - - LaunchUpdateButton.IsEnabled = true; - - string csgoExe = Path.Combine(WauncherDirectory, "csgo.exe"); - if (DataContext is not MainWindowViewModel vm) - return; - - if (!File.Exists(csgoExe)) - { - vm.IsNeedingInstall = true; - var blue = new SolidColorBrush(Color.Parse("#2196F3")); - LaunchUpdateButton.Background = blue; - ArrowButton.Background = blue; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - if (vm?.IsOfflineMode == true) - { - vm.IsNeedingInstall = false; - vm.UpdateAvailable = false; - var green = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = green; - ArrowButton.Background = green; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - _settings = SettingsWindowViewModel.LoadGlobal(); - if (_settings.SkipUpdates) - { - vm!.IsNeedingInstall = false; - vm.UpdateAvailable = false; - vm.IsCheckingUpdates = false; - var green = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = green; - ArrowButton.Background = green; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - await CheckForUpdatesAsync(); - } - - private async Task InstallGameFromCdnAsync() - { - if (DataContext is not MainWindowViewModel vm) return; - if (Interlocked.Exchange(ref _installInProgress, 1) == 1) - return; - - bool installSucceeded = false; - vm.IsNeedingInstall = false; - vm.IsInstalling = true; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = "Connecting..."; + private async Task StartupAsync() + { + // Yield to let Avalonia finish its initial layout/styling pass + // (Loaded sets the button disabled/gray; we need that to settle before overriding) + await Task.Delay(50); + + LaunchUpdateButton.IsEnabled = true; + + string csgoExe = Path.Combine(WauncherDirectory, "csgo.exe"); + if (DataContext is not MainWindowViewModel vm) + return; + + if (!File.Exists(csgoExe)) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + if (vm?.IsOfflineMode == true) + { + vm.IsNeedingInstall = false; + vm.UpdateAvailable = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + _settings = SettingsWindowViewModel.LoadGlobal(); + if (_settings.SkipUpdates) + { + vm!.IsNeedingInstall = false; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + await CheckForUpdatesAsync(); + } + + private async Task InstallGameFromCdnAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + if (Interlocked.Exchange(ref _installInProgress, 1) == 1) + return; + + bool installSucceeded = false; + vm.IsNeedingInstall = false; + vm.IsInstalling = true; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Connecting..."; vm.UpdateStatusSpeed = ""; - try - { - await DownloadManager.InstallFullGame( - onProgress: (file, speed, percent) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = $"Installing {ShortFileName(file)} {percent:F0}%"; - vm.UpdateStatusSpeed = string.IsNullOrWhiteSpace(speed) ? "" : speed; - vm.UpdateProgress = percent; - vm.UpdateIndeterminate = false; - }); - }, - onStatus: status => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = status; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); - if (!vm.UpdateIndeterminate) - vm.UpdateProgress = 0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = extractPercent; - }); - }); - - // Immediately apply any post-install patches so first-time installs - // end in a launch-ready state without requiring a second manual update. - Dispatcher.UIThread.Post(() => - { - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = "Checking post-install patches..."; - vm.UpdateStatusSpeed = ""; - }); - - var patches = await Task.Run(() => PatchManager.ValidatePatches()); - var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); - if (allPatches.Count > 0) - { - int totalFiles = allPatches.Count; - int completed = 0; - - foreach (var patch in allPatches) - { - var extractWatch = new System.Diagnostics.Stopwatch(); - await DownloadManager.DownloadPatch( - patch, - onProgress: (p) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; - vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); - vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; - }); - }, - onExtract: () => - { - extractWatch.Restart(); - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); - vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; - }); - }); - - completed++; - vm.UpdateProgress = (double)completed / totalFiles * 100.0; - } - } - - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = "Game installed and fully updated!"; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = false; - vm.UpdateProgress = 100; - }); - installSucceeded = true; - await Task.Delay(1500); - } - catch (Exception ex) - { - vm.UpdateStatusFile = $"Install error: {ex.Message}"; + try + { + await DownloadManager.InstallFullGame( + onProgress: (file, speed, percent) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = $"Installing {ShortFileName(file)} {percent:F0}%"; + vm.UpdateStatusSpeed = string.IsNullOrWhiteSpace(speed) ? "" : speed; + vm.UpdateProgress = percent; + vm.UpdateIndeterminate = false; + }); + }, + onStatus: status => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = status; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); + if (!vm.UpdateIndeterminate) + vm.UpdateProgress = 0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = extractPercent; + }); + }); + + // Immediately apply any post-install patches so first-time installs + // end in a launch-ready state without requiring a second manual update. + Dispatcher.UIThread.Post(() => + { + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Checking post-install patches..."; + vm.UpdateStatusSpeed = ""; + }); + + var patches = await Task.Run(() => PatchManager.ValidatePatches()); + var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); + if (allPatches.Count > 0) + { + int totalFiles = allPatches.Count; + int completed = 0; + + foreach (var patch in allPatches) + { + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); + + completed++; + vm.UpdateProgress = (double)completed / totalFiles * 100.0; + } + } + + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = "Game installed and fully updated!"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + vm.UpdateProgress = 100; + }); + installSucceeded = true; + await Task.Delay(1500); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Install error: {ex.Message}"; vm.UpdateStatusSpeed = ""; await Task.Delay(4000); } @@ -695,196 +682,196 @@ await DownloadManager.DownloadPatch( { vm.IsInstalling = false; vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - - _cachedPatches = null; - - if (!installSucceeded && !File.Exists(Path.Combine(WauncherDirectory, "csgo.exe"))) - { - vm.IsNeedingInstall = true; - var blue = new SolidColorBrush(Color.Parse("#2196F3")); - LaunchUpdateButton.Background = blue; - ArrowButton.Background = blue; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); - } - else - { - try - { - await CheckForUpdatesAsync(); - } - catch { } - } - - LaunchUpdateButton.IsEnabled = true; - Interlocked.Exchange(ref _installInProgress, 0); - } - } - - private async Task LoadPatchNotesAsync() - { - try - { - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Loading latest patch notes..."; - PatchNotesVersion.IsVisible = true; - }); - - if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) - { - Dispatcher.UIThread.Post(() => - { - var cachedItems = LoadCachedPatchNotes(); - if (cachedItems.Count > 0) - { - PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; - PatchNotesList.ItemsSource = cachedItems; - } - else - { - PatchNotesVersion.Text = "Patch notes are unavailable offline."; - PatchNotesList.ItemsSource = new List(); - } - - PatchNotesVersion.IsVisible = true; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - return; - } - - var md = await Api.GitHub.GetPatchNotesWauncher(); - var items = ParsePatchNotes(md); - SavePatchNotesCache(md); - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = items.Count > 0 - ? $"Updated {DateTime.Now:MMM d, h:mm tt}" - : "Patch notes are currently empty."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - } - catch - { - var items = LoadCachedPatchNotes(); - if (items.Count == 0) - { - items = BuildFallbackPatchNotes(); - } - - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Using fallback patch notes."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - } - } - - private static void SavePatchNotesCache(string markdown) - { - try - { - var directory = Path.GetDirectoryName(PatchNotesCachePath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(PatchNotesCachePath, markdown); - } - catch - { - // Caching is best-effort; keep patch notes functional if disk write fails. - } - } - - private static List LoadCachedPatchNotes() - { - try - { - if (!File.Exists(PatchNotesCachePath)) - { - return new List(); - } - - var markdown = File.ReadAllText(PatchNotesCachePath); - return ParsePatchNotes(markdown); - } - catch - { - return new List(); - } - } - - private static List BuildFallbackPatchNotes() - { - return new List - { - new() { Text = "Anniversary Update", IsMajorHeader = true }, - new() { Text = "What's Changed", IsHeader = true }, - new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, - new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, - new() { Text = "Bug fixes and security improvements.", IsBullet = true }, - }; - } - - private static List ParsePatchNotes(string markdown) - { - var items = new List(); - foreach (var raw in markdown.Split('\n')) - { - var line = raw.TrimEnd(); - if (string.IsNullOrWhiteSpace(line)) continue; - - line = line.Trim(); - line = line.Replace("**", "").Replace("__", ""); - line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); - line = Regex.Replace(line, @"`([^`]*)`", "$1"); - - if (line.StartsWith("# ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsMajorHeader = true - }); - } - else if (line.StartsWith("## ") || line.StartsWith("### ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsHeader = true - }); - } - else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Substring(2).Trim(), - IsBullet = true + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + + _cachedPatches = null; + + if (!installSucceeded && !File.Exists(Path.Combine(WauncherDirectory, "csgo.exe"))) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + } + else + { + try + { + await CheckForUpdatesAsync(); + } + catch { } + } + + LaunchUpdateButton.IsEnabled = true; + Interlocked.Exchange(ref _installInProgress, 0); + } + } + + private async Task LoadPatchNotesAsync() + { + try + { + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Loading latest patch notes..."; + PatchNotesVersion.IsVisible = true; + }); + + if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) + { + Dispatcher.UIThread.Post(() => + { + var cachedItems = LoadCachedPatchNotes(); + if (cachedItems.Count > 0) + { + PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; + PatchNotesList.ItemsSource = cachedItems; + } + else + { + PatchNotesVersion.Text = "Patch notes are unavailable offline."; + PatchNotesList.ItemsSource = new List(); + } + + PatchNotesVersion.IsVisible = true; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + return; + } + + var md = await Api.GitHub.GetPatchNotesWauncher(); + var items = ParsePatchNotes(md); + SavePatchNotesCache(md); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = items.Count > 0 + ? $"Updated {DateTime.Now:MMM d, h:mm tt}" + : "Patch notes are currently empty."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + catch + { + var items = LoadCachedPatchNotes(); + if (items.Count == 0) + { + items = BuildFallbackPatchNotes(); + } + + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Using fallback patch notes."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + } + + private static void SavePatchNotesCache(string markdown) + { + try + { + var directory = Path.GetDirectoryName(PatchNotesCachePath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(PatchNotesCachePath, markdown); + } + catch + { + // Caching is best-effort; keep patch notes functional if disk write fails. + } + } + + private static List LoadCachedPatchNotes() + { + try + { + if (!File.Exists(PatchNotesCachePath)) + { + return new List(); + } + + var markdown = File.ReadAllText(PatchNotesCachePath); + return ParsePatchNotes(markdown); + } + catch + { + return new List(); + } + } + + private static List BuildFallbackPatchNotes() + { + return new List + { + new() { Text = "Anniversary Update", IsMajorHeader = true }, + new() { Text = "What's Changed", IsHeader = true }, + new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, + new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, + new() { Text = "Bug fixes and security improvements.", IsBullet = true }, + }; + } + + private static List ParsePatchNotes(string markdown) + { + var items = new List(); + foreach (var raw in markdown.Split('\n')) + { + var line = raw.TrimEnd(); + if (string.IsNullOrWhiteSpace(line)) continue; + + line = line.Trim(); + line = line.Replace("**", "").Replace("__", ""); + line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); + line = Regex.Replace(line, @"`([^`]*)`", "$1"); + + if (line.StartsWith("# ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsMajorHeader = true + }); + } + else if (line.StartsWith("## ") || line.StartsWith("### ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsHeader = true }); } - else if (Regex.IsMatch(line, @"^\d+\.\s+")) - { - var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); - items.Add(new ViewModels.PatchNoteItem - { - Text = bulletText, - IsBullet = true - }); - } - else if (line.StartsWith("**") && line.EndsWith("**")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Trim('*', ' '), + else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Substring(2).Trim(), + IsBullet = true + }); + } + else if (Regex.IsMatch(line, @"^\d+\.\s+")) + { + var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); + items.Add(new ViewModels.PatchNoteItem + { + Text = bulletText, + IsBullet = true + }); + } + else if (line.StartsWith("**") && line.EndsWith("**")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Trim('*', ' '), IsHeader = true }); } @@ -897,329 +884,329 @@ private static void SavePatchNotesCache(string markdown) }); } } - return items; - } - - private sealed class GitHubRelease - { - [JsonProperty("tag_name")] - public string? TagName { get; set; } - - [JsonProperty("assets")] - public List? Assets { get; set; } - } - - private sealed class GitHubReleaseAsset - { - [JsonProperty("name")] - public string Name { get; set; } = string.Empty; - - [JsonProperty("browser_download_url")] - public string DownloadUrl { get; set; } = string.Empty; - } - - private static string NormalizeVersionToken(string? version) - { - if (string.IsNullOrWhiteSpace(version)) - return "0.0.0"; - - var cleaned = version.Trim(); - if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase)) - cleaned = cleaned[1..]; - - cleaned = Regex.Replace(cleaned, @"[^0-9\.]", string.Empty); - return string.IsNullOrWhiteSpace(cleaned) ? "0.0.0" : cleaned; - } - - private static bool TryParseVersion(string value, out global::System.Version parsed) - { - if (global::System.Version.TryParse(value, out parsed!)) - return true; - - var tokens = value.Split('.', StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length == 0) - { - parsed = new global::System.Version(0, 0, 0); - return false; - } - - while (tokens.Length < 3) - tokens = tokens.Append("0").ToArray(); - - return global::System.Version.TryParse(string.Join('.', tokens.Take(4)), out parsed!); - } - - private async Task CheckForSelfUpdateAsync() - { - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - - try - { - var latestReleaseJson = await Api.GitHub.GetLatestRelease(); - var release = JsonConvert.DeserializeObject(latestReleaseJson); - if (release == null) - return false; - - var currentVersion = NormalizeVersionToken(Wauncher.Utils.Version.Current); - var latestVersion = NormalizeVersionToken(release.TagName); - if (!TryParseVersion(currentVersion, out var current) || !TryParseVersion(latestVersion, out var latest)) - return false; - - if (latest <= current) - return false; - - var assets = release.Assets ?? new List(); - var preferred = assets.FirstOrDefault(a => - !string.IsNullOrWhiteSpace(a.DownloadUrl) && - string.Equals(a.Name, "wauncher.exe", StringComparison.OrdinalIgnoreCase)); - - preferred ??= assets.FirstOrDefault(a => - !string.IsNullOrWhiteSpace(a.DownloadUrl) && - a.Name.Contains("wauncher", StringComparison.OrdinalIgnoreCase) && - a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); - - if (preferred == null) - return false; - - _selfUpdateAvailable = true; - _selfUpdateDownloadUrl = preferred.DownloadUrl; - _selfUpdateVersion = latestVersion; - return true; - } - catch - { - return false; - } - } - - private async Task DownloadFileWithProgressAsync(string url, string destination, Action? onProgress, CancellationToken token) - { - using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); - response.EnsureSuccessStatusCode(); - - var totalBytes = response.Content.Headers.ContentLength; - await using var input = await response.Content.ReadAsStreamAsync(token); - await using var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true); - - var buffer = new byte[81920]; - long received = 0; - while (true) - { - int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), token); - if (read == 0) - break; - - await output.WriteAsync(buffer.AsMemory(0, read), token); - received += read; - if (totalBytes.HasValue && totalBytes.Value > 0) - { - onProgress?.Invoke((double)received / totalBytes.Value * 100.0); - } - } - - onProgress?.Invoke(100.0); - } - - private static string BuildSelfUpdateScript(string stagedExePath, string currentExePath) - { - return -$@"@echo off -setlocal -set ""SRC={stagedExePath}"" -set ""DST={currentExePath}"" - -for /L %%i in (1,1,60) do ( - copy /Y ""%SRC%"" ""%DST%"" >nul 2>nul && goto copied - timeout /t 1 /nobreak >nul -) - -exit /b 1 - -:copied -start """" ""%DST%"" -del /Q ""%SRC%"" >nul 2>nul -del /Q ""%~f0"" >nul 2>nul -exit /b 0 -"; - } - - private async Task Button_SelfUpdateAsync() - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) - return; - - if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) - return; - - _updateCts?.Dispose(); - _updateCts = new CancellationTokenSource(); - var token = _updateCts.Token; - - vm.IsUpdating = true; - vm.UpdateAvailable = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = "Downloading Wauncher update..."; - vm.UpdateStatusSpeed = ""; - - try - { - if (string.IsNullOrWhiteSpace(_selfUpdateDownloadUrl)) - throw new Exception("No self-update package URL found."); - - var updatesDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "self-update"); - Directory.CreateDirectory(updatesDir); - - var safeVersion = Regex.Replace(_selfUpdateVersion, @"[^0-9A-Za-z\.\-_]", string.Empty); - if (string.IsNullOrWhiteSpace(safeVersion)) - safeVersion = "latest"; - - var stagedExePath = Path.Combine(updatesDir, $"wauncher_{safeVersion}.exe"); - await DownloadFileWithProgressAsync(_selfUpdateDownloadUrl, stagedExePath, percent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateProgress = percent; - vm.UpdateStatusFile = $"Downloading Wauncher update... {percent:F0}%"; - }); - }, token); - - var currentExePath = Services.GetExePath(); - if (string.IsNullOrWhiteSpace(currentExePath)) - throw new Exception("Could not locate current Wauncher executable."); - - var scriptPath = Path.Combine(updatesDir, "apply_wauncher_update.cmd"); - var script = BuildSelfUpdateScript(stagedExePath, currentExePath); - File.WriteAllText(scriptPath, script, Encoding.ASCII); - - Process.Start(new ProcessStartInfo - { - FileName = "cmd.exe", - Arguments = $"/c \"{scriptPath}\"", - WorkingDirectory = updatesDir, - CreateNoWindow = true, - UseShellExecute = false, - }); - - vm.UpdateStatusFile = "Restarting Wauncher to apply update..."; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = 100; - await Task.Delay(500, token); - ForceQuit(); - } - catch (OperationCanceledException) - { - vm.UpdateStatusFile = "Update cancelled."; - vm.UpdateStatusSpeed = ""; - await Task.Delay(800); - } - catch (Exception ex) - { - vm.UpdateStatusFile = $"Self-update failed: {ex.Message}"; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = false; - await Task.Delay(2500); - } - finally - { - if (!_forceClose) - { - vm.IsUpdating = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - _updateCts?.Dispose(); - _updateCts = null; - - try - { - await CheckForUpdatesAsync(); - } - catch - { - // keep UI responsive even if refresh fails - } - } - - Interlocked.Exchange(ref _updateInProgress, 0); - } - } - - private async Task CheckForUpdatesAsync() - { - if (DataContext is not MainWindowViewModel vm) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - - if (vm.IsOfflineMode) - { - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - _cachedPatches = null; - vm.UpdateAvailable = false; - vm.IsCheckingUpdates = false; - var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = launchColor; - ArrowButton.Background = launchColor; - LaunchUpdateButton.IsEnabled = true; - return; - } - - vm.IsCheckingUpdates = true; - LaunchUpdateButton.Background = new SolidColorBrush(Color.Parse("#555555")); - ArrowButton.Background = new SolidColorBrush(Color.Parse("#555555")); - LaunchUpdateButton.IsEnabled = false; - try - { - bool hasSelfUpdate = await CheckForSelfUpdateAsync(); - if (hasSelfUpdate) - { - _cachedPatches = null; - vm.UpdateAvailable = true; - var selfUpdateColor = new SolidColorBrush(Color.Parse("#FFC107")); - LaunchUpdateButton.Background = selfUpdateColor; - ArrowButton.Background = selfUpdateColor; - - if (Interlocked.Exchange(ref _autoSelfUpdateTriggered, 1) == 0) - { - _ = Button_SelfUpdateAsync(); - } - - return; - } - - if (_settings.SkipUpdates) - { - _cachedPatches = null; - vm.UpdateAvailable = false; - var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = launchColor; - ArrowButton.Background = launchColor; - return; - } - - var patches = await Task.Run(() => PatchManager.ValidatePatches(deleteOutdatedFiles: false)); - bool hasUpdates = patches.Missing.Count > 0 || patches.Outdated.Count > 0; - - // Cache the result so Button_Update can consume it without re-validating. - _cachedPatches = patches; - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - vm.UpdateAvailable = hasUpdates; - var buttonColor = new SolidColorBrush( - Color.Parse(hasUpdates ? "#FFC107" : "#4CAF50")); + return items; + } + + private sealed class GitHubRelease + { + [JsonProperty("tag_name")] + public string? TagName { get; set; } + + [JsonProperty("assets")] + public List? Assets { get; set; } + } + + private sealed class GitHubReleaseAsset + { + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("browser_download_url")] + public string DownloadUrl { get; set; } = string.Empty; + } + + private static string NormalizeVersionToken(string? version) + { + if (string.IsNullOrWhiteSpace(version)) + return "0.0.0"; + + var cleaned = version.Trim(); + if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + cleaned = cleaned[1..]; + + cleaned = Regex.Replace(cleaned, @"[^0-9\.]", string.Empty); + return string.IsNullOrWhiteSpace(cleaned) ? "0.0.0" : cleaned; + } + + private static bool TryParseVersion(string value, out global::System.Version parsed) + { + if (global::System.Version.TryParse(value, out parsed!)) + return true; + + var tokens = value.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 0) + { + parsed = new global::System.Version(0, 0, 0); + return false; + } + + while (tokens.Length < 3) + tokens = tokens.Append("0").ToArray(); + + return global::System.Version.TryParse(string.Join('.', tokens.Take(4)), out parsed!); + } + + private async Task CheckForSelfUpdateAsync() + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + + try + { + var latestReleaseJson = await Api.GitHub.GetLatestRelease(); + var release = JsonConvert.DeserializeObject(latestReleaseJson); + if (release == null) + return false; + + var currentVersion = NormalizeVersionToken(Wauncher.Utils.Version.Current); + var latestVersion = NormalizeVersionToken(release.TagName); + if (!TryParseVersion(currentVersion, out var current) || !TryParseVersion(latestVersion, out var latest)) + return false; + + if (latest <= current) + return false; + + var assets = release.Assets ?? new List(); + var preferred = assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + string.Equals(a.Name, "wauncher.exe", StringComparison.OrdinalIgnoreCase)); + + preferred ??= assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + a.Name.Contains("wauncher", StringComparison.OrdinalIgnoreCase) && + a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); + + if (preferred == null) + return false; + + _selfUpdateAvailable = true; + _selfUpdateDownloadUrl = preferred.DownloadUrl; + _selfUpdateVersion = latestVersion; + return true; + } + catch + { + return false; + } + } + + private async Task DownloadFileWithProgressAsync(string url, string destination, Action? onProgress, CancellationToken token) + { + using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); + response.EnsureSuccessStatusCode(); + + var totalBytes = response.Content.Headers.ContentLength; + await using var input = await response.Content.ReadAsStreamAsync(token); + await using var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true); + + var buffer = new byte[81920]; + long received = 0; + while (true) + { + int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), token); + if (read == 0) + break; + + await output.WriteAsync(buffer.AsMemory(0, read), token); + received += read; + if (totalBytes.HasValue && totalBytes.Value > 0) + { + onProgress?.Invoke((double)received / totalBytes.Value * 100.0); + } + } + + onProgress?.Invoke(100.0); + } + + private static string BuildSelfUpdateScript(string stagedExePath, string currentExePath) + { + return +$@"@echo off +setlocal +set ""SRC={stagedExePath}"" +set ""DST={currentExePath}"" + +for /L %%i in (1,1,60) do ( + copy /Y ""%SRC%"" ""%DST%"" >nul 2>nul && goto copied + timeout /t 1 /nobreak >nul +) + +exit /b 1 + +:copied +start """" ""%DST%"" +del /Q ""%SRC%"" >nul 2>nul +del /Q ""%~f0"" >nul 2>nul +exit /b 0 +"; + } + + private async Task Button_SelfUpdateAsync() + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) + return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = "Downloading Wauncher update..."; + vm.UpdateStatusSpeed = ""; + + try + { + if (string.IsNullOrWhiteSpace(_selfUpdateDownloadUrl)) + throw new Exception("No self-update package URL found."); + + var updatesDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "self-update"); + Directory.CreateDirectory(updatesDir); + + var safeVersion = Regex.Replace(_selfUpdateVersion, @"[^0-9A-Za-z\.\-_]", string.Empty); + if (string.IsNullOrWhiteSpace(safeVersion)) + safeVersion = "latest"; + + var stagedExePath = Path.Combine(updatesDir, $"wauncher_{safeVersion}.exe"); + await DownloadFileWithProgressAsync(_selfUpdateDownloadUrl, stagedExePath, percent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateProgress = percent; + vm.UpdateStatusFile = $"Downloading Wauncher update... {percent:F0}%"; + }); + }, token); + + var currentExePath = Services.GetExePath(); + if (string.IsNullOrWhiteSpace(currentExePath)) + throw new Exception("Could not locate current Wauncher executable."); + + var scriptPath = Path.Combine(updatesDir, "apply_wauncher_update.cmd"); + var script = BuildSelfUpdateScript(stagedExePath, currentExePath); + File.WriteAllText(scriptPath, script, Encoding.ASCII); + + Process.Start(new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/c \"{scriptPath}\"", + WorkingDirectory = updatesDir, + CreateNoWindow = true, + UseShellExecute = false, + }); + + vm.UpdateStatusFile = "Restarting Wauncher to apply update..."; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 100; + await Task.Delay(500, token); + ForceQuit(); + } + catch (OperationCanceledException) + { + vm.UpdateStatusFile = "Update cancelled."; + vm.UpdateStatusSpeed = ""; + await Task.Delay(800); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Self-update failed: {ex.Message}"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + await Task.Delay(2500); + } + finally + { + if (!_forceClose) + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch + { + // keep UI responsive even if refresh fails + } + } + + Interlocked.Exchange(ref _updateInProgress, 0); + } + } + + private async Task CheckForUpdatesAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsOfflineMode) + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + _cachedPatches = null; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + LaunchUpdateButton.IsEnabled = true; + return; + } + + vm.IsCheckingUpdates = true; + LaunchUpdateButton.Background = new SolidColorBrush(Color.Parse("#555555")); + ArrowButton.Background = new SolidColorBrush(Color.Parse("#555555")); + LaunchUpdateButton.IsEnabled = false; + try + { + bool hasSelfUpdate = await CheckForSelfUpdateAsync(); + if (hasSelfUpdate) + { + _cachedPatches = null; + vm.UpdateAvailable = true; + var selfUpdateColor = new SolidColorBrush(Color.Parse("#FFC107")); + LaunchUpdateButton.Background = selfUpdateColor; + ArrowButton.Background = selfUpdateColor; + + if (Interlocked.Exchange(ref _autoSelfUpdateTriggered, 1) == 0) + { + _ = Button_SelfUpdateAsync(); + } + + return; + } + + if (_settings.SkipUpdates) + { + _cachedPatches = null; + vm.UpdateAvailable = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + return; + } + + var patches = await Task.Run(() => PatchManager.ValidatePatches(deleteOutdatedFiles: false)); + bool hasUpdates = patches.Missing.Count > 0 || patches.Outdated.Count > 0; + + // Cache the result so Button_Update can consume it without re-validating. + _cachedPatches = patches; + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + vm.UpdateAvailable = hasUpdates; + var buttonColor = new SolidColorBrush( + Color.Parse(hasUpdates ? "#FFC107" : "#4CAF50")); LaunchUpdateButton.Background = buttonColor; ArrowButton.Background = buttonColor; } @@ -1236,28 +1223,30 @@ private async Task CheckForUpdatesAsync() } } - private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) return; - - if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) - return; - - _updateCts?.Dispose(); - _updateCts = new CancellationTokenSource(); - var token = _updateCts.Token; - vm.IsUpdating = true; - vm.UpdateAvailable = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; + private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; vm.UpdateStatus = ""; try { // Use the result already computed by CheckForUpdatesAsync when available, // to avoid a redundant full validation on every update click. - var patches = _cachedPatches ?? await Task.Run(() => PatchManager.ValidatePatches(), token); + bool validateAll = _forceValidateAllOnce; + _forceValidateAllOnce = false; + var patches = _cachedPatches ?? await Task.Run(() => PatchManager.ValidatePatches(validateAll: validateAll), token); _cachedPatches = null; // consumed — force fresh check next time if (token.IsCancellationRequested) return; @@ -1275,44 +1264,44 @@ private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEv int totalFiles = allPatches.Count; int completed = 0; - foreach (var patch in allPatches) - { - if (token.IsCancellationRequested) break; - - var extractWatch = new System.Diagnostics.Stopwatch(); - await DownloadManager.DownloadPatch( - patch, - onProgress: (p) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; - vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); - vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; - }); - }, - onExtract: () => - { - extractWatch.Restart(); - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); - vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; - }); - }); + foreach (var patch in allPatches) + { + if (token.IsCancellationRequested) break; + + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); completed++; vm.UpdateProgress = (double)completed / totalFiles * 100.0; @@ -1339,27 +1328,27 @@ await DownloadManager.DownloadPatch( vm.UpdateIndeterminate = false; await Task.Delay(3000); } - finally - { - vm.IsUpdating = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - _cachedPatches = null; - _updateCts?.Dispose(); - _updateCts = null; - - try - { - await CheckForUpdatesAsync(); - } - catch { } - - Interlocked.Exchange(ref _updateInProgress, 0); - } - } + finally + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _cachedPatches = null; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch { } + + Interlocked.Exchange(ref _updateInProgress, 0); + } + } private void Button_CancelUpdate(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { @@ -1372,79 +1361,79 @@ private void FriendsTab_Click(object? sender, Avalonia.Interactivity.RoutedEvent if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "Friends"; } - private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "PatchNotes"; - PatchNotesScroll.Offset = new Vector(0, 0); - } - - private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (sender is not MenuItem { Tag: FriendInfo friend }) - return; - - var profileId = ResolveProfileSteamId(friend.SteamId); - if (string.IsNullOrWhiteSpace(profileId)) - return; - - try - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = $"https://eddies.cc/profiles/{profileId}", - UseShellExecute = true - }); - } - catch - { - // Best-effort open. - } - } - - private static string ResolveProfileSteamId(string? steamId) - { - if (string.IsNullOrWhiteSpace(steamId)) - return string.Empty; - - var value = steamId.Trim(); - if (ulong.TryParse(value, out _)) - return value; - - if (TryConvertSteamId2To64(value, out var steamId64)) - return steamId64.ToString(); - - return string.Empty; - } - - private static bool TryConvertSteamId2To64(string steamId2, out ulong steamId64) - { - steamId64 = 0; - var match = Regex.Match(steamId2, @"^STEAM_[0-5]:([0-1]):(\d+)$", RegexOptions.IgnoreCase); - if (!match.Success) - return false; - - if (!ulong.TryParse(match.Groups[1].Value, out var y)) - return false; - if (!ulong.TryParse(match.Groups[2].Value, out var z)) - return false; - - steamId64 = 76561197960265728UL + (z * 2UL) + y; - return true; - } - - private void Button_Settings(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { + private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "PatchNotes"; + PatchNotesScroll.Offset = new Vector(0, 0); + } + + private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: FriendInfo friend }) + return; + + var profileId = ResolveProfileSteamId(friend.SteamId); + if (string.IsNullOrWhiteSpace(profileId)) + return; + + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = $"https://eddies.cc/profiles/{profileId}", + UseShellExecute = true + }); + } + catch + { + // Best-effort open. + } + } + + private static string ResolveProfileSteamId(string? steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) + return string.Empty; + + var value = steamId.Trim(); + if (ulong.TryParse(value, out _)) + return value; + + if (TryConvertSteamId2To64(value, out var steamId64)) + return steamId64.ToString(); + + return string.Empty; + } + + private static bool TryConvertSteamId2To64(string steamId2, out ulong steamId64) + { + steamId64 = 0; + var match = Regex.Match(steamId2, @"^STEAM_[0-5]:([0-1]):(\d+)$", RegexOptions.IgnoreCase); + if (!match.Success) + return false; + + if (!ulong.TryParse(match.Groups[1].Value, out var y)) + return false; + if (!ulong.TryParse(match.Groups[2].Value, out var z)) + return false; + + steamId64 = 76561197960265728UL + (z * 2UL) + y; + return true; + } + + private void Button_Settings(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { if (_settingsWindow == null) { - _settingsWindow = new SettingsWindow(); - _settingsWindow.Closed += (s, e) => - { - _settingsWindow = null; - _settings = SettingsWindowViewModel.LoadGlobal(); - _ = CheckForUpdatesAsync(); - }; - _settingsWindow.Show(this); - } + _settingsWindow = new SettingsWindow(); + _settingsWindow.Closed += (s, e) => + { + _settingsWindow = null; + _settings = SettingsWindowViewModel.LoadGlobal(); + _ = CheckForUpdatesAsync(); + }; + _settingsWindow.Show(this); + } else _settingsWindow.Activate(); } @@ -1460,148 +1449,114 @@ private void Button_Info(object? sender, Avalonia.Interactivity.RoutedEventArgs } // ── Launch button glow + color ──────────────────────────────────────────── - private void SetLaunchGlow(bool updating) - { - var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); - LaunchUpdateButton.Background = brush; - ArrowButton.Background = brush; - LaunchButtonGlow.BoxShadow = updating - ? BoxShadows.Parse("0 0 18 2 #55FFC107") - : BoxShadows.Parse("0 0 18 2 #554CAF50"); - } - - private static string ShortFileName(string path) - { - if (string.IsNullOrWhiteSpace(path)) - return path; - - var normalized = path.Replace('\\', '/'); - if (normalized.Length <= 42) - return normalized; - - var fileName = Path.GetFileName(normalized); - if (fileName.Length <= 30) - return fileName; - - return fileName[..27] + "..."; - } - - private static string FormatDownloadSpeedAndEta(object progressArgs) - { - double speedBytes = 0; - if (TryGetDoubleProperty(progressArgs, "AverageBytesPerSecondSpeed", out var avg) && avg > 0) - speedBytes = avg; - else if (TryGetDoubleProperty(progressArgs, "BytesPerSecondSpeed", out var cur) && cur > 0) - speedBytes = cur; - - var speedText = speedBytes > 0 - ? $"{speedBytes / 1024.0 / 1024.0:F1} MB/s" - : ""; - - if (speedBytes <= 0 || - !TryGetLongProperty(progressArgs, "TotalBytesToReceive", out var totalBytes) || - !TryGetLongProperty(progressArgs, "ReceivedBytesSize", out var receivedBytes) || - totalBytes <= 0 || receivedBytes < 0 || receivedBytes >= totalBytes) - { - return speedText; - } - - var remainingBytes = totalBytes - receivedBytes; - var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); - var etaText = $"ETA {FormatEta(eta)}"; - - return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; - } - - private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) - { - if (watch == null || !watch.IsRunning || percent <= 1.0) - return ""; - - var elapsed = watch.Elapsed.TotalSeconds; - var total = elapsed / (percent / 100.0); - var remaining = Math.Max(0, total - elapsed); - return $"ETA {FormatEta(TimeSpan.FromSeconds(remaining))}"; - } - - private static string FormatEta(TimeSpan eta) - { - if (eta.TotalHours >= 1) - return eta.ToString(@"hh\:mm\:ss"); - return eta.ToString(@"mm\:ss"); - } - - private static bool TryGetDoubleProperty(object obj, string propertyName, out double value) - { - value = 0; - var prop = obj.GetType().GetProperty(propertyName); - if (prop == null) return false; - var raw = prop.GetValue(obj); - if (raw == null) return false; - try - { - value = Convert.ToDouble(raw); - return true; - } - catch - { - return false; - } - } - - private static bool TryGetLongProperty(object obj, string propertyName, out long value) - { - value = 0; - var prop = obj.GetType().GetProperty(propertyName); - if (prop == null) return false; - var raw = prop.GetValue(obj); - if (raw == null) return false; - try - { - value = Convert.ToInt64(raw); - return true; - } - catch - { - return false; - } - } - - // Minimal parser for launch options that supports quoted values. - private static IEnumerable ParseLaunchOptions(string options) - { - if (string.IsNullOrWhiteSpace(options)) - yield break; - - var current = new StringBuilder(); - bool inQuotes = false; - - foreach (var ch in options) - { - if (ch == '"') - { - inQuotes = !inQuotes; - continue; - } - - if (char.IsWhiteSpace(ch) && !inQuotes) - { - if (current.Length > 0) - { - yield return current.ToString(); - current.Clear(); - } - continue; - } - - current.Append(ch); - } - - if (current.Length > 0) - yield return current.ToString(); - } - - } -} + private void SetLaunchGlow(bool updating) + { + var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); + LaunchUpdateButton.Background = brush; + ArrowButton.Background = brush; + LaunchButtonGlow.BoxShadow = updating + ? BoxShadows.Parse("0 0 18 2 #55FFC107") + : BoxShadows.Parse("0 0 18 2 #554CAF50"); + } + + private static string ShortFileName(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return path; + + var normalized = path.Replace('\\', '/'); + if (normalized.Length <= 42) + return normalized; + + var fileName = Path.GetFileName(normalized); + if (fileName.Length <= 30) + return fileName; + + return fileName[..27] + "..."; + } + + private static string FormatDownloadSpeedAndEta(object progressArgs) + { + double speedBytes = 0; + if (TryGetDoubleProperty(progressArgs, "AverageBytesPerSecondSpeed", out var avg) && avg > 0) + speedBytes = avg; + else if (TryGetDoubleProperty(progressArgs, "BytesPerSecondSpeed", out var cur) && cur > 0) + speedBytes = cur; + + var speedText = speedBytes > 0 + ? $"{speedBytes / 1024.0 / 1024.0:F1} MB/s" + : ""; + + if (speedBytes <= 0 || + !TryGetLongProperty(progressArgs, "TotalBytesToReceive", out var totalBytes) || + !TryGetLongProperty(progressArgs, "ReceivedBytesSize", out var receivedBytes) || + totalBytes <= 0 || receivedBytes < 0 || receivedBytes >= totalBytes) + { + return speedText; + } + + var remainingBytes = totalBytes - receivedBytes; + var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); + var etaText = $"ETA {FormatEta(eta)}"; + + return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; + } + + private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) + { + if (watch == null || !watch.IsRunning || percent <= 1.0) + return ""; + + var elapsed = watch.Elapsed.TotalSeconds; + var total = elapsed / (percent / 100.0); + var remaining = Math.Max(0, total - elapsed); + return $"ETA {FormatEta(TimeSpan.FromSeconds(remaining))}"; + } + + private static string FormatEta(TimeSpan eta) + { + if (eta.TotalHours >= 1) + return eta.ToString(@"hh\:mm\:ss"); + return eta.ToString(@"mm\:ss"); + } + + private static bool TryGetDoubleProperty(object obj, string propertyName, out double value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToDouble(raw); + return true; + } + catch + { + return false; + } + } + + private static bool TryGetLongProperty(object obj, string propertyName, out long value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToInt64(raw); + return true; + } + catch + { + return false; + } + } + + } +} diff --git a/Wauncher/Views/SettingsWindow.axaml b/Wauncher/Views/SettingsWindow.axaml index 5ae67c6..cb9ba0f 100644 --- a/Wauncher/Views/SettingsWindow.axaml +++ b/Wauncher/Views/SettingsWindow.axaml @@ -173,29 +173,6 @@ OnContent="" /> - - - - - From 94f042b3888b262161123096cc88e0a92e8b9450 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 04:38:46 -0400 Subject: [PATCH 09/51] Wauncher update --- README.md | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b5a7f7e..cceb4b1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

-

ClassicCounter Launcher

+

ClassicCounter Wauncher

- Launcher for ClassicCounter with Discord RPC, Auto-Updates and More! + Wauncher for ClassicCounter with Discord RPC, Auto-Updates and More!
Written in C# using .NET 8.

@@ -13,21 +13,11 @@ [![MIT License][license-shield]][license-url] > [!IMPORTANT] -> .NET Runtime 8 is required to run the launcher. Download it from [**here**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.11-windows-x64-installer). +> .NET Runtime 8 is required to run the Wauncher. Download it from [**here**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.11-windows-x64-installer). -## Arguments -- `--debug-mode` - Enables debug mode, prints additional info. -- `--disable-rpc` - Disables Discord RPC. -- `--gc` - Launches the Game with custom Game Coordinator. -- `--install-dependencies` - Launches setup process for required Game dependencies. -- `--patch-only` - Will only check for patches, won't open the game. -- `--skip-updates` - Skips checking for launcher updates. -- `--skip-validating` - Skips validating patches. -- `--validate-all` - Validates all game files. - -> [!CAUTION] -> **Using `--skip-updates` or `--skip-validating` is NOT recommended!** -> **An outdated launcher or patches might cause issues.** +## Settings +- Validation behavior is controlled in the GUI. +- Use `Verify Game Files` from the launch button drop-up menu when you want a full file check. ## Packages Used - [CSGSI](https://github.com/rakijah/CSGSI) by [rakijah](https://github.com/rakijah) From b60f120ee932e1f52c99a8be31fafc491ae996ee Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:57:06 -0400 Subject: [PATCH 10/51] show verify progress during file scan From f671e669d956918a6de88c89ba23b34adf1442c0 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:58:42 -0400 Subject: [PATCH 11/51] show verify progress during file scan --- Wauncher/Views/MainWindow.axaml.cs | 2514 ++++++++++++++-------------- 1 file changed, 1257 insertions(+), 1257 deletions(-) diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index e452260..1d91f31 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -1,64 +1,64 @@ -using System.IO; -using System.Net.Http; -using System.Linq; -using System.Net.NetworkInformation; -using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Text.Json; -using System.Text; -using System.Text.RegularExpressions; -using Avalonia.Animation; -using Avalonia.Animation.Easings; -using Avalonia; +using System.IO; +using System.Net.Http; +using System.Linq; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Text.Json; +using System.Text; +using System.Text.RegularExpressions; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Threading; -using Wauncher.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Wauncher.ViewModels; -using Wauncher.Views; +using Avalonia.Platform; +using Avalonia.Threading; +using Wauncher.Utils; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Wauncher.ViewModels; +using Wauncher.Views; namespace Wauncher.Views { - public partial class MainWindow : Window - { - private InfoWindow? _infoWindow = null; - private SettingsWindow? _settingsWindow = null; - private SettingsWindowViewModel _settings; - private int _launchInProgress; - private int _updateInProgress; - private int _installInProgress; - - private bool _dropdownOpen = false; + public partial class MainWindow : Window + { + private InfoWindow? _infoWindow = null; + private SettingsWindow? _settingsWindow = null; + private SettingsWindowViewModel _settings; + private int _launchInProgress; + private int _updateInProgress; + private int _installInProgress; + + private bool _dropdownOpen = false; private const double HeightClosed = 720; private const double HeightOpen = 720; // ── Image carousel (center content area) ────────────────────────────────── - private Image[] _carouselImages = Array.Empty(); - private DispatcherTimer? _carouselTimer; - private int _currentCarouselIndex = 0; - private const int CarouselRotationIntervalSeconds = 5; - private readonly List _zoomCts = new(); - private static string WauncherDirectory => - Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); + private Image[] _carouselImages = Array.Empty(); + private DispatcherTimer? _carouselTimer; + private int _currentCarouselIndex = 0; + private const int CarouselRotationIntervalSeconds = 5; + private readonly List _zoomCts = new(); + private static string WauncherDirectory => + Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); public MainWindow() { InitializeComponent(); _settings = SettingsWindowViewModel.LoadGlobal(); - this.Loaded += (_, _) => - { - var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = buttonColor; - ArrowButton.Background = buttonColor; - LaunchUpdateButton.IsEnabled = true; - }; + this.Loaded += (_, _) => + { + var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = buttonColor; + ArrowButton.Background = buttonColor; + LaunchUpdateButton.IsEnabled = true; + }; this.Opened += (_, _) => { @@ -93,72 +93,72 @@ public MainWindow() this.Closed += (_, _) => TeardownCarousel(); } - // ── Image carousel (center content area) ────────────────────────────────── - private static readonly HttpClient _http = new(); - private static string PatchNotesCachePath => - Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "cache", - "patchnotes.md"); - - private async Task SetupCarouselAsync() - { - try - { - TeardownCarousel(); - - var carouselContainer = this.FindControl("CarouselContainer"); - var offlinePanel = this.FindControl("CarouselOfflinePanel"); - var offlineSubText = this.FindControl("CarouselOfflineSubText"); - if (carouselContainer == null) - return; - - bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); - var bitmaps = hasInternet - ? await LoadCarouselFromGitHubAsync() - : null; - - if (bitmaps == null || bitmaps.Count == 0) - { - if (offlinePanel != null) - offlinePanel.IsVisible = true; - if (offlineSubText != null) - { - offlineSubText.Text = hasInternet - ? "Carousel is temporarily unavailable." - : "Connect to Wi-Fi or Ethernet to load the carousel."; - } - return; - } - - if (offlinePanel != null) - offlinePanel.IsVisible = false; - - _carouselImages = CreateCarouselImages(bitmaps); - EnsureZoomSlots(_carouselImages.Length); - - foreach (var existingImage in carouselContainer.Children.OfType().ToList()) - carouselContainer.Children.Remove(existingImage); - - int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; - for (int i = 0; i < _carouselImages.Length; i++) - { - if (overlayIndex >= 0) - { - carouselContainer.Children.Insert(overlayIndex, _carouselImages[i]); - overlayIndex++; - } - else - { - carouselContainer.Children.Add(_carouselImages[i]); - } - } - - _currentCarouselIndex = 0; - _carouselImages[0].Opacity = 1.0; - StartZoomOut(_carouselImages[0], 0); + // ── Image carousel (center content area) ────────────────────────────────── + private static readonly HttpClient _http = new(); + private static string PatchNotesCachePath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache", + "patchnotes.md"); + + private async Task SetupCarouselAsync() + { + try + { + TeardownCarousel(); + + var carouselContainer = this.FindControl("CarouselContainer"); + var offlinePanel = this.FindControl("CarouselOfflinePanel"); + var offlineSubText = this.FindControl("CarouselOfflineSubText"); + if (carouselContainer == null) + return; + + bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); + var bitmaps = hasInternet + ? await LoadCarouselFromGitHubAsync() + : null; + + if (bitmaps == null || bitmaps.Count == 0) + { + if (offlinePanel != null) + offlinePanel.IsVisible = true; + if (offlineSubText != null) + { + offlineSubText.Text = hasInternet + ? "Carousel is temporarily unavailable." + : "Connect to Wi-Fi or Ethernet to load the carousel."; + } + return; + } + + if (offlinePanel != null) + offlinePanel.IsVisible = false; + + _carouselImages = CreateCarouselImages(bitmaps); + EnsureZoomSlots(_carouselImages.Length); + + foreach (var existingImage in carouselContainer.Children.OfType().ToList()) + carouselContainer.Children.Remove(existingImage); + + int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; + for (int i = 0; i < _carouselImages.Length; i++) + { + if (overlayIndex >= 0) + { + carouselContainer.Children.Insert(overlayIndex, _carouselImages[i]); + overlayIndex++; + } + else + { + carouselContainer.Children.Add(_carouselImages[i]); + } + } + + _currentCarouselIndex = 0; + _carouselImages[0].Opacity = 1.0; + StartZoomOut(_carouselImages[0], 0); _carouselTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CarouselRotationIntervalSeconds) }; _carouselTimer.Tick += (_, _) => RotateCarousel(); @@ -170,35 +170,35 @@ private async Task SetupCarouselAsync() } } - private async Task?> LoadCarouselFromGitHubAsync() - { - try - { - var json = await Api.GitHub.GetCarouselAssetsWauncher(); - var assets = JsonConvert.DeserializeObject>(json); - if (assets == null || assets.Count == 0) - return null; - - var urls = assets - .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) - .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) - .OrderBy(a => GetCarouselSortIndex(a.Name)) - .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) - .Select(a => a.DownloadUrl!) - .ToList(); - - if (urls.Count == 0) - return null; - - var bitmaps = new List(); - foreach (var url in urls) - { - try + private async Task?> LoadCarouselFromGitHubAsync() + { + try + { + var json = await Api.GitHub.GetCarouselAssetsWauncher(); + var assets = JsonConvert.DeserializeObject>(json); + if (assets == null || assets.Count == 0) + return null; + + var urls = assets + .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) + .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) + .OrderBy(a => GetCarouselSortIndex(a.Name)) + .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) + .Select(a => a.DownloadUrl!) + .ToList(); + + if (urls.Count == 0) + return null; + + var bitmaps = new List(); + foreach (var url in urls) + { + try { var bytes = await _http.GetByteArrayAsync(url); using var ms = new MemoryStream(bytes); @@ -207,84 +207,84 @@ private async Task SetupCarouselAsync() catch { } } return bitmaps.Count > 0 ? bitmaps : null; - } - catch { return null; } - } - - private static int GetCarouselSortIndex(string name) - { - var match = Regex.Match(name, @"^carousel_(\d+)", RegexOptions.IgnoreCase); - if (match.Success && int.TryParse(match.Groups[1].Value, out var index)) - return index; - return int.MaxValue; - } - - private sealed class GitHubAssetEntry - { - [JsonProperty("name")] - public string Name { get; set; } = string.Empty; - - [JsonProperty("type")] - public string Type { get; set; } = string.Empty; - - [JsonProperty("download_url")] - public string? DownloadUrl { get; set; } - } - - private static Image[] CreateCarouselImages(IReadOnlyList bitmaps) - { - var images = new Image[bitmaps.Count]; - for (int i = 0; i < bitmaps.Count; i++) - { - images[i] = new Image - { - Source = bitmaps[i], - Stretch = Stretch.UniformToFill, - Opacity = 0.0, - Transitions = new Transitions - { - new DoubleTransition - { - Property = Visual.OpacityProperty, - Duration = TimeSpan.FromSeconds(1.5), - Easing = new CubicEaseInOut() - } - } - }; - } - - return images; - } - - private void EnsureZoomSlots(int count) - { - while (_zoomCts.Count < count) - _zoomCts.Add(null); - } - - private void RotateCarousel() - { - if (_carouselImages.Length == 0) - return; - - // Fade out current image (zoom continues through the crossfade) - _carouselImages[_currentCarouselIndex].Opacity = 0.0; - - // Move to next image - _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; - - // Fade in next image and start fresh zoom-out - StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); - _carouselImages[_currentCarouselIndex].Opacity = 1.0; - } - - private void TeardownCarousel() - { - _carouselTimer?.Stop(); - _carouselTimer = null; - for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); - _carouselImages = Array.Empty(); - } + } + catch { return null; } + } + + private static int GetCarouselSortIndex(string name) + { + var match = Regex.Match(name, @"^carousel_(\d+)", RegexOptions.IgnoreCase); + if (match.Success && int.TryParse(match.Groups[1].Value, out var index)) + return index; + return int.MaxValue; + } + + private sealed class GitHubAssetEntry + { + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("type")] + public string Type { get; set; } = string.Empty; + + [JsonProperty("download_url")] + public string? DownloadUrl { get; set; } + } + + private static Image[] CreateCarouselImages(IReadOnlyList bitmaps) + { + var images = new Image[bitmaps.Count]; + for (int i = 0; i < bitmaps.Count; i++) + { + images[i] = new Image + { + Source = bitmaps[i], + Stretch = Stretch.UniformToFill, + Opacity = 0.0, + Transitions = new Transitions + { + new DoubleTransition + { + Property = Visual.OpacityProperty, + Duration = TimeSpan.FromSeconds(1.5), + Easing = new CubicEaseInOut() + } + } + }; + } + + return images; + } + + private void EnsureZoomSlots(int count) + { + while (_zoomCts.Count < count) + _zoomCts.Add(null); + } + + private void RotateCarousel() + { + if (_carouselImages.Length == 0) + return; + + // Fade out current image (zoom continues through the crossfade) + _carouselImages[_currentCarouselIndex].Opacity = 0.0; + + // Move to next image + _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; + + // Fade in next image and start fresh zoom-out + StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); + _carouselImages[_currentCarouselIndex].Opacity = 1.0; + } + + private void TeardownCarousel() + { + _carouselTimer?.Stop(); + _carouselTimer = null; + for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); + _carouselImages = Array.Empty(); + } private void StartZoomOut(Image img, int slot) { @@ -314,28 +314,28 @@ private void StartZoomOut(Image img, int slot) zoomTimer.Start(); } - private void StopZoom(int slot) - { - if (slot < 0 || slot >= _zoomCts.Count) - return; - - _zoomCts[slot]?.Cancel(); - _zoomCts[slot] = null; - } + private void StopZoom(int slot) + { + if (slot < 0 || slot >= _zoomCts.Count) + return; + + _zoomCts[slot]?.Cancel(); + _zoomCts[slot] = null; + } // ── Server dropdown ─────────────────────────────────────────── - private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) - { - CloseDropdown(); - return; - } - - _dropdownOpen = !_dropdownOpen; - - if (DataContext is MainWindowViewModel vm) - vm.IsDropdownOpen = _dropdownOpen; + private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) + { + CloseDropdown(); + return; + } + + _dropdownOpen = !_dropdownOpen; + + if (DataContext is MainWindowViewModel vm) + vm.IsDropdownOpen = _dropdownOpen; if (_dropdownOpen) { @@ -367,49 +367,49 @@ private void CloseDropdown() } // ── Game launch ─────────────────────────────────────────── - private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - - if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || Volatile.Read(ref _launchInProgress) == 1) - return; - else if (vm.IsNeedingInstall) - _ = InstallGameFromCdnAsync(); - else if (_settings.SkipUpdates) - _ = LaunchGameAsync(); - else if (vm.UpdateAvailable) - { - if (_selfUpdateAvailable) - _ = Button_SelfUpdateAsync(); - else - Button_Update(sender, e); - } - else - _ = LaunchGameAsync(); - } - - private async Task LaunchGameAsync() - { - if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) - return; - - var vm = DataContext as MainWindowViewModel; - try - { - _settings = SettingsWindowViewModel.LoadGlobal(); - + private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || Volatile.Read(ref _launchInProgress) == 1) + return; + else if (vm.IsNeedingInstall) + _ = InstallGameFromCdnAsync(); + else if (_settings.SkipUpdates) + _ = LaunchGameAsync(); + else if (vm.UpdateAvailable) + { + if (_selfUpdateAvailable) + _ = Button_SelfUpdateAsync(); + else + Button_Update(sender, e); + } + else + _ = LaunchGameAsync(); + } + + private async Task LaunchGameAsync() + { + if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) + return; + + var vm = DataContext as MainWindowViewModel; + try + { + _settings = SettingsWindowViewModel.LoadGlobal(); + if (vm != null) vm.GameStatus = "Running"; - // Clear any arguments left over from a previous launch before adding new ones. - Argument.ClearAdditionalArguments(); - - Argument.AddArgument("-novid"); - - var selected = vm?.SelectedServer; - if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) - { + // Clear any arguments left over from a previous launch before adding new ones. + Argument.ClearAdditionalArguments(); + + Argument.AddArgument("-novid"); + + var selected = vm?.SelectedServer; + if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) + { Argument.AddArgument("+connect"); Argument.AddArgument(selected.IpPort); } @@ -427,16 +427,16 @@ private async Task LaunchGameAsync() await Game.Monitor(); } - catch (Exception ex) - { - Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); - } - finally - { - if (vm != null) vm.GameStatus = "Not Running"; - Interlocked.Exchange(ref _launchInProgress, 0); - } - } + catch (Exception ex) + { + Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); + } + finally + { + if (vm != null) vm.GameStatus = "Not Running"; + Interlocked.Exchange(ref _launchInProgress, 0); + } + } // ── Window chrome ─────────────────────────────────────────── private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) @@ -466,215 +466,215 @@ public void ForceQuit() Close(); } - private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var dir = WauncherDirectory; - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = dir, - UseShellExecute = true - }); - } - - private void VerifyGameFiles_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is not MainWindowViewModel vm) - return; - - if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || vm.IsNeedingInstall) - return; - - _forceValidateAllOnce = true; - _cachedPatches = null; - Button_Update(sender, e); - } - - // ── Update ───────────────────────────────────────────────────── - private CancellationTokenSource? _updateCts; - private Patches? _cachedPatches; - private bool _selfUpdateAvailable; - private string _selfUpdateDownloadUrl = string.Empty; - private string _selfUpdateVersion = string.Empty; - private int _autoSelfUpdateTriggered; - private bool _forceValidateAllOnce; + private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var dir = WauncherDirectory; + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = dir, + UseShellExecute = true + }); + } + + private void VerifyGameFiles_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is not MainWindowViewModel vm) + return; + + if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || vm.IsNeedingInstall) + return; + + _forceValidateAllOnce = true; + _cachedPatches = null; + Button_Update(sender, e); + } + + // ── Update ───────────────────────────────────────────────────── + private CancellationTokenSource? _updateCts; + private Patches? _cachedPatches; + private bool _selfUpdateAvailable; + private string _selfUpdateDownloadUrl = string.Empty; + private string _selfUpdateVersion = string.Empty; + private int _autoSelfUpdateTriggered; + private bool _forceValidateAllOnce; /// /// Called on window open. If csgo.exe is missing, triggers a full CDN install. /// Otherwise runs the normal patch update check. /// - private async Task StartupAsync() - { - // Yield to let Avalonia finish its initial layout/styling pass - // (Loaded sets the button disabled/gray; we need that to settle before overriding) - await Task.Delay(50); - - LaunchUpdateButton.IsEnabled = true; - - string csgoExe = Path.Combine(WauncherDirectory, "csgo.exe"); - if (DataContext is not MainWindowViewModel vm) - return; - - if (!File.Exists(csgoExe)) - { - vm.IsNeedingInstall = true; - var blue = new SolidColorBrush(Color.Parse("#2196F3")); - LaunchUpdateButton.Background = blue; - ArrowButton.Background = blue; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - if (vm?.IsOfflineMode == true) - { - vm.IsNeedingInstall = false; - vm.UpdateAvailable = false; - var green = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = green; - ArrowButton.Background = green; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - _settings = SettingsWindowViewModel.LoadGlobal(); - if (_settings.SkipUpdates) - { - vm!.IsNeedingInstall = false; - vm.UpdateAvailable = false; - vm.IsCheckingUpdates = false; - var green = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = green; - ArrowButton.Background = green; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - await CheckForUpdatesAsync(); - } - - private async Task InstallGameFromCdnAsync() - { - if (DataContext is not MainWindowViewModel vm) return; - if (Interlocked.Exchange(ref _installInProgress, 1) == 1) - return; - - bool installSucceeded = false; - vm.IsNeedingInstall = false; - vm.IsInstalling = true; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = "Connecting..."; + private async Task StartupAsync() + { + // Yield to let Avalonia finish its initial layout/styling pass + // (Loaded sets the button disabled/gray; we need that to settle before overriding) + await Task.Delay(50); + + LaunchUpdateButton.IsEnabled = true; + + string csgoExe = Path.Combine(WauncherDirectory, "csgo.exe"); + if (DataContext is not MainWindowViewModel vm) + return; + + if (!File.Exists(csgoExe)) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + if (vm?.IsOfflineMode == true) + { + vm.IsNeedingInstall = false; + vm.UpdateAvailable = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + _settings = SettingsWindowViewModel.LoadGlobal(); + if (_settings.SkipUpdates) + { + vm!.IsNeedingInstall = false; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + await CheckForUpdatesAsync(); + } + + private async Task InstallGameFromCdnAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + if (Interlocked.Exchange(ref _installInProgress, 1) == 1) + return; + + bool installSucceeded = false; + vm.IsNeedingInstall = false; + vm.IsInstalling = true; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Connecting..."; vm.UpdateStatusSpeed = ""; - try - { - await DownloadManager.InstallFullGame( - onProgress: (file, speed, percent) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = $"Installing {ShortFileName(file)} {percent:F0}%"; - vm.UpdateStatusSpeed = string.IsNullOrWhiteSpace(speed) ? "" : speed; - vm.UpdateProgress = percent; - vm.UpdateIndeterminate = false; - }); - }, - onStatus: status => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = status; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); - if (!vm.UpdateIndeterminate) - vm.UpdateProgress = 0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = extractPercent; - }); - }); - - // Immediately apply any post-install patches so first-time installs - // end in a launch-ready state without requiring a second manual update. - Dispatcher.UIThread.Post(() => - { - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = "Checking post-install patches..."; - vm.UpdateStatusSpeed = ""; - }); - - var patches = await Task.Run(() => PatchManager.ValidatePatches()); - var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); - if (allPatches.Count > 0) - { - int totalFiles = allPatches.Count; - int completed = 0; - - foreach (var patch in allPatches) - { - var extractWatch = new System.Diagnostics.Stopwatch(); - await DownloadManager.DownloadPatch( - patch, - onProgress: (p) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; - vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); - vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; - }); - }, - onExtract: () => - { - extractWatch.Restart(); - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); - vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; - }); - }); - - completed++; - vm.UpdateProgress = (double)completed / totalFiles * 100.0; - } - } - - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = "Game installed and fully updated!"; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = false; - vm.UpdateProgress = 100; - }); - installSucceeded = true; - await Task.Delay(1500); - } - catch (Exception ex) - { - vm.UpdateStatusFile = $"Install error: {ex.Message}"; + try + { + await DownloadManager.InstallFullGame( + onProgress: (file, speed, percent) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = $"Installing {ShortFileName(file)} {percent:F0}%"; + vm.UpdateStatusSpeed = string.IsNullOrWhiteSpace(speed) ? "" : speed; + vm.UpdateProgress = percent; + vm.UpdateIndeterminate = false; + }); + }, + onStatus: status => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = status; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); + if (!vm.UpdateIndeterminate) + vm.UpdateProgress = 0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = extractPercent; + }); + }); + + // Immediately apply any post-install patches so first-time installs + // end in a launch-ready state without requiring a second manual update. + Dispatcher.UIThread.Post(() => + { + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Checking post-install patches..."; + vm.UpdateStatusSpeed = ""; + }); + + var patches = await Task.Run(() => PatchManager.ValidatePatches()); + var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); + if (allPatches.Count > 0) + { + int totalFiles = allPatches.Count; + int completed = 0; + + foreach (var patch in allPatches) + { + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); + + completed++; + vm.UpdateProgress = (double)completed / totalFiles * 100.0; + } + } + + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = "Game installed and fully updated!"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + vm.UpdateProgress = 100; + }); + installSucceeded = true; + await Task.Delay(1500); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Install error: {ex.Message}"; vm.UpdateStatusSpeed = ""; await Task.Delay(4000); } @@ -682,196 +682,196 @@ await DownloadManager.DownloadPatch( { vm.IsInstalling = false; vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - - _cachedPatches = null; - - if (!installSucceeded && !File.Exists(Path.Combine(WauncherDirectory, "csgo.exe"))) - { - vm.IsNeedingInstall = true; - var blue = new SolidColorBrush(Color.Parse("#2196F3")); - LaunchUpdateButton.Background = blue; - ArrowButton.Background = blue; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); - } - else - { - try - { - await CheckForUpdatesAsync(); - } - catch { } - } - - LaunchUpdateButton.IsEnabled = true; - Interlocked.Exchange(ref _installInProgress, 0); - } - } - - private async Task LoadPatchNotesAsync() - { - try - { - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Loading latest patch notes..."; - PatchNotesVersion.IsVisible = true; - }); - - if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) - { - Dispatcher.UIThread.Post(() => - { - var cachedItems = LoadCachedPatchNotes(); - if (cachedItems.Count > 0) - { - PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; - PatchNotesList.ItemsSource = cachedItems; - } - else - { - PatchNotesVersion.Text = "Patch notes are unavailable offline."; - PatchNotesList.ItemsSource = new List(); - } - - PatchNotesVersion.IsVisible = true; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - return; - } - - var md = await Api.GitHub.GetPatchNotesWauncher(); - var items = ParsePatchNotes(md); - SavePatchNotesCache(md); - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = items.Count > 0 - ? $"Updated {DateTime.Now:MMM d, h:mm tt}" - : "Patch notes are currently empty."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - } - catch - { - var items = LoadCachedPatchNotes(); - if (items.Count == 0) - { - items = BuildFallbackPatchNotes(); - } - - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Using fallback patch notes."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - } - } - - private static void SavePatchNotesCache(string markdown) - { - try - { - var directory = Path.GetDirectoryName(PatchNotesCachePath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(PatchNotesCachePath, markdown); - } - catch - { - // Caching is best-effort; keep patch notes functional if disk write fails. - } - } - - private static List LoadCachedPatchNotes() - { - try - { - if (!File.Exists(PatchNotesCachePath)) - { - return new List(); - } - - var markdown = File.ReadAllText(PatchNotesCachePath); - return ParsePatchNotes(markdown); - } - catch - { - return new List(); - } - } - - private static List BuildFallbackPatchNotes() - { - return new List - { - new() { Text = "Anniversary Update", IsMajorHeader = true }, - new() { Text = "What's Changed", IsHeader = true }, - new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, - new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, - new() { Text = "Bug fixes and security improvements.", IsBullet = true }, - }; - } - - private static List ParsePatchNotes(string markdown) - { - var items = new List(); - foreach (var raw in markdown.Split('\n')) - { - var line = raw.TrimEnd(); - if (string.IsNullOrWhiteSpace(line)) continue; - - line = line.Trim(); - line = line.Replace("**", "").Replace("__", ""); - line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); - line = Regex.Replace(line, @"`([^`]*)`", "$1"); - - if (line.StartsWith("# ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsMajorHeader = true - }); - } - else if (line.StartsWith("## ") || line.StartsWith("### ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsHeader = true + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + + _cachedPatches = null; + + if (!installSucceeded && !File.Exists(Path.Combine(WauncherDirectory, "csgo.exe"))) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + } + else + { + try + { + await CheckForUpdatesAsync(); + } + catch { } + } + + LaunchUpdateButton.IsEnabled = true; + Interlocked.Exchange(ref _installInProgress, 0); + } + } + + private async Task LoadPatchNotesAsync() + { + try + { + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Loading latest patch notes..."; + PatchNotesVersion.IsVisible = true; + }); + + if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) + { + Dispatcher.UIThread.Post(() => + { + var cachedItems = LoadCachedPatchNotes(); + if (cachedItems.Count > 0) + { + PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; + PatchNotesList.ItemsSource = cachedItems; + } + else + { + PatchNotesVersion.Text = "Patch notes are unavailable offline."; + PatchNotesList.ItemsSource = new List(); + } + + PatchNotesVersion.IsVisible = true; + PatchNotesScroll.Offset = new Vector(0, 0); }); + return; } - else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Substring(2).Trim(), - IsBullet = true - }); - } - else if (Regex.IsMatch(line, @"^\d+\.\s+")) - { - var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); - items.Add(new ViewModels.PatchNoteItem - { - Text = bulletText, - IsBullet = true - }); - } - else if (line.StartsWith("**") && line.EndsWith("**")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Trim('*', ' '), + + var md = await Api.GitHub.GetPatchNotesWauncher(); + var items = ParsePatchNotes(md); + SavePatchNotesCache(md); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = items.Count > 0 + ? $"Updated {DateTime.Now:MMM d, h:mm tt}" + : "Patch notes are currently empty."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + catch + { + var items = LoadCachedPatchNotes(); + if (items.Count == 0) + { + items = BuildFallbackPatchNotes(); + } + + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Using fallback patch notes."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + } + + private static void SavePatchNotesCache(string markdown) + { + try + { + var directory = Path.GetDirectoryName(PatchNotesCachePath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(PatchNotesCachePath, markdown); + } + catch + { + // Caching is best-effort; keep patch notes functional if disk write fails. + } + } + + private static List LoadCachedPatchNotes() + { + try + { + if (!File.Exists(PatchNotesCachePath)) + { + return new List(); + } + + var markdown = File.ReadAllText(PatchNotesCachePath); + return ParsePatchNotes(markdown); + } + catch + { + return new List(); + } + } + + private static List BuildFallbackPatchNotes() + { + return new List + { + new() { Text = "Anniversary Update", IsMajorHeader = true }, + new() { Text = "What's Changed", IsHeader = true }, + new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, + new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, + new() { Text = "Bug fixes and security improvements.", IsBullet = true }, + }; + } + + private static List ParsePatchNotes(string markdown) + { + var items = new List(); + foreach (var raw in markdown.Split('\n')) + { + var line = raw.TrimEnd(); + if (string.IsNullOrWhiteSpace(line)) continue; + + line = line.Trim(); + line = line.Replace("**", "").Replace("__", ""); + line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); + line = Regex.Replace(line, @"`([^`]*)`", "$1"); + + if (line.StartsWith("# ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsMajorHeader = true + }); + } + else if (line.StartsWith("## ") || line.StartsWith("### ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsHeader = true + }); + } + else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Substring(2).Trim(), + IsBullet = true + }); + } + else if (Regex.IsMatch(line, @"^\d+\.\s+")) + { + var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); + items.Add(new ViewModels.PatchNoteItem + { + Text = bulletText, + IsBullet = true + }); + } + else if (line.StartsWith("**") && line.EndsWith("**")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Trim('*', ' '), IsHeader = true }); } @@ -884,329 +884,329 @@ private static void SavePatchNotesCache(string markdown) }); } } - return items; - } - - private sealed class GitHubRelease - { - [JsonProperty("tag_name")] - public string? TagName { get; set; } - - [JsonProperty("assets")] - public List? Assets { get; set; } - } - - private sealed class GitHubReleaseAsset - { - [JsonProperty("name")] - public string Name { get; set; } = string.Empty; - - [JsonProperty("browser_download_url")] - public string DownloadUrl { get; set; } = string.Empty; - } - - private static string NormalizeVersionToken(string? version) - { - if (string.IsNullOrWhiteSpace(version)) - return "0.0.0"; - - var cleaned = version.Trim(); - if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase)) - cleaned = cleaned[1..]; - - cleaned = Regex.Replace(cleaned, @"[^0-9\.]", string.Empty); - return string.IsNullOrWhiteSpace(cleaned) ? "0.0.0" : cleaned; - } - - private static bool TryParseVersion(string value, out global::System.Version parsed) - { - if (global::System.Version.TryParse(value, out parsed!)) - return true; - - var tokens = value.Split('.', StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length == 0) - { - parsed = new global::System.Version(0, 0, 0); - return false; - } - - while (tokens.Length < 3) - tokens = tokens.Append("0").ToArray(); - - return global::System.Version.TryParse(string.Join('.', tokens.Take(4)), out parsed!); - } - - private async Task CheckForSelfUpdateAsync() - { - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - - try - { - var latestReleaseJson = await Api.GitHub.GetLatestRelease(); - var release = JsonConvert.DeserializeObject(latestReleaseJson); - if (release == null) - return false; - - var currentVersion = NormalizeVersionToken(Wauncher.Utils.Version.Current); - var latestVersion = NormalizeVersionToken(release.TagName); - if (!TryParseVersion(currentVersion, out var current) || !TryParseVersion(latestVersion, out var latest)) - return false; - - if (latest <= current) - return false; - - var assets = release.Assets ?? new List(); - var preferred = assets.FirstOrDefault(a => - !string.IsNullOrWhiteSpace(a.DownloadUrl) && - string.Equals(a.Name, "wauncher.exe", StringComparison.OrdinalIgnoreCase)); - - preferred ??= assets.FirstOrDefault(a => - !string.IsNullOrWhiteSpace(a.DownloadUrl) && - a.Name.Contains("wauncher", StringComparison.OrdinalIgnoreCase) && - a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); - - if (preferred == null) - return false; - - _selfUpdateAvailable = true; - _selfUpdateDownloadUrl = preferred.DownloadUrl; - _selfUpdateVersion = latestVersion; - return true; - } - catch - { - return false; - } - } - - private async Task DownloadFileWithProgressAsync(string url, string destination, Action? onProgress, CancellationToken token) - { - using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); - response.EnsureSuccessStatusCode(); - - var totalBytes = response.Content.Headers.ContentLength; - await using var input = await response.Content.ReadAsStreamAsync(token); - await using var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true); - - var buffer = new byte[81920]; - long received = 0; - while (true) - { - int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), token); - if (read == 0) - break; - - await output.WriteAsync(buffer.AsMemory(0, read), token); - received += read; - if (totalBytes.HasValue && totalBytes.Value > 0) - { - onProgress?.Invoke((double)received / totalBytes.Value * 100.0); - } - } - - onProgress?.Invoke(100.0); - } - - private static string BuildSelfUpdateScript(string stagedExePath, string currentExePath) - { - return -$@"@echo off -setlocal -set ""SRC={stagedExePath}"" -set ""DST={currentExePath}"" - -for /L %%i in (1,1,60) do ( - copy /Y ""%SRC%"" ""%DST%"" >nul 2>nul && goto copied - timeout /t 1 /nobreak >nul -) - -exit /b 1 - -:copied -start """" ""%DST%"" -del /Q ""%SRC%"" >nul 2>nul -del /Q ""%~f0"" >nul 2>nul -exit /b 0 -"; - } - - private async Task Button_SelfUpdateAsync() - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) - return; - - if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) - return; - - _updateCts?.Dispose(); - _updateCts = new CancellationTokenSource(); - var token = _updateCts.Token; - - vm.IsUpdating = true; - vm.UpdateAvailable = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = "Downloading Wauncher update..."; - vm.UpdateStatusSpeed = ""; - - try - { - if (string.IsNullOrWhiteSpace(_selfUpdateDownloadUrl)) - throw new Exception("No self-update package URL found."); - - var updatesDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "self-update"); - Directory.CreateDirectory(updatesDir); - - var safeVersion = Regex.Replace(_selfUpdateVersion, @"[^0-9A-Za-z\.\-_]", string.Empty); - if (string.IsNullOrWhiteSpace(safeVersion)) - safeVersion = "latest"; - - var stagedExePath = Path.Combine(updatesDir, $"wauncher_{safeVersion}.exe"); - await DownloadFileWithProgressAsync(_selfUpdateDownloadUrl, stagedExePath, percent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateProgress = percent; - vm.UpdateStatusFile = $"Downloading Wauncher update... {percent:F0}%"; - }); - }, token); - - var currentExePath = Services.GetExePath(); - if (string.IsNullOrWhiteSpace(currentExePath)) - throw new Exception("Could not locate current Wauncher executable."); - - var scriptPath = Path.Combine(updatesDir, "apply_wauncher_update.cmd"); - var script = BuildSelfUpdateScript(stagedExePath, currentExePath); - File.WriteAllText(scriptPath, script, Encoding.ASCII); - - Process.Start(new ProcessStartInfo - { - FileName = "cmd.exe", - Arguments = $"/c \"{scriptPath}\"", - WorkingDirectory = updatesDir, - CreateNoWindow = true, - UseShellExecute = false, - }); - - vm.UpdateStatusFile = "Restarting Wauncher to apply update..."; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = 100; - await Task.Delay(500, token); - ForceQuit(); - } - catch (OperationCanceledException) - { - vm.UpdateStatusFile = "Update cancelled."; - vm.UpdateStatusSpeed = ""; - await Task.Delay(800); - } - catch (Exception ex) - { - vm.UpdateStatusFile = $"Self-update failed: {ex.Message}"; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = false; - await Task.Delay(2500); - } - finally - { - if (!_forceClose) - { - vm.IsUpdating = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - _updateCts?.Dispose(); - _updateCts = null; - - try - { - await CheckForUpdatesAsync(); - } - catch - { - // keep UI responsive even if refresh fails - } - } - - Interlocked.Exchange(ref _updateInProgress, 0); - } - } - - private async Task CheckForUpdatesAsync() - { - if (DataContext is not MainWindowViewModel vm) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - - if (vm.IsOfflineMode) - { - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - _cachedPatches = null; - vm.UpdateAvailable = false; - vm.IsCheckingUpdates = false; - var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = launchColor; - ArrowButton.Background = launchColor; - LaunchUpdateButton.IsEnabled = true; - return; - } - - vm.IsCheckingUpdates = true; - LaunchUpdateButton.Background = new SolidColorBrush(Color.Parse("#555555")); - ArrowButton.Background = new SolidColorBrush(Color.Parse("#555555")); - LaunchUpdateButton.IsEnabled = false; - try - { - bool hasSelfUpdate = await CheckForSelfUpdateAsync(); - if (hasSelfUpdate) - { - _cachedPatches = null; - vm.UpdateAvailable = true; - var selfUpdateColor = new SolidColorBrush(Color.Parse("#FFC107")); - LaunchUpdateButton.Background = selfUpdateColor; - ArrowButton.Background = selfUpdateColor; - - if (Interlocked.Exchange(ref _autoSelfUpdateTriggered, 1) == 0) - { - _ = Button_SelfUpdateAsync(); - } - - return; - } - - if (_settings.SkipUpdates) - { - _cachedPatches = null; - vm.UpdateAvailable = false; - var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = launchColor; - ArrowButton.Background = launchColor; - return; - } - - var patches = await Task.Run(() => PatchManager.ValidatePatches(deleteOutdatedFiles: false)); - bool hasUpdates = patches.Missing.Count > 0 || patches.Outdated.Count > 0; - - // Cache the result so Button_Update can consume it without re-validating. - _cachedPatches = patches; - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - vm.UpdateAvailable = hasUpdates; - var buttonColor = new SolidColorBrush( - Color.Parse(hasUpdates ? "#FFC107" : "#4CAF50")); + return items; + } + + private sealed class GitHubRelease + { + [JsonProperty("tag_name")] + public string? TagName { get; set; } + + [JsonProperty("assets")] + public List? Assets { get; set; } + } + + private sealed class GitHubReleaseAsset + { + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("browser_download_url")] + public string DownloadUrl { get; set; } = string.Empty; + } + + private static string NormalizeVersionToken(string? version) + { + if (string.IsNullOrWhiteSpace(version)) + return "0.0.0"; + + var cleaned = version.Trim(); + if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + cleaned = cleaned[1..]; + + cleaned = Regex.Replace(cleaned, @"[^0-9\.]", string.Empty); + return string.IsNullOrWhiteSpace(cleaned) ? "0.0.0" : cleaned; + } + + private static bool TryParseVersion(string value, out global::System.Version parsed) + { + if (global::System.Version.TryParse(value, out parsed!)) + return true; + + var tokens = value.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 0) + { + parsed = new global::System.Version(0, 0, 0); + return false; + } + + while (tokens.Length < 3) + tokens = tokens.Append("0").ToArray(); + + return global::System.Version.TryParse(string.Join('.', tokens.Take(4)), out parsed!); + } + + private async Task CheckForSelfUpdateAsync() + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + + try + { + var latestReleaseJson = await Api.GitHub.GetLatestRelease(); + var release = JsonConvert.DeserializeObject(latestReleaseJson); + if (release == null) + return false; + + var currentVersion = NormalizeVersionToken(Wauncher.Utils.Version.Current); + var latestVersion = NormalizeVersionToken(release.TagName); + if (!TryParseVersion(currentVersion, out var current) || !TryParseVersion(latestVersion, out var latest)) + return false; + + if (latest <= current) + return false; + + var assets = release.Assets ?? new List(); + var preferred = assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + string.Equals(a.Name, "wauncher.exe", StringComparison.OrdinalIgnoreCase)); + + preferred ??= assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + a.Name.Contains("wauncher", StringComparison.OrdinalIgnoreCase) && + a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); + + if (preferred == null) + return false; + + _selfUpdateAvailable = true; + _selfUpdateDownloadUrl = preferred.DownloadUrl; + _selfUpdateVersion = latestVersion; + return true; + } + catch + { + return false; + } + } + + private async Task DownloadFileWithProgressAsync(string url, string destination, Action? onProgress, CancellationToken token) + { + using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); + response.EnsureSuccessStatusCode(); + + var totalBytes = response.Content.Headers.ContentLength; + await using var input = await response.Content.ReadAsStreamAsync(token); + await using var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true); + + var buffer = new byte[81920]; + long received = 0; + while (true) + { + int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), token); + if (read == 0) + break; + + await output.WriteAsync(buffer.AsMemory(0, read), token); + received += read; + if (totalBytes.HasValue && totalBytes.Value > 0) + { + onProgress?.Invoke((double)received / totalBytes.Value * 100.0); + } + } + + onProgress?.Invoke(100.0); + } + + private static string BuildSelfUpdateScript(string stagedExePath, string currentExePath) + { + return +$@"@echo off +setlocal +set ""SRC={stagedExePath}"" +set ""DST={currentExePath}"" + +for /L %%i in (1,1,60) do ( + copy /Y ""%SRC%"" ""%DST%"" >nul 2>nul && goto copied + timeout /t 1 /nobreak >nul +) + +exit /b 1 + +:copied +start """" ""%DST%"" +del /Q ""%SRC%"" >nul 2>nul +del /Q ""%~f0"" >nul 2>nul +exit /b 0 +"; + } + + private async Task Button_SelfUpdateAsync() + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) + return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = "Downloading Wauncher update..."; + vm.UpdateStatusSpeed = ""; + + try + { + if (string.IsNullOrWhiteSpace(_selfUpdateDownloadUrl)) + throw new Exception("No self-update package URL found."); + + var updatesDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "self-update"); + Directory.CreateDirectory(updatesDir); + + var safeVersion = Regex.Replace(_selfUpdateVersion, @"[^0-9A-Za-z\.\-_]", string.Empty); + if (string.IsNullOrWhiteSpace(safeVersion)) + safeVersion = "latest"; + + var stagedExePath = Path.Combine(updatesDir, $"wauncher_{safeVersion}.exe"); + await DownloadFileWithProgressAsync(_selfUpdateDownloadUrl, stagedExePath, percent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateProgress = percent; + vm.UpdateStatusFile = $"Downloading Wauncher update... {percent:F0}%"; + }); + }, token); + + var currentExePath = Services.GetExePath(); + if (string.IsNullOrWhiteSpace(currentExePath)) + throw new Exception("Could not locate current Wauncher executable."); + + var scriptPath = Path.Combine(updatesDir, "apply_wauncher_update.cmd"); + var script = BuildSelfUpdateScript(stagedExePath, currentExePath); + File.WriteAllText(scriptPath, script, Encoding.ASCII); + + Process.Start(new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/c \"{scriptPath}\"", + WorkingDirectory = updatesDir, + CreateNoWindow = true, + UseShellExecute = false, + }); + + vm.UpdateStatusFile = "Restarting Wauncher to apply update..."; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 100; + await Task.Delay(500, token); + ForceQuit(); + } + catch (OperationCanceledException) + { + vm.UpdateStatusFile = "Update cancelled."; + vm.UpdateStatusSpeed = ""; + await Task.Delay(800); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Self-update failed: {ex.Message}"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + await Task.Delay(2500); + } + finally + { + if (!_forceClose) + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch + { + // keep UI responsive even if refresh fails + } + } + + Interlocked.Exchange(ref _updateInProgress, 0); + } + } + + private async Task CheckForUpdatesAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsOfflineMode) + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + _cachedPatches = null; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + LaunchUpdateButton.IsEnabled = true; + return; + } + + vm.IsCheckingUpdates = true; + LaunchUpdateButton.Background = new SolidColorBrush(Color.Parse("#555555")); + ArrowButton.Background = new SolidColorBrush(Color.Parse("#555555")); + LaunchUpdateButton.IsEnabled = false; + try + { + bool hasSelfUpdate = await CheckForSelfUpdateAsync(); + if (hasSelfUpdate) + { + _cachedPatches = null; + vm.UpdateAvailable = true; + var selfUpdateColor = new SolidColorBrush(Color.Parse("#FFC107")); + LaunchUpdateButton.Background = selfUpdateColor; + ArrowButton.Background = selfUpdateColor; + + if (Interlocked.Exchange(ref _autoSelfUpdateTriggered, 1) == 0) + { + _ = Button_SelfUpdateAsync(); + } + + return; + } + + if (_settings.SkipUpdates) + { + _cachedPatches = null; + vm.UpdateAvailable = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + return; + } + + var patches = await Task.Run(() => PatchManager.ValidatePatches(deleteOutdatedFiles: false)); + bool hasUpdates = patches.Missing.Count > 0 || patches.Outdated.Count > 0; + + // Cache the result so Button_Update can consume it without re-validating. + _cachedPatches = patches; + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + vm.UpdateAvailable = hasUpdates; + var buttonColor = new SolidColorBrush( + Color.Parse(hasUpdates ? "#FFC107" : "#4CAF50")); LaunchUpdateButton.Background = buttonColor; ArrowButton.Background = buttonColor; } @@ -1223,30 +1223,30 @@ private async Task CheckForUpdatesAsync() } } - private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) return; - - if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) - return; - - _updateCts?.Dispose(); - _updateCts = new CancellationTokenSource(); - var token = _updateCts.Token; - vm.IsUpdating = true; - vm.UpdateAvailable = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; + private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; vm.UpdateStatus = ""; try { // Use the result already computed by CheckForUpdatesAsync when available, // to avoid a redundant full validation on every update click. - bool validateAll = _forceValidateAllOnce; - _forceValidateAllOnce = false; - var patches = _cachedPatches ?? await Task.Run(() => PatchManager.ValidatePatches(validateAll: validateAll), token); + bool validateAll = _forceValidateAllOnce; + _forceValidateAllOnce = false; + var patches = _cachedPatches ?? await Task.Run(() => PatchManager.ValidatePatches(validateAll: validateAll), token); _cachedPatches = null; // consumed — force fresh check next time if (token.IsCancellationRequested) return; @@ -1264,44 +1264,44 @@ private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEv int totalFiles = allPatches.Count; int completed = 0; - foreach (var patch in allPatches) - { - if (token.IsCancellationRequested) break; - - var extractWatch = new System.Diagnostics.Stopwatch(); - await DownloadManager.DownloadPatch( - patch, - onProgress: (p) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; - vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); - vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; - }); - }, - onExtract: () => - { - extractWatch.Restart(); - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); - vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; - }); - }); + foreach (var patch in allPatches) + { + if (token.IsCancellationRequested) break; + + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); completed++; vm.UpdateProgress = (double)completed / totalFiles * 100.0; @@ -1328,27 +1328,27 @@ await DownloadManager.DownloadPatch( vm.UpdateIndeterminate = false; await Task.Delay(3000); } - finally - { - vm.IsUpdating = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - _cachedPatches = null; - _updateCts?.Dispose(); - _updateCts = null; - - try - { - await CheckForUpdatesAsync(); - } - catch { } - - Interlocked.Exchange(ref _updateInProgress, 0); - } - } + finally + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _cachedPatches = null; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch { } + + Interlocked.Exchange(ref _updateInProgress, 0); + } + } private void Button_CancelUpdate(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { @@ -1361,79 +1361,79 @@ private void FriendsTab_Click(object? sender, Avalonia.Interactivity.RoutedEvent if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "Friends"; } - private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "PatchNotes"; - PatchNotesScroll.Offset = new Vector(0, 0); - } - - private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (sender is not MenuItem { Tag: FriendInfo friend }) - return; - - var profileId = ResolveProfileSteamId(friend.SteamId); - if (string.IsNullOrWhiteSpace(profileId)) - return; - - try - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = $"https://eddies.cc/profiles/{profileId}", - UseShellExecute = true - }); - } - catch - { - // Best-effort open. - } - } - - private static string ResolveProfileSteamId(string? steamId) - { - if (string.IsNullOrWhiteSpace(steamId)) - return string.Empty; - - var value = steamId.Trim(); - if (ulong.TryParse(value, out _)) - return value; - - if (TryConvertSteamId2To64(value, out var steamId64)) - return steamId64.ToString(); - - return string.Empty; - } - - private static bool TryConvertSteamId2To64(string steamId2, out ulong steamId64) - { - steamId64 = 0; - var match = Regex.Match(steamId2, @"^STEAM_[0-5]:([0-1]):(\d+)$", RegexOptions.IgnoreCase); - if (!match.Success) - return false; - - if (!ulong.TryParse(match.Groups[1].Value, out var y)) - return false; - if (!ulong.TryParse(match.Groups[2].Value, out var z)) - return false; - - steamId64 = 76561197960265728UL + (z * 2UL) + y; - return true; - } - - private void Button_Settings(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { + private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "PatchNotes"; + PatchNotesScroll.Offset = new Vector(0, 0); + } + + private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: FriendInfo friend }) + return; + + var profileId = ResolveProfileSteamId(friend.SteamId); + if (string.IsNullOrWhiteSpace(profileId)) + return; + + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = $"https://eddies.cc/profiles/{profileId}", + UseShellExecute = true + }); + } + catch + { + // Best-effort open. + } + } + + private static string ResolveProfileSteamId(string? steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) + return string.Empty; + + var value = steamId.Trim(); + if (ulong.TryParse(value, out _)) + return value; + + if (TryConvertSteamId2To64(value, out var steamId64)) + return steamId64.ToString(); + + return string.Empty; + } + + private static bool TryConvertSteamId2To64(string steamId2, out ulong steamId64) + { + steamId64 = 0; + var match = Regex.Match(steamId2, @"^STEAM_[0-5]:([0-1]):(\d+)$", RegexOptions.IgnoreCase); + if (!match.Success) + return false; + + if (!ulong.TryParse(match.Groups[1].Value, out var y)) + return false; + if (!ulong.TryParse(match.Groups[2].Value, out var z)) + return false; + + steamId64 = 76561197960265728UL + (z * 2UL) + y; + return true; + } + + private void Button_Settings(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { if (_settingsWindow == null) { - _settingsWindow = new SettingsWindow(); - _settingsWindow.Closed += (s, e) => - { - _settingsWindow = null; - _settings = SettingsWindowViewModel.LoadGlobal(); - _ = CheckForUpdatesAsync(); - }; - _settingsWindow.Show(this); - } + _settingsWindow = new SettingsWindow(); + _settingsWindow.Closed += (s, e) => + { + _settingsWindow = null; + _settings = SettingsWindowViewModel.LoadGlobal(); + _ = CheckForUpdatesAsync(); + }; + _settingsWindow.Show(this); + } else _settingsWindow.Activate(); } @@ -1449,114 +1449,114 @@ private void Button_Info(object? sender, Avalonia.Interactivity.RoutedEventArgs } // ── Launch button glow + color ──────────────────────────────────────────── - private void SetLaunchGlow(bool updating) - { - var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); - LaunchUpdateButton.Background = brush; - ArrowButton.Background = brush; - LaunchButtonGlow.BoxShadow = updating - ? BoxShadows.Parse("0 0 18 2 #55FFC107") - : BoxShadows.Parse("0 0 18 2 #554CAF50"); - } - - private static string ShortFileName(string path) - { - if (string.IsNullOrWhiteSpace(path)) - return path; - - var normalized = path.Replace('\\', '/'); - if (normalized.Length <= 42) - return normalized; - - var fileName = Path.GetFileName(normalized); - if (fileName.Length <= 30) - return fileName; - - return fileName[..27] + "..."; - } - - private static string FormatDownloadSpeedAndEta(object progressArgs) - { - double speedBytes = 0; - if (TryGetDoubleProperty(progressArgs, "AverageBytesPerSecondSpeed", out var avg) && avg > 0) - speedBytes = avg; - else if (TryGetDoubleProperty(progressArgs, "BytesPerSecondSpeed", out var cur) && cur > 0) - speedBytes = cur; - - var speedText = speedBytes > 0 - ? $"{speedBytes / 1024.0 / 1024.0:F1} MB/s" - : ""; - - if (speedBytes <= 0 || - !TryGetLongProperty(progressArgs, "TotalBytesToReceive", out var totalBytes) || - !TryGetLongProperty(progressArgs, "ReceivedBytesSize", out var receivedBytes) || - totalBytes <= 0 || receivedBytes < 0 || receivedBytes >= totalBytes) - { - return speedText; - } - - var remainingBytes = totalBytes - receivedBytes; - var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); - var etaText = $"ETA {FormatEta(eta)}"; - - return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; - } - - private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) - { - if (watch == null || !watch.IsRunning || percent <= 1.0) - return ""; - - var elapsed = watch.Elapsed.TotalSeconds; - var total = elapsed / (percent / 100.0); - var remaining = Math.Max(0, total - elapsed); - return $"ETA {FormatEta(TimeSpan.FromSeconds(remaining))}"; - } - - private static string FormatEta(TimeSpan eta) - { - if (eta.TotalHours >= 1) - return eta.ToString(@"hh\:mm\:ss"); - return eta.ToString(@"mm\:ss"); - } - - private static bool TryGetDoubleProperty(object obj, string propertyName, out double value) - { - value = 0; - var prop = obj.GetType().GetProperty(propertyName); - if (prop == null) return false; - var raw = prop.GetValue(obj); - if (raw == null) return false; - try - { - value = Convert.ToDouble(raw); - return true; - } - catch - { - return false; - } - } - - private static bool TryGetLongProperty(object obj, string propertyName, out long value) - { - value = 0; - var prop = obj.GetType().GetProperty(propertyName); - if (prop == null) return false; - var raw = prop.GetValue(obj); - if (raw == null) return false; - try - { - value = Convert.ToInt64(raw); - return true; - } - catch - { - return false; - } - } - - } -} + private void SetLaunchGlow(bool updating) + { + var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); + LaunchUpdateButton.Background = brush; + ArrowButton.Background = brush; + LaunchButtonGlow.BoxShadow = updating + ? BoxShadows.Parse("0 0 18 2 #55FFC107") + : BoxShadows.Parse("0 0 18 2 #554CAF50"); + } + + private static string ShortFileName(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return path; + + var normalized = path.Replace('\\', '/'); + if (normalized.Length <= 42) + return normalized; + + var fileName = Path.GetFileName(normalized); + if (fileName.Length <= 30) + return fileName; + + return fileName[..27] + "..."; + } + + private static string FormatDownloadSpeedAndEta(object progressArgs) + { + double speedBytes = 0; + if (TryGetDoubleProperty(progressArgs, "AverageBytesPerSecondSpeed", out var avg) && avg > 0) + speedBytes = avg; + else if (TryGetDoubleProperty(progressArgs, "BytesPerSecondSpeed", out var cur) && cur > 0) + speedBytes = cur; + + var speedText = speedBytes > 0 + ? $"{speedBytes / 1024.0 / 1024.0:F1} MB/s" + : ""; + + if (speedBytes <= 0 || + !TryGetLongProperty(progressArgs, "TotalBytesToReceive", out var totalBytes) || + !TryGetLongProperty(progressArgs, "ReceivedBytesSize", out var receivedBytes) || + totalBytes <= 0 || receivedBytes < 0 || receivedBytes >= totalBytes) + { + return speedText; + } + + var remainingBytes = totalBytes - receivedBytes; + var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); + var etaText = $"ETA {FormatEta(eta)}"; + + return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; + } + + private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) + { + if (watch == null || !watch.IsRunning || percent <= 1.0) + return ""; + + var elapsed = watch.Elapsed.TotalSeconds; + var total = elapsed / (percent / 100.0); + var remaining = Math.Max(0, total - elapsed); + return $"ETA {FormatEta(TimeSpan.FromSeconds(remaining))}"; + } + + private static string FormatEta(TimeSpan eta) + { + if (eta.TotalHours >= 1) + return eta.ToString(@"hh\:mm\:ss"); + return eta.ToString(@"mm\:ss"); + } + + private static bool TryGetDoubleProperty(object obj, string propertyName, out double value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToDouble(raw); + return true; + } + catch + { + return false; + } + } + + private static bool TryGetLongProperty(object obj, string propertyName, out long value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToInt64(raw); + return true; + } + catch + { + return false; + } + } + + } +} From b8aeec2bdbd728448fe5fa403307d174af4113f7 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:00:50 -0400 Subject: [PATCH 12/51] fix protocol command compile error --- Wauncher/ViewModels/MainWindowViewModel.cs | 1132 ++++++++++---------- 1 file changed, 566 insertions(+), 566 deletions(-) diff --git a/Wauncher/ViewModels/MainWindowViewModel.cs b/Wauncher/ViewModels/MainWindowViewModel.cs index 919db26..7459c1b 100644 --- a/Wauncher/ViewModels/MainWindowViewModel.cs +++ b/Wauncher/ViewModels/MainWindowViewModel.cs @@ -1,568 +1,568 @@ -using Avalonia.Threading; -using CommunityToolkit.Mvvm.ComponentModel; -using Wauncher.Utils; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Linq; -using System.Net.NetworkInformation; -using System.Text; -using System.Threading; -using FriendInfo = Wauncher.Utils.FriendInfo; - -namespace Wauncher.ViewModels -{ - public class ServerInfo : INotifyPropertyChanged - { - public event PropertyChangedEventHandler? PropertyChanged; - - public string Name { get; set; } = ""; - public string IpPort { get; set; } = ""; - - private int _players; - private int _maxPlayers; - private bool _isOnline; - private string _map = ""; - - public int Players - { - get => _players; - set - { - if (_players == value) return; - _players = value; - Notify(nameof(Players), nameof(PlayerCount)); - } - } - - public int MaxPlayers - { - get => _maxPlayers; - set - { - if (_maxPlayers == value) return; - _maxPlayers = value; - Notify(nameof(MaxPlayers), nameof(PlayerCount)); - } - } - - public bool IsOnline - { - get => _isOnline; - set - { - if (_isOnline == value) return; - _isOnline = value; - Notify(nameof(IsOnline), nameof(DotColor)); - } - } - - public string Map - { - get => _map; - set - { - if (_map == value) return; - _map = value; - Notify(nameof(Map), nameof(MapDisplay)); - } - } - - public bool IsNone => string.IsNullOrEmpty(IpPort); - - public string PlayerCount => IsNone ? "" : $"{Players}/{MaxPlayers}"; - public string DotColor => IsNone ? "Transparent" : (IsOnline ? "#4CAF50" : "#F44336"); - public string NameColor => IsNone ? "#66FFFFFF" : "White"; - public string MapDisplay => (!IsNone && !string.IsNullOrEmpty(Map)) ? Map : ""; - - private void Notify(params string[] names) - { - Dispatcher.UIThread.Post(() => - { - foreach (var name in names) - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); - }); - } - } - - public partial class MainWindowViewModel : ViewModelBase - { - [ObservableProperty] - private string _gameStatus = "Not Running"; - - [ObservableProperty] - private string _protocolManager = "None"; - - [ObservableProperty] - private string _profilePicture = "https://avatars.githubusercontent.com/u/75831703?v=4"; - - [ObservableProperty] - private string _usernameGreeting = "Hello, username"; - - [ObservableProperty] - private string _whitelistDotColor = "Gray"; - - [ObservableProperty] - private string _whitelistText = "Unknown"; - - [ObservableProperty] - private bool _isDropdownOpen = false; - - [ObservableProperty] - private string _activeRightTab = "Friends"; - - public bool IsFriendsTabActive => ActiveRightTab == "Friends"; - public bool IsPatchNotesTabActive => ActiveRightTab == "PatchNotes"; - - partial void OnActiveRightTabChanged(string value) - { - OnPropertyChanged(nameof(IsFriendsTabActive)); - OnPropertyChanged(nameof(IsPatchNotesTabActive)); - } - - [ObservableProperty] - private bool _isOfflineMode = false; - - public bool IsOnlineMode => !IsOfflineMode; - partial void OnIsOfflineModeChanged(bool value) => OnPropertyChanged(nameof(IsOnlineMode)); - - [ObservableProperty] - private bool _isUpdating = false; - - [ObservableProperty] - private bool _isInstalling = false; - - [ObservableProperty] - private bool _isNeedingInstall = false; - - [ObservableProperty] - private bool _isCheckingUpdates = false; - - public bool IsCheckingOrUpdating => IsCheckingUpdates || IsUpdating || IsInstalling; - public bool IsUpdatingOrInstalling => IsUpdating || IsInstalling; - - partial void OnIsCheckingUpdatesChanged(bool value) => OnPropertyChanged(nameof(IsCheckingOrUpdating)); - partial void OnIsInstallingChanged(bool value) - { - OnPropertyChanged(nameof(LaunchButtonText)); - OnPropertyChanged(nameof(IsCheckingOrUpdating)); - OnPropertyChanged(nameof(IsUpdatingOrInstalling)); - } - partial void OnIsNeedingInstallChanged(bool value) => OnPropertyChanged(nameof(LaunchButtonText)); - - [ObservableProperty] - private string _updateStatus = ""; - - [ObservableProperty] - private string _updateStatusFile = ""; - - [ObservableProperty] - private string _updateStatusSpeed = ""; - - [ObservableProperty] - private double _updateProgress = 0; - - [ObservableProperty] - private bool _updateIndeterminate = false; - - [ObservableProperty] - private bool _updateAvailable = false; - - public string LaunchButtonText => - IsInstalling ? "Installing Game..." : - IsUpdating ? "Updating..." : - IsNeedingInstall ? "Install Game" : - UpdateAvailable ? "Update" : - "Launch Game"; - - partial void OnIsUpdatingChanged(bool value) - { - OnPropertyChanged(nameof(LaunchButtonText)); - OnPropertyChanged(nameof(IsCheckingOrUpdating)); - OnPropertyChanged(nameof(IsUpdatingOrInstalling)); - } - partial void OnUpdateAvailableChanged(bool value) => OnPropertyChanged(nameof(LaunchButtonText)); - - [ObservableProperty] - private ServerInfo? _selectedServer; - - // What the SELECTED SERVER label shows - public string SelectedLabel => SelectedServer?.IsNone == false - ? SelectedServer.Name - : "Server not selected..."; - - public bool IsNoServerSelected => SelectedServer == null || SelectedServer.IsNone; - public bool IsServerSelected => SelectedServer != null && !SelectedServer.IsNone; - - public ObservableCollection Servers { get; } = new() - { - // ── None (clears selection) ────────────────────────────────────── - new ServerInfo { Name = "None", IpPort = "", IsOnline = false }, - - // ── Real servers ───────────────────────────────────────────────── - new ServerInfo { Name = "NA | PUG | 64 Tick", IpPort = "na.classiccounter.cc:27015", Players = 0, MaxPlayers = 10, IsOnline = true }, - new ServerInfo { Name = "NA | PUG-2 | 64 Tick", IpPort = "na.classiccounter.cc:27016", Players = 0, MaxPlayers = 10, IsOnline = true }, - new ServerInfo { Name = "EU | PUG | 64 Tick", IpPort = "eu.classiccounter.cc:27016", Players = 0, MaxPlayers = 10, IsOnline = true }, - new ServerInfo { Name = "EU | PUG | 128 Tick", IpPort = "eu.classiccounter.cc:27015", Players = 0, MaxPlayers = 10, IsOnline = true }, - new ServerInfo { Name = "EU | PUG-2 | 128 Tick",IpPort = "eu.classiccounter.cc:27022", Players = 0, MaxPlayers = 10, IsOnline = true }, - }; - - partial void OnSelectedServerChanged(ServerInfo? value) - { - // Update the label shown in the trigger button - ProtocolManager = (value == null || value.IsNone) ? "None" : value.Name; - OnPropertyChanged(nameof(SelectedLabel)); - OnPropertyChanged(nameof(IsNoServerSelected)); - OnPropertyChanged(nameof(IsServerSelected)); - } - - public MainWindowViewModel() - { - if (Argument.Exists("--protocol-command")) - ProtocolManager = "Ready to Launch!"; - - _ = LoadSelfProfileAsync(); - - CheckWhitelistStatus(); - UpdateOfflineMode(); - NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; - - // Query servers immediately, then refresh every 30 seconds - _ = RefreshServersSafeAsync(); - _serverRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; - _serverRefreshTimer.Tick += async (_, _) => await RefreshServersSafeAsync(); - _serverRefreshTimer.Start(); - - // Query friends immediately, then refresh every 30 seconds - _ = RefreshFriendsSafeAsync(); - _friendsTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; - _friendsTimer.Tick += async (_, _) => await RefreshFriendsSafeAsync(); - _friendsTimer.Start(); - } - - private DispatcherTimer? _serverRefreshTimer; - private int _serverRefreshInProgress; - - // ── Friends ─────────────────────────────────────────────────────────────── - public ObservableCollection Friends { get; } = new(); - - [ObservableProperty] private bool _friendsShowStatus = true; - [ObservableProperty] private string _friendsStatus = "Loading..."; - [ObservableProperty] private bool _showNoFriendsState = false; - public bool ShowGenericFriendsStatus => FriendsShowStatus && !ShowNoFriendsState; - - partial void OnFriendsShowStatusChanged(bool value) => OnPropertyChanged(nameof(ShowGenericFriendsStatus)); - partial void OnShowNoFriendsStateChanged(bool value) => OnPropertyChanged(nameof(ShowGenericFriendsStatus)); - - private DispatcherTimer? _friendsTimer; - private int _friendsRefreshInProgress; - private string _lastRenderedFriendsSignature = string.Empty; - private string _lastKnownSteamId2 = string.Empty; - - private async Task LoadSelfProfileAsync() - { - try - { - bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); - if (!hasSteam || string.IsNullOrWhiteSpace(Steam.recentSteamID64)) - return; - - var rawSelfJson = await Api.Eddies.GetSelfInfo(Steam.recentSteamID64); - var self = Api.ParseSelfInfoPayload(rawSelfJson); - if (self == null) - return; - - Dispatcher.UIThread.Post(() => - { - if (!string.IsNullOrWhiteSpace(self.AvatarUrl)) - ProfilePicture = AvatarCache.GetDisplaySource(self.AvatarUrl); - - if (!string.IsNullOrWhiteSpace(self.Username)) - UsernameGreeting = $"Hello, {self.Username}"; - }); - } - catch - { - // Best-effort profile load; keep defaults on failure. - } - } - - private async Task RefreshServersAsync() - { - if (IsOfflineMode) - { - foreach (var s in Servers.Where(s => !s.IsNone)) - { - s.IsOnline = false; - s.Players = 0; - s.MaxPlayers = 0; - s.Map = ""; - } - return; - } - - await ServerQuery.RefreshServers(Servers.Where(s => !s.IsNone)); - - // Re-order by player count descending; None always stays at index 0 - var sorted = Servers.Where(s => !s.IsNone) - .OrderByDescending(s => s.Players) - .ToList(); - int insertAt = 1; - foreach (var server in sorted) - { - int from = Servers.IndexOf(server); - if (from != insertAt) - Servers.Move(from, insertAt); - insertAt++; - } - } - - private async Task RefreshServersSafeAsync() - { - if (Interlocked.Exchange(ref _serverRefreshInProgress, 1) == 1) - return; - - try - { - await RefreshServersAsync(); - } - finally - { - Interlocked.Exchange(ref _serverRefreshInProgress, 0); - } - } - - private async Task RefreshFriendsAsync() - { - try - { - if (IsOfflineMode) - { - var steamIdForCache = !string.IsNullOrWhiteSpace(_lastKnownSteamId2) - ? _lastKnownSteamId2 - : (Steam.recentSteamID2 ?? string.Empty); - - if (TryShowCachedFriends(steamIdForCache, forceOfflineStatus: true)) - return; - - Dispatcher.UIThread.Post(() => - { - Friends.Clear(); - _lastRenderedFriendsSignature = string.Empty; - ShowNoFriendsState = false; - FriendsStatus = "Offline mode"; - FriendsShowStatus = true; - }); - return; - } - - bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); - string steamId = Steam.recentSteamID2 ?? string.Empty; - if (!string.IsNullOrWhiteSpace(steamId)) - _lastKnownSteamId2 = steamId; - - if (!hasSteam) - { - Dispatcher.UIThread.Post(() => - { - ShowNoFriendsState = false; - FriendsStatus = "Steam is not installed."; - FriendsShowStatus = true; - }); - return; - } - - if (string.IsNullOrEmpty(Steam.recentSteamID2)) - { - Dispatcher.UIThread.Post(() => - { - ShowNoFriendsState = false; - FriendsStatus = "Sign in to Steam to see friends."; - FriendsShowStatus = true; - }); - return; - } - - string rawFriendsJson; - try - { - rawFriendsJson = await Api.Eddies.GetFriends(Steam.recentSteamID64 ?? string.Empty); - } - catch - { - rawFriendsJson = await Api.Eddies.GetFriendsBySteamId2(Steam.recentSteamID2 ?? string.Empty); - } - var apiFriends = Api.ParseFriendsPayload(rawFriendsJson) - .OrderBy(f => f.Status == "Offline" ? 1 : 0) - .ToList(); - - await FriendsCache.SaveAsync(steamId, apiFriends); - - Dispatcher.UIThread.Post(() => - { - var sorted = apiFriends; - - foreach (var f in sorted) - f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); - - var signature = BuildFriendsSignature(sorted); - if (!string.Equals(signature, _lastRenderedFriendsSignature, StringComparison.Ordinal)) - { - Friends.Clear(); - foreach (var f in sorted) - Friends.Add(f); - _lastRenderedFriendsSignature = signature; - } - - FriendsShowStatus = Friends.Count == 0; - ShowNoFriendsState = Friends.Count == 0; - FriendsStatus = Friends.Count == 0 ? "No friends found." : ""; - }); - } - catch - { - if (TryShowCachedFriends(Steam.recentSteamID2 ?? string.Empty, forceOfflineStatus: true)) - return; - - Dispatcher.UIThread.Post(() => - { - Friends.Clear(); - _lastRenderedFriendsSignature = string.Empty; - ShowNoFriendsState = false; - FriendsStatus = IsOfflineMode ? "Offline mode" : "Couldn't load friends right now."; - FriendsShowStatus = true; - }); - } - } - - private bool TryShowCachedFriends(string steamId, bool forceOfflineStatus) - { - var cached = FriendsCache.Load(steamId); - if (cached.Count == 0) - return false; - - var sorted = cached - .OrderBy(f => f.Status == "Offline" ? 1 : 0) - .ToList(); - - if (forceOfflineStatus) - { - foreach (var f in sorted) - f.Status = "Offline"; - } - - foreach (var f in sorted) - f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); - - Dispatcher.UIThread.Post(() => - { - var signature = BuildFriendsSignature(sorted); - if (!string.Equals(signature, _lastRenderedFriendsSignature, StringComparison.Ordinal)) - { - Friends.Clear(); - foreach (var f in sorted) - Friends.Add(f); - _lastRenderedFriendsSignature = signature; - } - - FriendsShowStatus = false; - ShowNoFriendsState = false; - FriendsStatus = ""; - }); - - return true; - } - - private static string BuildFriendsSignature(IEnumerable friends) - { - var sb = new StringBuilder(); - foreach (var f in friends) - { - sb.Append(f.Username ?? string.Empty) - .Append('\u001f') - .Append(f.AvatarUrl ?? string.Empty) - .Append('\u001f') - .Append(f.Status ?? "Offline") - .Append('\u001e'); - } - return sb.ToString(); - } - - private async Task RefreshFriendsSafeAsync() - { - if (Interlocked.Exchange(ref _friendsRefreshInProgress, 1) == 1) - return; - - try - { - await RefreshFriendsAsync(); - } - finally - { - Interlocked.Exchange(ref _friendsRefreshInProgress, 0); - } - } - - - - - - private void CheckWhitelistStatus() - { - Task.Run(async () => - { - try - { - bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); - if (!hasSteam) - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - WhitelistDotColor = "Gray"; - WhitelistText = "Unknown"; - }); - return; - } - - if (string.IsNullOrEmpty(Steam.recentSteamID2)) - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - WhitelistDotColor = "Gray"; - WhitelistText = "Unknown"; - }); - return; - } - - var response = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); - bool whitelisted = response?.Files != null && response.Files.Count > 0; - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - WhitelistDotColor = whitelisted ? "#4CAF50" : "#F44336"; - WhitelistText = whitelisted ? "Whitelisted" : "Not Whitelisted"; - }); - } - catch - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - WhitelistDotColor = "Gray"; - WhitelistText = "Unknown"; - }); - } - }); - } - - private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) - { - Dispatcher.UIThread.Post(UpdateOfflineMode); - } - - private void UpdateOfflineMode() - { - IsOfflineMode = !NetworkInterface.GetIsNetworkAvailable(); - } - } -} +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using Wauncher.Utils; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Net.NetworkInformation; +using System.Text; +using System.Threading; +using FriendInfo = Wauncher.Utils.FriendInfo; + +namespace Wauncher.ViewModels +{ + public class ServerInfo : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + + public string Name { get; set; } = ""; + public string IpPort { get; set; } = ""; + + private int _players; + private int _maxPlayers; + private bool _isOnline; + private string _map = ""; + + public int Players + { + get => _players; + set + { + if (_players == value) return; + _players = value; + Notify(nameof(Players), nameof(PlayerCount)); + } + } + + public int MaxPlayers + { + get => _maxPlayers; + set + { + if (_maxPlayers == value) return; + _maxPlayers = value; + Notify(nameof(MaxPlayers), nameof(PlayerCount)); + } + } + + public bool IsOnline + { + get => _isOnline; + set + { + if (_isOnline == value) return; + _isOnline = value; + Notify(nameof(IsOnline), nameof(DotColor)); + } + } + + public string Map + { + get => _map; + set + { + if (_map == value) return; + _map = value; + Notify(nameof(Map), nameof(MapDisplay)); + } + } + + public bool IsNone => string.IsNullOrEmpty(IpPort); + + public string PlayerCount => IsNone ? "" : $"{Players}/{MaxPlayers}"; + public string DotColor => IsNone ? "Transparent" : (IsOnline ? "#4CAF50" : "#F44336"); + public string NameColor => IsNone ? "#66FFFFFF" : "White"; + public string MapDisplay => (!IsNone && !string.IsNullOrEmpty(Map)) ? Map : ""; + + private void Notify(params string[] names) + { + Dispatcher.UIThread.Post(() => + { + foreach (var name in names) + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + }); + } + } + + public partial class MainWindowViewModel : ViewModelBase + { + [ObservableProperty] + private string _gameStatus = "Not Running"; + + [ObservableProperty] + private string _protocolManager = "None"; + + [ObservableProperty] + private string _profilePicture = "https://avatars.githubusercontent.com/u/75831703?v=4"; + + [ObservableProperty] + private string _usernameGreeting = "Hello, username"; + + [ObservableProperty] + private string _whitelistDotColor = "Gray"; + + [ObservableProperty] + private string _whitelistText = "Unknown"; + + [ObservableProperty] + private bool _isDropdownOpen = false; + + [ObservableProperty] + private string _activeRightTab = "Friends"; + + public bool IsFriendsTabActive => ActiveRightTab == "Friends"; + public bool IsPatchNotesTabActive => ActiveRightTab == "PatchNotes"; + + partial void OnActiveRightTabChanged(string value) + { + OnPropertyChanged(nameof(IsFriendsTabActive)); + OnPropertyChanged(nameof(IsPatchNotesTabActive)); + } + + [ObservableProperty] + private bool _isOfflineMode = false; + + public bool IsOnlineMode => !IsOfflineMode; + partial void OnIsOfflineModeChanged(bool value) => OnPropertyChanged(nameof(IsOnlineMode)); + + [ObservableProperty] + private bool _isUpdating = false; + + [ObservableProperty] + private bool _isInstalling = false; + + [ObservableProperty] + private bool _isNeedingInstall = false; + + [ObservableProperty] + private bool _isCheckingUpdates = false; + + public bool IsCheckingOrUpdating => IsCheckingUpdates || IsUpdating || IsInstalling; + public bool IsUpdatingOrInstalling => IsUpdating || IsInstalling; + + partial void OnIsCheckingUpdatesChanged(bool value) => OnPropertyChanged(nameof(IsCheckingOrUpdating)); + partial void OnIsInstallingChanged(bool value) + { + OnPropertyChanged(nameof(LaunchButtonText)); + OnPropertyChanged(nameof(IsCheckingOrUpdating)); + OnPropertyChanged(nameof(IsUpdatingOrInstalling)); + } + partial void OnIsNeedingInstallChanged(bool value) => OnPropertyChanged(nameof(LaunchButtonText)); + + [ObservableProperty] + private string _updateStatus = ""; + + [ObservableProperty] + private string _updateStatusFile = ""; + + [ObservableProperty] + private string _updateStatusSpeed = ""; + + [ObservableProperty] + private double _updateProgress = 0; + + [ObservableProperty] + private bool _updateIndeterminate = false; + + [ObservableProperty] + private bool _updateAvailable = false; + + public string LaunchButtonText => + IsInstalling ? "Installing Game..." : + IsUpdating ? "Updating..." : + IsNeedingInstall ? "Install Game" : + UpdateAvailable ? "Update" : + "Launch Game"; + + partial void OnIsUpdatingChanged(bool value) + { + OnPropertyChanged(nameof(LaunchButtonText)); + OnPropertyChanged(nameof(IsCheckingOrUpdating)); + OnPropertyChanged(nameof(IsUpdatingOrInstalling)); + } + partial void OnUpdateAvailableChanged(bool value) => OnPropertyChanged(nameof(LaunchButtonText)); + + [ObservableProperty] + private ServerInfo? _selectedServer; + + // What the SELECTED SERVER label shows + public string SelectedLabel => SelectedServer?.IsNone == false + ? SelectedServer.Name + : "Server not selected..."; + + public bool IsNoServerSelected => SelectedServer == null || SelectedServer.IsNone; + public bool IsServerSelected => SelectedServer != null && !SelectedServer.IsNone; + + public ObservableCollection Servers { get; } = new() + { + // ── None (clears selection) ────────────────────────────────────── + new ServerInfo { Name = "None", IpPort = "", IsOnline = false }, + + // ── Real servers ───────────────────────────────────────────────── + new ServerInfo { Name = "NA | PUG | 64 Tick", IpPort = "na.classiccounter.cc:27015", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "NA | PUG-2 | 64 Tick", IpPort = "na.classiccounter.cc:27016", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "EU | PUG | 64 Tick", IpPort = "eu.classiccounter.cc:27016", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "EU | PUG | 128 Tick", IpPort = "eu.classiccounter.cc:27015", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "EU | PUG-2 | 128 Tick",IpPort = "eu.classiccounter.cc:27022", Players = 0, MaxPlayers = 10, IsOnline = true }, + }; + + partial void OnSelectedServerChanged(ServerInfo? value) + { + // Update the label shown in the trigger button + ProtocolManager = (value == null || value.IsNone) ? "None" : value.Name; + OnPropertyChanged(nameof(SelectedLabel)); + OnPropertyChanged(nameof(IsNoServerSelected)); + OnPropertyChanged(nameof(IsServerSelected)); + } + + public MainWindowViewModel() + { + if (Argument.HasProtocolCommand()) + ProtocolManager = "Ready to Launch!"; + + _ = LoadSelfProfileAsync(); + + CheckWhitelistStatus(); + UpdateOfflineMode(); + NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; + + // Query servers immediately, then refresh every 30 seconds + _ = RefreshServersSafeAsync(); + _serverRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; + _serverRefreshTimer.Tick += async (_, _) => await RefreshServersSafeAsync(); + _serverRefreshTimer.Start(); + + // Query friends immediately, then refresh every 30 seconds + _ = RefreshFriendsSafeAsync(); + _friendsTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; + _friendsTimer.Tick += async (_, _) => await RefreshFriendsSafeAsync(); + _friendsTimer.Start(); + } + + private DispatcherTimer? _serverRefreshTimer; + private int _serverRefreshInProgress; + + // ── Friends ─────────────────────────────────────────────────────────────── + public ObservableCollection Friends { get; } = new(); + + [ObservableProperty] private bool _friendsShowStatus = true; + [ObservableProperty] private string _friendsStatus = "Loading..."; + [ObservableProperty] private bool _showNoFriendsState = false; + public bool ShowGenericFriendsStatus => FriendsShowStatus && !ShowNoFriendsState; + + partial void OnFriendsShowStatusChanged(bool value) => OnPropertyChanged(nameof(ShowGenericFriendsStatus)); + partial void OnShowNoFriendsStateChanged(bool value) => OnPropertyChanged(nameof(ShowGenericFriendsStatus)); + + private DispatcherTimer? _friendsTimer; + private int _friendsRefreshInProgress; + private string _lastRenderedFriendsSignature = string.Empty; + private string _lastKnownSteamId2 = string.Empty; + + private async Task LoadSelfProfileAsync() + { + try + { + bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); + if (!hasSteam || string.IsNullOrWhiteSpace(Steam.recentSteamID64)) + return; + + var rawSelfJson = await Api.Eddies.GetSelfInfo(Steam.recentSteamID64); + var self = Api.ParseSelfInfoPayload(rawSelfJson); + if (self == null) + return; + + Dispatcher.UIThread.Post(() => + { + if (!string.IsNullOrWhiteSpace(self.AvatarUrl)) + ProfilePicture = AvatarCache.GetDisplaySource(self.AvatarUrl); + + if (!string.IsNullOrWhiteSpace(self.Username)) + UsernameGreeting = $"Hello, {self.Username}"; + }); + } + catch + { + // Best-effort profile load; keep defaults on failure. + } + } + + private async Task RefreshServersAsync() + { + if (IsOfflineMode) + { + foreach (var s in Servers.Where(s => !s.IsNone)) + { + s.IsOnline = false; + s.Players = 0; + s.MaxPlayers = 0; + s.Map = ""; + } + return; + } + + await ServerQuery.RefreshServers(Servers.Where(s => !s.IsNone)); + + // Re-order by player count descending; None always stays at index 0 + var sorted = Servers.Where(s => !s.IsNone) + .OrderByDescending(s => s.Players) + .ToList(); + int insertAt = 1; + foreach (var server in sorted) + { + int from = Servers.IndexOf(server); + if (from != insertAt) + Servers.Move(from, insertAt); + insertAt++; + } + } + + private async Task RefreshServersSafeAsync() + { + if (Interlocked.Exchange(ref _serverRefreshInProgress, 1) == 1) + return; + + try + { + await RefreshServersAsync(); + } + finally + { + Interlocked.Exchange(ref _serverRefreshInProgress, 0); + } + } + + private async Task RefreshFriendsAsync() + { + try + { + if (IsOfflineMode) + { + var steamIdForCache = !string.IsNullOrWhiteSpace(_lastKnownSteamId2) + ? _lastKnownSteamId2 + : (Steam.recentSteamID2 ?? string.Empty); + + if (TryShowCachedFriends(steamIdForCache, forceOfflineStatus: true)) + return; + + Dispatcher.UIThread.Post(() => + { + Friends.Clear(); + _lastRenderedFriendsSignature = string.Empty; + ShowNoFriendsState = false; + FriendsStatus = "Offline mode"; + FriendsShowStatus = true; + }); + return; + } + + bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); + string steamId = Steam.recentSteamID2 ?? string.Empty; + if (!string.IsNullOrWhiteSpace(steamId)) + _lastKnownSteamId2 = steamId; + + if (!hasSteam) + { + Dispatcher.UIThread.Post(() => + { + ShowNoFriendsState = false; + FriendsStatus = "Steam is not installed."; + FriendsShowStatus = true; + }); + return; + } + + if (string.IsNullOrEmpty(Steam.recentSteamID2)) + { + Dispatcher.UIThread.Post(() => + { + ShowNoFriendsState = false; + FriendsStatus = "Sign in to Steam to see friends."; + FriendsShowStatus = true; + }); + return; + } + + string rawFriendsJson; + try + { + rawFriendsJson = await Api.Eddies.GetFriends(Steam.recentSteamID64 ?? string.Empty); + } + catch + { + rawFriendsJson = await Api.Eddies.GetFriendsBySteamId2(Steam.recentSteamID2 ?? string.Empty); + } + var apiFriends = Api.ParseFriendsPayload(rawFriendsJson) + .OrderBy(f => f.Status == "Offline" ? 1 : 0) + .ToList(); + + await FriendsCache.SaveAsync(steamId, apiFriends); + + Dispatcher.UIThread.Post(() => + { + var sorted = apiFriends; + + foreach (var f in sorted) + f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); + + var signature = BuildFriendsSignature(sorted); + if (!string.Equals(signature, _lastRenderedFriendsSignature, StringComparison.Ordinal)) + { + Friends.Clear(); + foreach (var f in sorted) + Friends.Add(f); + _lastRenderedFriendsSignature = signature; + } + + FriendsShowStatus = Friends.Count == 0; + ShowNoFriendsState = Friends.Count == 0; + FriendsStatus = Friends.Count == 0 ? "No friends found." : ""; + }); + } + catch + { + if (TryShowCachedFriends(Steam.recentSteamID2 ?? string.Empty, forceOfflineStatus: true)) + return; + + Dispatcher.UIThread.Post(() => + { + Friends.Clear(); + _lastRenderedFriendsSignature = string.Empty; + ShowNoFriendsState = false; + FriendsStatus = IsOfflineMode ? "Offline mode" : "Couldn't load friends right now."; + FriendsShowStatus = true; + }); + } + } + + private bool TryShowCachedFriends(string steamId, bool forceOfflineStatus) + { + var cached = FriendsCache.Load(steamId); + if (cached.Count == 0) + return false; + + var sorted = cached + .OrderBy(f => f.Status == "Offline" ? 1 : 0) + .ToList(); + + if (forceOfflineStatus) + { + foreach (var f in sorted) + f.Status = "Offline"; + } + + foreach (var f in sorted) + f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); + + Dispatcher.UIThread.Post(() => + { + var signature = BuildFriendsSignature(sorted); + if (!string.Equals(signature, _lastRenderedFriendsSignature, StringComparison.Ordinal)) + { + Friends.Clear(); + foreach (var f in sorted) + Friends.Add(f); + _lastRenderedFriendsSignature = signature; + } + + FriendsShowStatus = false; + ShowNoFriendsState = false; + FriendsStatus = ""; + }); + + return true; + } + + private static string BuildFriendsSignature(IEnumerable friends) + { + var sb = new StringBuilder(); + foreach (var f in friends) + { + sb.Append(f.Username ?? string.Empty) + .Append('\u001f') + .Append(f.AvatarUrl ?? string.Empty) + .Append('\u001f') + .Append(f.Status ?? "Offline") + .Append('\u001e'); + } + return sb.ToString(); + } + + private async Task RefreshFriendsSafeAsync() + { + if (Interlocked.Exchange(ref _friendsRefreshInProgress, 1) == 1) + return; + + try + { + await RefreshFriendsAsync(); + } + finally + { + Interlocked.Exchange(ref _friendsRefreshInProgress, 0); + } + } + + + + + + private void CheckWhitelistStatus() + { + Task.Run(async () => + { + try + { + bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); + if (!hasSteam) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = "Gray"; + WhitelistText = "Unknown"; + }); + return; + } + + if (string.IsNullOrEmpty(Steam.recentSteamID2)) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = "Gray"; + WhitelistText = "Unknown"; + }); + return; + } + + var response = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); + bool whitelisted = response?.Files != null && response.Files.Count > 0; + + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = whitelisted ? "#4CAF50" : "#F44336"; + WhitelistText = whitelisted ? "Whitelisted" : "Not Whitelisted"; + }); + } + catch + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = "Gray"; + WhitelistText = "Unknown"; + }); + } + }); + } + + private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) + { + Dispatcher.UIThread.Post(UpdateOfflineMode); + } + + private void UpdateOfflineMode() + { + IsOfflineMode = !NetworkInterface.GetIsNetworkAvailable(); + } + } +} From 5da4031464162fb55fb6380c22c89d63c1e84ab0 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:09:32 -0400 Subject: [PATCH 13/51] show verify progress during file scan --- Wauncher/Views/MainWindow.axaml.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index 1d91f31..166f043 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -1246,6 +1246,18 @@ private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEv // to avoid a redundant full validation on every update click. bool validateAll = _forceValidateAllOnce; _forceValidateAllOnce = false; + bool usingCachedPatches = _cachedPatches != null; + + if (!usingCachedPatches) + { + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = validateAll + ? "Verifying all game files..." + : "Checking game files..."; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 0; + } + var patches = _cachedPatches ?? await Task.Run(() => PatchManager.ValidatePatches(validateAll: validateAll), token); _cachedPatches = null; // consumed — force fresh check next time if (token.IsCancellationRequested) return; From 35bd63c74413ba1fc376f9fe1c3f46404a92f809 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:22:53 -0400 Subject: [PATCH 14/51] make publish.bat wauncher-only --- publish.bat | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/publish.bat b/publish.bat index e3789b1..a2baac1 100644 --- a/publish.bat +++ b/publish.bat @@ -1,15 +1,29 @@ @echo off for /f %%a in ('echo prompt $E^| cmd') do set "ESC=%%a" +setlocal + echo ============================= -echo %ESC%[42mBuilding...%ESC%[0m -dotnet publish Launcher -c Release -dotnet publish Wauncher -c Release +echo %ESC%[42mBuilding Wauncher...%ESC%[0m +dotnet publish Wauncher\Wauncher.csproj -c Release -r win-x64 --self-contained false +if errorlevel 1 ( + echo Publish failed. + exit /b 1 +) + echo ============================= -echo %ESC%[41mHashing...%ESC%[0m -certutil -hashfile "Launcher\bin\Release\net8.0-windows7.0\win-x64\publish\launcher.exe" MD5 +echo %ESC%[41mHashing wauncher.exe...%ESC%[0m certutil -hashfile "Wauncher\bin\Release\net8.0-windows7.0\win-x64\publish\wauncher.exe" MD5 + echo ============================= -echo %ESC%[1;43mCopying...%ESC%[0m -set /p "destination=Copying destination (in quotations): " -xcopy "Wauncher\bin\Release\net8.0-windows7.0\win-x64\publish\" %destination% /e /y -timeout /t 5 \ No newline at end of file +echo %ESC%[1;43mCopying Wauncher publish output...%ESC%[0m +set "defaultDest=C:\Games\ClassicCounter" +set /p "destination=Destination folder (Enter for default: C:\Games\ClassicCounter): " +if "%destination%"=="" set "destination=%defaultDest%" + +if not exist "%destination%" ( + mkdir "%destination%" +) + +xcopy "Wauncher\bin\Release\net8.0-windows7.0\win-x64\publish\*" "%destination%\" /e /y /i >nul +echo Copied to: %destination% +timeout /t 3 >nul From ed36d6fa0f74aeaeb30ce1808eae4c44c52a6c41 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:36:29 -0400 Subject: [PATCH 15/51] add wauncher build/publish docs --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index cceb4b1..2d7c344 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ - Validation behavior is controlled in the GUI. - Use `Verify Game Files` from the launch button drop-up menu when you want a full file check. +## Build / Publish +- Build: `dotnet build Wauncher/Wauncher.csproj -c Release` +- Publish: `dotnet publish Wauncher/Wauncher.csproj -c Release -r win-x64 --self-contained false` +- Quick publish script: `publish.bat` (builds + hashes + optional copy target) + ## Packages Used - [CSGSI](https://github.com/rakijah/CSGSI) by [rakijah](https://github.com/rakijah) - [DiscordRichPresence](https://github.com/Lachee/discord-rpc-csharp) by [Lachee](https://github.com/Lachee) From 464d9e34349f4544693e70b9be8e2b71376a936c Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:44:04 -0400 Subject: [PATCH 16/51] update readme packages and known issues --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 2d7c344..d5627e7 100644 --- a/README.md +++ b/README.md @@ -19,18 +19,30 @@ - Validation behavior is controlled in the GUI. - Use `Verify Game Files` from the launch button drop-up menu when you want a full file check. +## Known Issues +- Friends list currently cannot show when a friend is online and actively in-game. + ## Build / Publish - Build: `dotnet build Wauncher/Wauncher.csproj -c Release` - Publish: `dotnet publish Wauncher/Wauncher.csproj -c Release -r win-x64 --self-contained false` - Quick publish script: `publish.bat` (builds + hashes + optional copy target) ## Packages Used +- [AsyncImageLoader.Avalonia](https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia) by [AvaloniaUtils](https://github.com/AvaloniaUtils) +- [Avalonia](https://github.com/AvaloniaUI/Avalonia) by [AvaloniaUI](https://github.com/AvaloniaUI) +- [Avalonia.Desktop](https://github.com/AvaloniaUI/Avalonia) by [AvaloniaUI](https://github.com/AvaloniaUI) +- [Avalonia.Themes.Fluent](https://github.com/AvaloniaUI/Avalonia) by [AvaloniaUI](https://github.com/AvaloniaUI) +- [Avalonia.Fonts.Inter](https://github.com/AvaloniaUI/Avalonia) by [AvaloniaUI](https://github.com/AvaloniaUI) +- [Avalonia.Diagnostics](https://github.com/AvaloniaUI/Avalonia) by [AvaloniaUI](https://github.com/AvaloniaUI) +- [CommunityToolkit.Mvvm](https://github.com/CommunityToolkit/dotnet) by [CommunityToolkit](https://github.com/CommunityToolkit) - [CSGSI](https://github.com/rakijah/CSGSI) by [rakijah](https://github.com/rakijah) - [DiscordRichPresence](https://github.com/Lachee/discord-rpc-csharp) by [Lachee](https://github.com/Lachee) - [Downloader](https://github.com/bezzad/Downloader) by [bezzad](https://github.com/bezzad) - [Gameloop.Vdf](https://github.com/shravan2x/Gameloop.Vdf) by [shravan2x](https://github.com/shravan2x) - [Refit](https://github.com/reactiveui/refit) by [ReactiveUI](https://github.com/reactiveui) +- [Refit.Newtonsoft.Json](https://github.com/reactiveui/refit) by [ReactiveUI](https://github.com/reactiveui) - [Spectre.Console](https://github.com/spectreconsole/spectre.console) by [Spectre Console](https://github.com/spectreconsole) +- [Svg.Controls.Skia.Avalonia](https://github.com/wieslawsoltes/Svg.Skia) by [wieslawsoltes](https://github.com/wieslawsoltes) [downloads-shield]: https://img.shields.io/github/downloads/classiccounter/launcher/total.svg?style=for-the-badge [downloads-url]: https://github.com/classiccounter/launcher/releases/latest From c7b1b5d1eb220c3c3d385bb4fceea65ac69db241 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:15:49 -0400 Subject: [PATCH 17/51] show verify progress and restore launch options --- Wauncher/Views/MainWindow.axaml.cs | 2589 ++++++++++++++-------------- 1 file changed, 1314 insertions(+), 1275 deletions(-) diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index 166f043..ccb29be 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -1,64 +1,64 @@ -using System.IO; -using System.Net.Http; -using System.Linq; -using System.Net.NetworkInformation; -using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Text.Json; -using System.Text; -using System.Text.RegularExpressions; -using Avalonia.Animation; -using Avalonia.Animation.Easings; -using Avalonia; +using System.IO; +using System.Net.Http; +using System.Linq; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Text.Json; +using System.Text; +using System.Text.RegularExpressions; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Threading; -using Wauncher.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Wauncher.ViewModels; -using Wauncher.Views; +using Avalonia.Platform; +using Avalonia.Threading; +using Wauncher.Utils; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Wauncher.ViewModels; +using Wauncher.Views; namespace Wauncher.Views { - public partial class MainWindow : Window - { - private InfoWindow? _infoWindow = null; - private SettingsWindow? _settingsWindow = null; - private SettingsWindowViewModel _settings; - private int _launchInProgress; - private int _updateInProgress; - private int _installInProgress; - - private bool _dropdownOpen = false; + public partial class MainWindow : Window + { + private InfoWindow? _infoWindow = null; + private SettingsWindow? _settingsWindow = null; + private SettingsWindowViewModel _settings; + private int _launchInProgress; + private int _updateInProgress; + private int _installInProgress; + + private bool _dropdownOpen = false; private const double HeightClosed = 720; private const double HeightOpen = 720; // ── Image carousel (center content area) ────────────────────────────────── - private Image[] _carouselImages = Array.Empty(); - private DispatcherTimer? _carouselTimer; - private int _currentCarouselIndex = 0; - private const int CarouselRotationIntervalSeconds = 5; - private readonly List _zoomCts = new(); - private static string WauncherDirectory => - Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); + private Image[] _carouselImages = Array.Empty(); + private DispatcherTimer? _carouselTimer; + private int _currentCarouselIndex = 0; + private const int CarouselRotationIntervalSeconds = 5; + private readonly List _zoomCts = new(); + private static string WauncherDirectory => + Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); public MainWindow() { InitializeComponent(); _settings = SettingsWindowViewModel.LoadGlobal(); - this.Loaded += (_, _) => - { - var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = buttonColor; - ArrowButton.Background = buttonColor; - LaunchUpdateButton.IsEnabled = true; - }; + this.Loaded += (_, _) => + { + var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = buttonColor; + ArrowButton.Background = buttonColor; + LaunchUpdateButton.IsEnabled = true; + }; this.Opened += (_, _) => { @@ -93,72 +93,72 @@ public MainWindow() this.Closed += (_, _) => TeardownCarousel(); } - // ── Image carousel (center content area) ────────────────────────────────── - private static readonly HttpClient _http = new(); - private static string PatchNotesCachePath => - Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "cache", - "patchnotes.md"); - - private async Task SetupCarouselAsync() - { - try - { - TeardownCarousel(); - - var carouselContainer = this.FindControl("CarouselContainer"); - var offlinePanel = this.FindControl("CarouselOfflinePanel"); - var offlineSubText = this.FindControl("CarouselOfflineSubText"); - if (carouselContainer == null) - return; - - bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); - var bitmaps = hasInternet - ? await LoadCarouselFromGitHubAsync() - : null; - - if (bitmaps == null || bitmaps.Count == 0) - { - if (offlinePanel != null) - offlinePanel.IsVisible = true; - if (offlineSubText != null) - { - offlineSubText.Text = hasInternet - ? "Carousel is temporarily unavailable." - : "Connect to Wi-Fi or Ethernet to load the carousel."; - } - return; - } - - if (offlinePanel != null) - offlinePanel.IsVisible = false; - - _carouselImages = CreateCarouselImages(bitmaps); - EnsureZoomSlots(_carouselImages.Length); - - foreach (var existingImage in carouselContainer.Children.OfType().ToList()) - carouselContainer.Children.Remove(existingImage); - - int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; - for (int i = 0; i < _carouselImages.Length; i++) - { - if (overlayIndex >= 0) - { - carouselContainer.Children.Insert(overlayIndex, _carouselImages[i]); - overlayIndex++; - } - else - { - carouselContainer.Children.Add(_carouselImages[i]); - } - } - - _currentCarouselIndex = 0; - _carouselImages[0].Opacity = 1.0; - StartZoomOut(_carouselImages[0], 0); + // ── Image carousel (center content area) ────────────────────────────────── + private static readonly HttpClient _http = new(); + private static string PatchNotesCachePath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache", + "patchnotes.md"); + + private async Task SetupCarouselAsync() + { + try + { + TeardownCarousel(); + + var carouselContainer = this.FindControl("CarouselContainer"); + var offlinePanel = this.FindControl("CarouselOfflinePanel"); + var offlineSubText = this.FindControl("CarouselOfflineSubText"); + if (carouselContainer == null) + return; + + bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); + var bitmaps = hasInternet + ? await LoadCarouselFromGitHubAsync() + : null; + + if (bitmaps == null || bitmaps.Count == 0) + { + if (offlinePanel != null) + offlinePanel.IsVisible = true; + if (offlineSubText != null) + { + offlineSubText.Text = hasInternet + ? "Carousel is temporarily unavailable." + : "Connect to Wi-Fi or Ethernet to load the carousel."; + } + return; + } + + if (offlinePanel != null) + offlinePanel.IsVisible = false; + + _carouselImages = CreateCarouselImages(bitmaps); + EnsureZoomSlots(_carouselImages.Length); + + foreach (var existingImage in carouselContainer.Children.OfType().ToList()) + carouselContainer.Children.Remove(existingImage); + + int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; + for (int i = 0; i < _carouselImages.Length; i++) + { + if (overlayIndex >= 0) + { + carouselContainer.Children.Insert(overlayIndex, _carouselImages[i]); + overlayIndex++; + } + else + { + carouselContainer.Children.Add(_carouselImages[i]); + } + } + + _currentCarouselIndex = 0; + _carouselImages[0].Opacity = 1.0; + StartZoomOut(_carouselImages[0], 0); _carouselTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CarouselRotationIntervalSeconds) }; _carouselTimer.Tick += (_, _) => RotateCarousel(); @@ -170,35 +170,35 @@ private async Task SetupCarouselAsync() } } - private async Task?> LoadCarouselFromGitHubAsync() - { - try - { - var json = await Api.GitHub.GetCarouselAssetsWauncher(); - var assets = JsonConvert.DeserializeObject>(json); - if (assets == null || assets.Count == 0) - return null; - - var urls = assets - .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) - .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) - .OrderBy(a => GetCarouselSortIndex(a.Name)) - .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) - .Select(a => a.DownloadUrl!) - .ToList(); - - if (urls.Count == 0) - return null; - - var bitmaps = new List(); - foreach (var url in urls) - { - try + private async Task?> LoadCarouselFromGitHubAsync() + { + try + { + var json = await Api.GitHub.GetCarouselAssetsWauncher(); + var assets = JsonConvert.DeserializeObject>(json); + if (assets == null || assets.Count == 0) + return null; + + var urls = assets + .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) + .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) + .OrderBy(a => GetCarouselSortIndex(a.Name)) + .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) + .Select(a => a.DownloadUrl!) + .ToList(); + + if (urls.Count == 0) + return null; + + var bitmaps = new List(); + foreach (var url in urls) + { + try { var bytes = await _http.GetByteArrayAsync(url); using var ms = new MemoryStream(bytes); @@ -207,84 +207,84 @@ private async Task SetupCarouselAsync() catch { } } return bitmaps.Count > 0 ? bitmaps : null; - } - catch { return null; } - } - - private static int GetCarouselSortIndex(string name) - { - var match = Regex.Match(name, @"^carousel_(\d+)", RegexOptions.IgnoreCase); - if (match.Success && int.TryParse(match.Groups[1].Value, out var index)) - return index; - return int.MaxValue; - } - - private sealed class GitHubAssetEntry - { - [JsonProperty("name")] - public string Name { get; set; } = string.Empty; - - [JsonProperty("type")] - public string Type { get; set; } = string.Empty; - - [JsonProperty("download_url")] - public string? DownloadUrl { get; set; } - } - - private static Image[] CreateCarouselImages(IReadOnlyList bitmaps) - { - var images = new Image[bitmaps.Count]; - for (int i = 0; i < bitmaps.Count; i++) - { - images[i] = new Image - { - Source = bitmaps[i], - Stretch = Stretch.UniformToFill, - Opacity = 0.0, - Transitions = new Transitions - { - new DoubleTransition - { - Property = Visual.OpacityProperty, - Duration = TimeSpan.FromSeconds(1.5), - Easing = new CubicEaseInOut() - } - } - }; - } - - return images; - } - - private void EnsureZoomSlots(int count) - { - while (_zoomCts.Count < count) - _zoomCts.Add(null); - } - - private void RotateCarousel() - { - if (_carouselImages.Length == 0) - return; - - // Fade out current image (zoom continues through the crossfade) - _carouselImages[_currentCarouselIndex].Opacity = 0.0; - - // Move to next image - _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; - - // Fade in next image and start fresh zoom-out - StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); - _carouselImages[_currentCarouselIndex].Opacity = 1.0; - } - - private void TeardownCarousel() - { - _carouselTimer?.Stop(); - _carouselTimer = null; - for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); - _carouselImages = Array.Empty(); - } + } + catch { return null; } + } + + private static int GetCarouselSortIndex(string name) + { + var match = Regex.Match(name, @"^carousel_(\d+)", RegexOptions.IgnoreCase); + if (match.Success && int.TryParse(match.Groups[1].Value, out var index)) + return index; + return int.MaxValue; + } + + private sealed class GitHubAssetEntry + { + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("type")] + public string Type { get; set; } = string.Empty; + + [JsonProperty("download_url")] + public string? DownloadUrl { get; set; } + } + + private static Image[] CreateCarouselImages(IReadOnlyList bitmaps) + { + var images = new Image[bitmaps.Count]; + for (int i = 0; i < bitmaps.Count; i++) + { + images[i] = new Image + { + Source = bitmaps[i], + Stretch = Stretch.UniformToFill, + Opacity = 0.0, + Transitions = new Transitions + { + new DoubleTransition + { + Property = Visual.OpacityProperty, + Duration = TimeSpan.FromSeconds(1.5), + Easing = new CubicEaseInOut() + } + } + }; + } + + return images; + } + + private void EnsureZoomSlots(int count) + { + while (_zoomCts.Count < count) + _zoomCts.Add(null); + } + + private void RotateCarousel() + { + if (_carouselImages.Length == 0) + return; + + // Fade out current image (zoom continues through the crossfade) + _carouselImages[_currentCarouselIndex].Opacity = 0.0; + + // Move to next image + _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; + + // Fade in next image and start fresh zoom-out + StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); + _carouselImages[_currentCarouselIndex].Opacity = 1.0; + } + + private void TeardownCarousel() + { + _carouselTimer?.Stop(); + _carouselTimer = null; + for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); + _carouselImages = Array.Empty(); + } private void StartZoomOut(Image img, int slot) { @@ -314,28 +314,28 @@ private void StartZoomOut(Image img, int slot) zoomTimer.Start(); } - private void StopZoom(int slot) - { - if (slot < 0 || slot >= _zoomCts.Count) - return; - - _zoomCts[slot]?.Cancel(); - _zoomCts[slot] = null; - } + private void StopZoom(int slot) + { + if (slot < 0 || slot >= _zoomCts.Count) + return; + + _zoomCts[slot]?.Cancel(); + _zoomCts[slot] = null; + } // ── Server dropdown ─────────────────────────────────────────── - private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) - { - CloseDropdown(); - return; - } - - _dropdownOpen = !_dropdownOpen; - - if (DataContext is MainWindowViewModel vm) - vm.IsDropdownOpen = _dropdownOpen; + private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) + { + CloseDropdown(); + return; + } + + _dropdownOpen = !_dropdownOpen; + + if (DataContext is MainWindowViewModel vm) + vm.IsDropdownOpen = _dropdownOpen; if (_dropdownOpen) { @@ -367,49 +367,54 @@ private void CloseDropdown() } // ── Game launch ─────────────────────────────────────────── - private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - - if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || Volatile.Read(ref _launchInProgress) == 1) - return; - else if (vm.IsNeedingInstall) - _ = InstallGameFromCdnAsync(); - else if (_settings.SkipUpdates) - _ = LaunchGameAsync(); - else if (vm.UpdateAvailable) - { - if (_selfUpdateAvailable) - _ = Button_SelfUpdateAsync(); - else - Button_Update(sender, e); - } - else - _ = LaunchGameAsync(); - } - - private async Task LaunchGameAsync() - { - if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) - return; - - var vm = DataContext as MainWindowViewModel; - try - { - _settings = SettingsWindowViewModel.LoadGlobal(); - + private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || Volatile.Read(ref _launchInProgress) == 1) + return; + else if (vm.IsNeedingInstall) + _ = InstallGameFromCdnAsync(); + else if (_settings.SkipUpdates) + _ = LaunchGameAsync(); + else if (vm.UpdateAvailable) + { + if (_selfUpdateAvailable) + _ = Button_SelfUpdateAsync(); + else + Button_Update(sender, e); + } + else + _ = LaunchGameAsync(); + } + + private async Task LaunchGameAsync() + { + if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) + return; + + var vm = DataContext as MainWindowViewModel; + try + { + _settings = SettingsWindowViewModel.LoadGlobal(); + if (vm != null) vm.GameStatus = "Running"; - // Clear any arguments left over from a previous launch before adding new ones. - Argument.ClearAdditionalArguments(); - - Argument.AddArgument("-novid"); - - var selected = vm?.SelectedServer; - if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) - { + // Clear any arguments left over from a previous launch before adding new ones. + Argument.ClearAdditionalArguments(); + + Argument.AddArgument("-novid"); + if (!string.IsNullOrWhiteSpace(_settings.LaunchOptions)) + { + foreach (var arg in ParseLaunchOptions(_settings.LaunchOptions)) + Argument.AddArgument(arg); + } + + var selected = vm?.SelectedServer; + if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) + { Argument.AddArgument("+connect"); Argument.AddArgument(selected.IpPort); } @@ -427,16 +432,16 @@ private async Task LaunchGameAsync() await Game.Monitor(); } - catch (Exception ex) - { - Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); - } - finally - { - if (vm != null) vm.GameStatus = "Not Running"; - Interlocked.Exchange(ref _launchInProgress, 0); - } - } + catch (Exception ex) + { + Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); + } + finally + { + if (vm != null) vm.GameStatus = "Not Running"; + Interlocked.Exchange(ref _launchInProgress, 0); + } + } // ── Window chrome ─────────────────────────────────────────── private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) @@ -466,215 +471,215 @@ public void ForceQuit() Close(); } - private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var dir = WauncherDirectory; - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = dir, - UseShellExecute = true - }); - } - - private void VerifyGameFiles_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is not MainWindowViewModel vm) - return; - - if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || vm.IsNeedingInstall) - return; - - _forceValidateAllOnce = true; - _cachedPatches = null; - Button_Update(sender, e); - } - - // ── Update ───────────────────────────────────────────────────── - private CancellationTokenSource? _updateCts; - private Patches? _cachedPatches; - private bool _selfUpdateAvailable; - private string _selfUpdateDownloadUrl = string.Empty; - private string _selfUpdateVersion = string.Empty; - private int _autoSelfUpdateTriggered; - private bool _forceValidateAllOnce; + private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var dir = WauncherDirectory; + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = dir, + UseShellExecute = true + }); + } + + private void VerifyGameFiles_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is not MainWindowViewModel vm) + return; + + if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || vm.IsNeedingInstall) + return; + + _forceValidateAllOnce = true; + _cachedPatches = null; + Button_Update(sender, e); + } + + // ── Update ───────────────────────────────────────────────────── + private CancellationTokenSource? _updateCts; + private Patches? _cachedPatches; + private bool _selfUpdateAvailable; + private string _selfUpdateDownloadUrl = string.Empty; + private string _selfUpdateVersion = string.Empty; + private int _autoSelfUpdateTriggered; + private bool _forceValidateAllOnce; /// /// Called on window open. If csgo.exe is missing, triggers a full CDN install. /// Otherwise runs the normal patch update check. /// - private async Task StartupAsync() - { - // Yield to let Avalonia finish its initial layout/styling pass - // (Loaded sets the button disabled/gray; we need that to settle before overriding) - await Task.Delay(50); - - LaunchUpdateButton.IsEnabled = true; - - string csgoExe = Path.Combine(WauncherDirectory, "csgo.exe"); - if (DataContext is not MainWindowViewModel vm) - return; - - if (!File.Exists(csgoExe)) - { - vm.IsNeedingInstall = true; - var blue = new SolidColorBrush(Color.Parse("#2196F3")); - LaunchUpdateButton.Background = blue; - ArrowButton.Background = blue; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - if (vm?.IsOfflineMode == true) - { - vm.IsNeedingInstall = false; - vm.UpdateAvailable = false; - var green = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = green; - ArrowButton.Background = green; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - _settings = SettingsWindowViewModel.LoadGlobal(); - if (_settings.SkipUpdates) - { - vm!.IsNeedingInstall = false; - vm.UpdateAvailable = false; - vm.IsCheckingUpdates = false; - var green = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = green; - ArrowButton.Background = green; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - await CheckForUpdatesAsync(); - } - - private async Task InstallGameFromCdnAsync() - { - if (DataContext is not MainWindowViewModel vm) return; - if (Interlocked.Exchange(ref _installInProgress, 1) == 1) - return; - - bool installSucceeded = false; - vm.IsNeedingInstall = false; - vm.IsInstalling = true; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = "Connecting..."; + private async Task StartupAsync() + { + // Yield to let Avalonia finish its initial layout/styling pass + // (Loaded sets the button disabled/gray; we need that to settle before overriding) + await Task.Delay(50); + + LaunchUpdateButton.IsEnabled = true; + + string csgoExe = Path.Combine(WauncherDirectory, "csgo.exe"); + if (DataContext is not MainWindowViewModel vm) + return; + + if (!File.Exists(csgoExe)) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + if (vm?.IsOfflineMode == true) + { + vm.IsNeedingInstall = false; + vm.UpdateAvailable = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + _settings = SettingsWindowViewModel.LoadGlobal(); + if (_settings.SkipUpdates) + { + vm!.IsNeedingInstall = false; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + await CheckForUpdatesAsync(); + } + + private async Task InstallGameFromCdnAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + if (Interlocked.Exchange(ref _installInProgress, 1) == 1) + return; + + bool installSucceeded = false; + vm.IsNeedingInstall = false; + vm.IsInstalling = true; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Connecting..."; vm.UpdateStatusSpeed = ""; - try - { - await DownloadManager.InstallFullGame( - onProgress: (file, speed, percent) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = $"Installing {ShortFileName(file)} {percent:F0}%"; - vm.UpdateStatusSpeed = string.IsNullOrWhiteSpace(speed) ? "" : speed; - vm.UpdateProgress = percent; - vm.UpdateIndeterminate = false; - }); - }, - onStatus: status => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = status; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); - if (!vm.UpdateIndeterminate) - vm.UpdateProgress = 0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = extractPercent; - }); - }); - - // Immediately apply any post-install patches so first-time installs - // end in a launch-ready state without requiring a second manual update. - Dispatcher.UIThread.Post(() => - { - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = "Checking post-install patches..."; - vm.UpdateStatusSpeed = ""; - }); - - var patches = await Task.Run(() => PatchManager.ValidatePatches()); - var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); - if (allPatches.Count > 0) - { - int totalFiles = allPatches.Count; - int completed = 0; - - foreach (var patch in allPatches) - { - var extractWatch = new System.Diagnostics.Stopwatch(); - await DownloadManager.DownloadPatch( - patch, - onProgress: (p) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; - vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); - vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; - }); - }, - onExtract: () => - { - extractWatch.Restart(); - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); - vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; - }); - }); - - completed++; - vm.UpdateProgress = (double)completed / totalFiles * 100.0; - } - } - - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = "Game installed and fully updated!"; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = false; - vm.UpdateProgress = 100; - }); - installSucceeded = true; - await Task.Delay(1500); - } - catch (Exception ex) - { - vm.UpdateStatusFile = $"Install error: {ex.Message}"; + try + { + await DownloadManager.InstallFullGame( + onProgress: (file, speed, percent) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = $"Installing {ShortFileName(file)} {percent:F0}%"; + vm.UpdateStatusSpeed = string.IsNullOrWhiteSpace(speed) ? "" : speed; + vm.UpdateProgress = percent; + vm.UpdateIndeterminate = false; + }); + }, + onStatus: status => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = status; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); + if (!vm.UpdateIndeterminate) + vm.UpdateProgress = 0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = extractPercent; + }); + }); + + // Immediately apply any post-install patches so first-time installs + // end in a launch-ready state without requiring a second manual update. + Dispatcher.UIThread.Post(() => + { + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Checking post-install patches..."; + vm.UpdateStatusSpeed = ""; + }); + + var patches = await Task.Run(() => PatchManager.ValidatePatches()); + var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); + if (allPatches.Count > 0) + { + int totalFiles = allPatches.Count; + int completed = 0; + + foreach (var patch in allPatches) + { + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); + + completed++; + vm.UpdateProgress = (double)completed / totalFiles * 100.0; + } + } + + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = "Game installed and fully updated!"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + vm.UpdateProgress = 100; + }); + installSucceeded = true; + await Task.Delay(1500); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Install error: {ex.Message}"; vm.UpdateStatusSpeed = ""; await Task.Delay(4000); } @@ -682,196 +687,196 @@ await DownloadManager.DownloadPatch( { vm.IsInstalling = false; vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - - _cachedPatches = null; - - if (!installSucceeded && !File.Exists(Path.Combine(WauncherDirectory, "csgo.exe"))) - { - vm.IsNeedingInstall = true; - var blue = new SolidColorBrush(Color.Parse("#2196F3")); - LaunchUpdateButton.Background = blue; - ArrowButton.Background = blue; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); - } - else - { - try - { - await CheckForUpdatesAsync(); - } - catch { } - } - - LaunchUpdateButton.IsEnabled = true; - Interlocked.Exchange(ref _installInProgress, 0); - } - } - - private async Task LoadPatchNotesAsync() - { - try - { - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Loading latest patch notes..."; - PatchNotesVersion.IsVisible = true; - }); - - if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) - { - Dispatcher.UIThread.Post(() => - { - var cachedItems = LoadCachedPatchNotes(); - if (cachedItems.Count > 0) - { - PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; - PatchNotesList.ItemsSource = cachedItems; - } - else - { - PatchNotesVersion.Text = "Patch notes are unavailable offline."; - PatchNotesList.ItemsSource = new List(); - } - - PatchNotesVersion.IsVisible = true; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - return; - } - - var md = await Api.GitHub.GetPatchNotesWauncher(); - var items = ParsePatchNotes(md); - SavePatchNotesCache(md); - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = items.Count > 0 - ? $"Updated {DateTime.Now:MMM d, h:mm tt}" - : "Patch notes are currently empty."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - } - catch - { - var items = LoadCachedPatchNotes(); - if (items.Count == 0) - { - items = BuildFallbackPatchNotes(); - } - - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Using fallback patch notes."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - } - } - - private static void SavePatchNotesCache(string markdown) - { - try - { - var directory = Path.GetDirectoryName(PatchNotesCachePath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(PatchNotesCachePath, markdown); - } - catch - { - // Caching is best-effort; keep patch notes functional if disk write fails. - } - } - - private static List LoadCachedPatchNotes() - { - try - { - if (!File.Exists(PatchNotesCachePath)) - { - return new List(); - } - - var markdown = File.ReadAllText(PatchNotesCachePath); - return ParsePatchNotes(markdown); - } - catch - { - return new List(); - } - } - - private static List BuildFallbackPatchNotes() - { - return new List - { - new() { Text = "Anniversary Update", IsMajorHeader = true }, - new() { Text = "What's Changed", IsHeader = true }, - new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, - new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, - new() { Text = "Bug fixes and security improvements.", IsBullet = true }, - }; - } - - private static List ParsePatchNotes(string markdown) - { - var items = new List(); - foreach (var raw in markdown.Split('\n')) - { - var line = raw.TrimEnd(); - if (string.IsNullOrWhiteSpace(line)) continue; - - line = line.Trim(); - line = line.Replace("**", "").Replace("__", ""); - line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); - line = Regex.Replace(line, @"`([^`]*)`", "$1"); - - if (line.StartsWith("# ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsMajorHeader = true - }); - } - else if (line.StartsWith("## ") || line.StartsWith("### ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsHeader = true + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + + _cachedPatches = null; + + if (!installSucceeded && !File.Exists(Path.Combine(WauncherDirectory, "csgo.exe"))) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + } + else + { + try + { + await CheckForUpdatesAsync(); + } + catch { } + } + + LaunchUpdateButton.IsEnabled = true; + Interlocked.Exchange(ref _installInProgress, 0); + } + } + + private async Task LoadPatchNotesAsync() + { + try + { + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Loading latest patch notes..."; + PatchNotesVersion.IsVisible = true; + }); + + if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) + { + Dispatcher.UIThread.Post(() => + { + var cachedItems = LoadCachedPatchNotes(); + if (cachedItems.Count > 0) + { + PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; + PatchNotesList.ItemsSource = cachedItems; + } + else + { + PatchNotesVersion.Text = "Patch notes are unavailable offline."; + PatchNotesList.ItemsSource = new List(); + } + + PatchNotesVersion.IsVisible = true; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + return; + } + + var md = await Api.GitHub.GetPatchNotesWauncher(); + var items = ParsePatchNotes(md); + SavePatchNotesCache(md); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = items.Count > 0 + ? $"Updated {DateTime.Now:MMM d, h:mm tt}" + : "Patch notes are currently empty."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + catch + { + var items = LoadCachedPatchNotes(); + if (items.Count == 0) + { + items = BuildFallbackPatchNotes(); + } + + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Using fallback patch notes."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + } + + private static void SavePatchNotesCache(string markdown) + { + try + { + var directory = Path.GetDirectoryName(PatchNotesCachePath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(PatchNotesCachePath, markdown); + } + catch + { + // Caching is best-effort; keep patch notes functional if disk write fails. + } + } + + private static List LoadCachedPatchNotes() + { + try + { + if (!File.Exists(PatchNotesCachePath)) + { + return new List(); + } + + var markdown = File.ReadAllText(PatchNotesCachePath); + return ParsePatchNotes(markdown); + } + catch + { + return new List(); + } + } + + private static List BuildFallbackPatchNotes() + { + return new List + { + new() { Text = "Anniversary Update", IsMajorHeader = true }, + new() { Text = "What's Changed", IsHeader = true }, + new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, + new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, + new() { Text = "Bug fixes and security improvements.", IsBullet = true }, + }; + } + + private static List ParsePatchNotes(string markdown) + { + var items = new List(); + foreach (var raw in markdown.Split('\n')) + { + var line = raw.TrimEnd(); + if (string.IsNullOrWhiteSpace(line)) continue; + + line = line.Trim(); + line = line.Replace("**", "").Replace("__", ""); + line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); + line = Regex.Replace(line, @"`([^`]*)`", "$1"); + + if (line.StartsWith("# ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsMajorHeader = true + }); + } + else if (line.StartsWith("## ") || line.StartsWith("### ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsHeader = true }); } - else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Substring(2).Trim(), - IsBullet = true - }); - } - else if (Regex.IsMatch(line, @"^\d+\.\s+")) - { - var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); - items.Add(new ViewModels.PatchNoteItem - { - Text = bulletText, - IsBullet = true - }); - } - else if (line.StartsWith("**") && line.EndsWith("**")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Trim('*', ' '), + else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Substring(2).Trim(), + IsBullet = true + }); + } + else if (Regex.IsMatch(line, @"^\d+\.\s+")) + { + var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); + items.Add(new ViewModels.PatchNoteItem + { + Text = bulletText, + IsBullet = true + }); + } + else if (line.StartsWith("**") && line.EndsWith("**")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Trim('*', ' '), IsHeader = true }); } @@ -884,329 +889,329 @@ private static void SavePatchNotesCache(string markdown) }); } } - return items; - } - - private sealed class GitHubRelease - { - [JsonProperty("tag_name")] - public string? TagName { get; set; } - - [JsonProperty("assets")] - public List? Assets { get; set; } - } - - private sealed class GitHubReleaseAsset - { - [JsonProperty("name")] - public string Name { get; set; } = string.Empty; - - [JsonProperty("browser_download_url")] - public string DownloadUrl { get; set; } = string.Empty; - } - - private static string NormalizeVersionToken(string? version) - { - if (string.IsNullOrWhiteSpace(version)) - return "0.0.0"; - - var cleaned = version.Trim(); - if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase)) - cleaned = cleaned[1..]; - - cleaned = Regex.Replace(cleaned, @"[^0-9\.]", string.Empty); - return string.IsNullOrWhiteSpace(cleaned) ? "0.0.0" : cleaned; - } - - private static bool TryParseVersion(string value, out global::System.Version parsed) - { - if (global::System.Version.TryParse(value, out parsed!)) - return true; - - var tokens = value.Split('.', StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length == 0) - { - parsed = new global::System.Version(0, 0, 0); - return false; - } - - while (tokens.Length < 3) - tokens = tokens.Append("0").ToArray(); - - return global::System.Version.TryParse(string.Join('.', tokens.Take(4)), out parsed!); - } - - private async Task CheckForSelfUpdateAsync() - { - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - - try - { - var latestReleaseJson = await Api.GitHub.GetLatestRelease(); - var release = JsonConvert.DeserializeObject(latestReleaseJson); - if (release == null) - return false; - - var currentVersion = NormalizeVersionToken(Wauncher.Utils.Version.Current); - var latestVersion = NormalizeVersionToken(release.TagName); - if (!TryParseVersion(currentVersion, out var current) || !TryParseVersion(latestVersion, out var latest)) - return false; - - if (latest <= current) - return false; - - var assets = release.Assets ?? new List(); - var preferred = assets.FirstOrDefault(a => - !string.IsNullOrWhiteSpace(a.DownloadUrl) && - string.Equals(a.Name, "wauncher.exe", StringComparison.OrdinalIgnoreCase)); - - preferred ??= assets.FirstOrDefault(a => - !string.IsNullOrWhiteSpace(a.DownloadUrl) && - a.Name.Contains("wauncher", StringComparison.OrdinalIgnoreCase) && - a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); - - if (preferred == null) - return false; - - _selfUpdateAvailable = true; - _selfUpdateDownloadUrl = preferred.DownloadUrl; - _selfUpdateVersion = latestVersion; - return true; - } - catch - { - return false; - } - } - - private async Task DownloadFileWithProgressAsync(string url, string destination, Action? onProgress, CancellationToken token) - { - using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); - response.EnsureSuccessStatusCode(); - - var totalBytes = response.Content.Headers.ContentLength; - await using var input = await response.Content.ReadAsStreamAsync(token); - await using var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true); - - var buffer = new byte[81920]; - long received = 0; - while (true) - { - int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), token); - if (read == 0) - break; - - await output.WriteAsync(buffer.AsMemory(0, read), token); - received += read; - if (totalBytes.HasValue && totalBytes.Value > 0) - { - onProgress?.Invoke((double)received / totalBytes.Value * 100.0); - } - } - - onProgress?.Invoke(100.0); - } - - private static string BuildSelfUpdateScript(string stagedExePath, string currentExePath) - { - return -$@"@echo off -setlocal -set ""SRC={stagedExePath}"" -set ""DST={currentExePath}"" - -for /L %%i in (1,1,60) do ( - copy /Y ""%SRC%"" ""%DST%"" >nul 2>nul && goto copied - timeout /t 1 /nobreak >nul -) - -exit /b 1 - -:copied -start """" ""%DST%"" -del /Q ""%SRC%"" >nul 2>nul -del /Q ""%~f0"" >nul 2>nul -exit /b 0 -"; - } - - private async Task Button_SelfUpdateAsync() - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) - return; - - if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) - return; - - _updateCts?.Dispose(); - _updateCts = new CancellationTokenSource(); - var token = _updateCts.Token; - - vm.IsUpdating = true; - vm.UpdateAvailable = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = "Downloading Wauncher update..."; - vm.UpdateStatusSpeed = ""; - - try - { - if (string.IsNullOrWhiteSpace(_selfUpdateDownloadUrl)) - throw new Exception("No self-update package URL found."); - - var updatesDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "self-update"); - Directory.CreateDirectory(updatesDir); - - var safeVersion = Regex.Replace(_selfUpdateVersion, @"[^0-9A-Za-z\.\-_]", string.Empty); - if (string.IsNullOrWhiteSpace(safeVersion)) - safeVersion = "latest"; - - var stagedExePath = Path.Combine(updatesDir, $"wauncher_{safeVersion}.exe"); - await DownloadFileWithProgressAsync(_selfUpdateDownloadUrl, stagedExePath, percent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateProgress = percent; - vm.UpdateStatusFile = $"Downloading Wauncher update... {percent:F0}%"; - }); - }, token); - - var currentExePath = Services.GetExePath(); - if (string.IsNullOrWhiteSpace(currentExePath)) - throw new Exception("Could not locate current Wauncher executable."); - - var scriptPath = Path.Combine(updatesDir, "apply_wauncher_update.cmd"); - var script = BuildSelfUpdateScript(stagedExePath, currentExePath); - File.WriteAllText(scriptPath, script, Encoding.ASCII); - - Process.Start(new ProcessStartInfo - { - FileName = "cmd.exe", - Arguments = $"/c \"{scriptPath}\"", - WorkingDirectory = updatesDir, - CreateNoWindow = true, - UseShellExecute = false, - }); - - vm.UpdateStatusFile = "Restarting Wauncher to apply update..."; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = 100; - await Task.Delay(500, token); - ForceQuit(); - } - catch (OperationCanceledException) - { - vm.UpdateStatusFile = "Update cancelled."; - vm.UpdateStatusSpeed = ""; - await Task.Delay(800); - } - catch (Exception ex) - { - vm.UpdateStatusFile = $"Self-update failed: {ex.Message}"; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = false; - await Task.Delay(2500); - } - finally - { - if (!_forceClose) - { - vm.IsUpdating = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - _updateCts?.Dispose(); - _updateCts = null; - - try - { - await CheckForUpdatesAsync(); - } - catch - { - // keep UI responsive even if refresh fails - } - } - - Interlocked.Exchange(ref _updateInProgress, 0); - } - } - - private async Task CheckForUpdatesAsync() - { - if (DataContext is not MainWindowViewModel vm) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - - if (vm.IsOfflineMode) - { - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - _cachedPatches = null; - vm.UpdateAvailable = false; - vm.IsCheckingUpdates = false; - var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = launchColor; - ArrowButton.Background = launchColor; - LaunchUpdateButton.IsEnabled = true; - return; - } - - vm.IsCheckingUpdates = true; - LaunchUpdateButton.Background = new SolidColorBrush(Color.Parse("#555555")); - ArrowButton.Background = new SolidColorBrush(Color.Parse("#555555")); - LaunchUpdateButton.IsEnabled = false; - try - { - bool hasSelfUpdate = await CheckForSelfUpdateAsync(); - if (hasSelfUpdate) - { - _cachedPatches = null; - vm.UpdateAvailable = true; - var selfUpdateColor = new SolidColorBrush(Color.Parse("#FFC107")); - LaunchUpdateButton.Background = selfUpdateColor; - ArrowButton.Background = selfUpdateColor; - - if (Interlocked.Exchange(ref _autoSelfUpdateTriggered, 1) == 0) - { - _ = Button_SelfUpdateAsync(); - } - - return; - } - - if (_settings.SkipUpdates) - { - _cachedPatches = null; - vm.UpdateAvailable = false; - var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = launchColor; - ArrowButton.Background = launchColor; - return; - } - - var patches = await Task.Run(() => PatchManager.ValidatePatches(deleteOutdatedFiles: false)); - bool hasUpdates = patches.Missing.Count > 0 || patches.Outdated.Count > 0; - - // Cache the result so Button_Update can consume it without re-validating. - _cachedPatches = patches; - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - vm.UpdateAvailable = hasUpdates; - var buttonColor = new SolidColorBrush( - Color.Parse(hasUpdates ? "#FFC107" : "#4CAF50")); + return items; + } + + private sealed class GitHubRelease + { + [JsonProperty("tag_name")] + public string? TagName { get; set; } + + [JsonProperty("assets")] + public List? Assets { get; set; } + } + + private sealed class GitHubReleaseAsset + { + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("browser_download_url")] + public string DownloadUrl { get; set; } = string.Empty; + } + + private static string NormalizeVersionToken(string? version) + { + if (string.IsNullOrWhiteSpace(version)) + return "0.0.0"; + + var cleaned = version.Trim(); + if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + cleaned = cleaned[1..]; + + cleaned = Regex.Replace(cleaned, @"[^0-9\.]", string.Empty); + return string.IsNullOrWhiteSpace(cleaned) ? "0.0.0" : cleaned; + } + + private static bool TryParseVersion(string value, out global::System.Version parsed) + { + if (global::System.Version.TryParse(value, out parsed!)) + return true; + + var tokens = value.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 0) + { + parsed = new global::System.Version(0, 0, 0); + return false; + } + + while (tokens.Length < 3) + tokens = tokens.Append("0").ToArray(); + + return global::System.Version.TryParse(string.Join('.', tokens.Take(4)), out parsed!); + } + + private async Task CheckForSelfUpdateAsync() + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + + try + { + var latestReleaseJson = await Api.GitHub.GetLatestRelease(); + var release = JsonConvert.DeserializeObject(latestReleaseJson); + if (release == null) + return false; + + var currentVersion = NormalizeVersionToken(Wauncher.Utils.Version.Current); + var latestVersion = NormalizeVersionToken(release.TagName); + if (!TryParseVersion(currentVersion, out var current) || !TryParseVersion(latestVersion, out var latest)) + return false; + + if (latest <= current) + return false; + + var assets = release.Assets ?? new List(); + var preferred = assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + string.Equals(a.Name, "wauncher.exe", StringComparison.OrdinalIgnoreCase)); + + preferred ??= assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + a.Name.Contains("wauncher", StringComparison.OrdinalIgnoreCase) && + a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); + + if (preferred == null) + return false; + + _selfUpdateAvailable = true; + _selfUpdateDownloadUrl = preferred.DownloadUrl; + _selfUpdateVersion = latestVersion; + return true; + } + catch + { + return false; + } + } + + private async Task DownloadFileWithProgressAsync(string url, string destination, Action? onProgress, CancellationToken token) + { + using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); + response.EnsureSuccessStatusCode(); + + var totalBytes = response.Content.Headers.ContentLength; + await using var input = await response.Content.ReadAsStreamAsync(token); + await using var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true); + + var buffer = new byte[81920]; + long received = 0; + while (true) + { + int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), token); + if (read == 0) + break; + + await output.WriteAsync(buffer.AsMemory(0, read), token); + received += read; + if (totalBytes.HasValue && totalBytes.Value > 0) + { + onProgress?.Invoke((double)received / totalBytes.Value * 100.0); + } + } + + onProgress?.Invoke(100.0); + } + + private static string BuildSelfUpdateScript(string stagedExePath, string currentExePath) + { + return +$@"@echo off +setlocal +set ""SRC={stagedExePath}"" +set ""DST={currentExePath}"" + +for /L %%i in (1,1,60) do ( + copy /Y ""%SRC%"" ""%DST%"" >nul 2>nul && goto copied + timeout /t 1 /nobreak >nul +) + +exit /b 1 + +:copied +start """" ""%DST%"" +del /Q ""%SRC%"" >nul 2>nul +del /Q ""%~f0"" >nul 2>nul +exit /b 0 +"; + } + + private async Task Button_SelfUpdateAsync() + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) + return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = "Downloading Wauncher update..."; + vm.UpdateStatusSpeed = ""; + + try + { + if (string.IsNullOrWhiteSpace(_selfUpdateDownloadUrl)) + throw new Exception("No self-update package URL found."); + + var updatesDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "self-update"); + Directory.CreateDirectory(updatesDir); + + var safeVersion = Regex.Replace(_selfUpdateVersion, @"[^0-9A-Za-z\.\-_]", string.Empty); + if (string.IsNullOrWhiteSpace(safeVersion)) + safeVersion = "latest"; + + var stagedExePath = Path.Combine(updatesDir, $"wauncher_{safeVersion}.exe"); + await DownloadFileWithProgressAsync(_selfUpdateDownloadUrl, stagedExePath, percent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateProgress = percent; + vm.UpdateStatusFile = $"Downloading Wauncher update... {percent:F0}%"; + }); + }, token); + + var currentExePath = Services.GetExePath(); + if (string.IsNullOrWhiteSpace(currentExePath)) + throw new Exception("Could not locate current Wauncher executable."); + + var scriptPath = Path.Combine(updatesDir, "apply_wauncher_update.cmd"); + var script = BuildSelfUpdateScript(stagedExePath, currentExePath); + File.WriteAllText(scriptPath, script, Encoding.ASCII); + + Process.Start(new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/c \"{scriptPath}\"", + WorkingDirectory = updatesDir, + CreateNoWindow = true, + UseShellExecute = false, + }); + + vm.UpdateStatusFile = "Restarting Wauncher to apply update..."; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 100; + await Task.Delay(500, token); + ForceQuit(); + } + catch (OperationCanceledException) + { + vm.UpdateStatusFile = "Update cancelled."; + vm.UpdateStatusSpeed = ""; + await Task.Delay(800); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Self-update failed: {ex.Message}"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + await Task.Delay(2500); + } + finally + { + if (!_forceClose) + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch + { + // keep UI responsive even if refresh fails + } + } + + Interlocked.Exchange(ref _updateInProgress, 0); + } + } + + private async Task CheckForUpdatesAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsOfflineMode) + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + _cachedPatches = null; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + LaunchUpdateButton.IsEnabled = true; + return; + } + + vm.IsCheckingUpdates = true; + LaunchUpdateButton.Background = new SolidColorBrush(Color.Parse("#555555")); + ArrowButton.Background = new SolidColorBrush(Color.Parse("#555555")); + LaunchUpdateButton.IsEnabled = false; + try + { + bool hasSelfUpdate = await CheckForSelfUpdateAsync(); + if (hasSelfUpdate) + { + _cachedPatches = null; + vm.UpdateAvailable = true; + var selfUpdateColor = new SolidColorBrush(Color.Parse("#FFC107")); + LaunchUpdateButton.Background = selfUpdateColor; + ArrowButton.Background = selfUpdateColor; + + if (Interlocked.Exchange(ref _autoSelfUpdateTriggered, 1) == 0) + { + _ = Button_SelfUpdateAsync(); + } + + return; + } + + if (_settings.SkipUpdates) + { + _cachedPatches = null; + vm.UpdateAvailable = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + return; + } + + var patches = await Task.Run(() => PatchManager.ValidatePatches(deleteOutdatedFiles: false)); + bool hasUpdates = patches.Missing.Count > 0 || patches.Outdated.Count > 0; + + // Cache the result so Button_Update can consume it without re-validating. + _cachedPatches = patches; + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + vm.UpdateAvailable = hasUpdates; + var buttonColor = new SolidColorBrush( + Color.Parse(hasUpdates ? "#FFC107" : "#4CAF50")); LaunchUpdateButton.Background = buttonColor; ArrowButton.Background = buttonColor; } @@ -1223,44 +1228,44 @@ private async Task CheckForUpdatesAsync() } } - private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) return; - - if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) - return; - - _updateCts?.Dispose(); - _updateCts = new CancellationTokenSource(); - var token = _updateCts.Token; - vm.IsUpdating = true; - vm.UpdateAvailable = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; + private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; vm.UpdateStatus = ""; - try - { - // Use the result already computed by CheckForUpdatesAsync when available, - // to avoid a redundant full validation on every update click. - bool validateAll = _forceValidateAllOnce; - _forceValidateAllOnce = false; - bool usingCachedPatches = _cachedPatches != null; - - if (!usingCachedPatches) - { - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = validateAll - ? "Verifying all game files..." - : "Checking game files..."; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = 0; - } - - var patches = _cachedPatches ?? await Task.Run(() => PatchManager.ValidatePatches(validateAll: validateAll), token); - _cachedPatches = null; // consumed — force fresh check next time - if (token.IsCancellationRequested) return; + try + { + // Use the result already computed by CheckForUpdatesAsync when available, + // to avoid a redundant full validation on every update click. + bool validateAll = _forceValidateAllOnce; + _forceValidateAllOnce = false; + bool usingCachedPatches = _cachedPatches != null; + + if (!usingCachedPatches) + { + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = validateAll + ? "Verifying all game files..." + : "Checking game files..."; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 0; + } + + var patches = _cachedPatches ?? await Task.Run(() => PatchManager.ValidatePatches(validateAll: validateAll), token); + _cachedPatches = null; // consumed — force fresh check next time + if (token.IsCancellationRequested) return; bool hasPatches = patches.Missing.Count > 0 || patches.Outdated.Count > 0; if (!hasPatches) @@ -1276,44 +1281,44 @@ private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEv int totalFiles = allPatches.Count; int completed = 0; - foreach (var patch in allPatches) - { - if (token.IsCancellationRequested) break; - - var extractWatch = new System.Diagnostics.Stopwatch(); - await DownloadManager.DownloadPatch( - patch, - onProgress: (p) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; - vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); - vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; - }); - }, - onExtract: () => - { - extractWatch.Restart(); - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); - vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; - }); - }); + foreach (var patch in allPatches) + { + if (token.IsCancellationRequested) break; + + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); completed++; vm.UpdateProgress = (double)completed / totalFiles * 100.0; @@ -1340,27 +1345,27 @@ await DownloadManager.DownloadPatch( vm.UpdateIndeterminate = false; await Task.Delay(3000); } - finally - { - vm.IsUpdating = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - _cachedPatches = null; - _updateCts?.Dispose(); - _updateCts = null; - - try - { - await CheckForUpdatesAsync(); - } - catch { } - - Interlocked.Exchange(ref _updateInProgress, 0); - } - } + finally + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _cachedPatches = null; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch { } + + Interlocked.Exchange(ref _updateInProgress, 0); + } + } private void Button_CancelUpdate(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { @@ -1373,79 +1378,79 @@ private void FriendsTab_Click(object? sender, Avalonia.Interactivity.RoutedEvent if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "Friends"; } - private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "PatchNotes"; - PatchNotesScroll.Offset = new Vector(0, 0); - } - - private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (sender is not MenuItem { Tag: FriendInfo friend }) - return; - - var profileId = ResolveProfileSteamId(friend.SteamId); - if (string.IsNullOrWhiteSpace(profileId)) - return; - - try - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = $"https://eddies.cc/profiles/{profileId}", - UseShellExecute = true - }); - } - catch - { - // Best-effort open. - } - } - - private static string ResolveProfileSteamId(string? steamId) - { - if (string.IsNullOrWhiteSpace(steamId)) - return string.Empty; - - var value = steamId.Trim(); - if (ulong.TryParse(value, out _)) - return value; - - if (TryConvertSteamId2To64(value, out var steamId64)) - return steamId64.ToString(); - - return string.Empty; - } - - private static bool TryConvertSteamId2To64(string steamId2, out ulong steamId64) - { - steamId64 = 0; - var match = Regex.Match(steamId2, @"^STEAM_[0-5]:([0-1]):(\d+)$", RegexOptions.IgnoreCase); - if (!match.Success) - return false; - - if (!ulong.TryParse(match.Groups[1].Value, out var y)) - return false; - if (!ulong.TryParse(match.Groups[2].Value, out var z)) - return false; - - steamId64 = 76561197960265728UL + (z * 2UL) + y; - return true; - } - - private void Button_Settings(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { + private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "PatchNotes"; + PatchNotesScroll.Offset = new Vector(0, 0); + } + + private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: FriendInfo friend }) + return; + + var profileId = ResolveProfileSteamId(friend.SteamId); + if (string.IsNullOrWhiteSpace(profileId)) + return; + + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = $"https://eddies.cc/profiles/{profileId}", + UseShellExecute = true + }); + } + catch + { + // Best-effort open. + } + } + + private static string ResolveProfileSteamId(string? steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) + return string.Empty; + + var value = steamId.Trim(); + if (ulong.TryParse(value, out _)) + return value; + + if (TryConvertSteamId2To64(value, out var steamId64)) + return steamId64.ToString(); + + return string.Empty; + } + + private static bool TryConvertSteamId2To64(string steamId2, out ulong steamId64) + { + steamId64 = 0; + var match = Regex.Match(steamId2, @"^STEAM_[0-5]:([0-1]):(\d+)$", RegexOptions.IgnoreCase); + if (!match.Success) + return false; + + if (!ulong.TryParse(match.Groups[1].Value, out var y)) + return false; + if (!ulong.TryParse(match.Groups[2].Value, out var z)) + return false; + + steamId64 = 76561197960265728UL + (z * 2UL) + y; + return true; + } + + private void Button_Settings(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { if (_settingsWindow == null) { - _settingsWindow = new SettingsWindow(); - _settingsWindow.Closed += (s, e) => - { - _settingsWindow = null; - _settings = SettingsWindowViewModel.LoadGlobal(); - _ = CheckForUpdatesAsync(); - }; - _settingsWindow.Show(this); - } + _settingsWindow = new SettingsWindow(); + _settingsWindow.Closed += (s, e) => + { + _settingsWindow = null; + _settings = SettingsWindowViewModel.LoadGlobal(); + _ = CheckForUpdatesAsync(); + }; + _settingsWindow.Show(this); + } else _settingsWindow.Activate(); } @@ -1461,114 +1466,148 @@ private void Button_Info(object? sender, Avalonia.Interactivity.RoutedEventArgs } // ── Launch button glow + color ──────────────────────────────────────────── - private void SetLaunchGlow(bool updating) - { - var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); - LaunchUpdateButton.Background = brush; - ArrowButton.Background = brush; - LaunchButtonGlow.BoxShadow = updating - ? BoxShadows.Parse("0 0 18 2 #55FFC107") - : BoxShadows.Parse("0 0 18 2 #554CAF50"); - } - - private static string ShortFileName(string path) - { - if (string.IsNullOrWhiteSpace(path)) - return path; - - var normalized = path.Replace('\\', '/'); - if (normalized.Length <= 42) - return normalized; - - var fileName = Path.GetFileName(normalized); - if (fileName.Length <= 30) - return fileName; - - return fileName[..27] + "..."; - } - - private static string FormatDownloadSpeedAndEta(object progressArgs) - { - double speedBytes = 0; - if (TryGetDoubleProperty(progressArgs, "AverageBytesPerSecondSpeed", out var avg) && avg > 0) - speedBytes = avg; - else if (TryGetDoubleProperty(progressArgs, "BytesPerSecondSpeed", out var cur) && cur > 0) - speedBytes = cur; - - var speedText = speedBytes > 0 - ? $"{speedBytes / 1024.0 / 1024.0:F1} MB/s" - : ""; - - if (speedBytes <= 0 || - !TryGetLongProperty(progressArgs, "TotalBytesToReceive", out var totalBytes) || - !TryGetLongProperty(progressArgs, "ReceivedBytesSize", out var receivedBytes) || - totalBytes <= 0 || receivedBytes < 0 || receivedBytes >= totalBytes) - { - return speedText; - } - - var remainingBytes = totalBytes - receivedBytes; - var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); - var etaText = $"ETA {FormatEta(eta)}"; - - return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; - } - - private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) - { - if (watch == null || !watch.IsRunning || percent <= 1.0) - return ""; - - var elapsed = watch.Elapsed.TotalSeconds; - var total = elapsed / (percent / 100.0); - var remaining = Math.Max(0, total - elapsed); - return $"ETA {FormatEta(TimeSpan.FromSeconds(remaining))}"; - } - - private static string FormatEta(TimeSpan eta) - { - if (eta.TotalHours >= 1) - return eta.ToString(@"hh\:mm\:ss"); - return eta.ToString(@"mm\:ss"); - } - - private static bool TryGetDoubleProperty(object obj, string propertyName, out double value) - { - value = 0; - var prop = obj.GetType().GetProperty(propertyName); - if (prop == null) return false; - var raw = prop.GetValue(obj); - if (raw == null) return false; - try - { - value = Convert.ToDouble(raw); - return true; - } - catch - { - return false; - } - } - - private static bool TryGetLongProperty(object obj, string propertyName, out long value) - { - value = 0; - var prop = obj.GetType().GetProperty(propertyName); - if (prop == null) return false; - var raw = prop.GetValue(obj); - if (raw == null) return false; - try - { - value = Convert.ToInt64(raw); - return true; - } - catch - { - return false; - } - } - - } -} + private void SetLaunchGlow(bool updating) + { + var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); + LaunchUpdateButton.Background = brush; + ArrowButton.Background = brush; + LaunchButtonGlow.BoxShadow = updating + ? BoxShadows.Parse("0 0 18 2 #55FFC107") + : BoxShadows.Parse("0 0 18 2 #554CAF50"); + } + + private static string ShortFileName(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return path; + + var normalized = path.Replace('\\', '/'); + if (normalized.Length <= 42) + return normalized; + + var fileName = Path.GetFileName(normalized); + if (fileName.Length <= 30) + return fileName; + + return fileName[..27] + "..."; + } + + private static string FormatDownloadSpeedAndEta(object progressArgs) + { + double speedBytes = 0; + if (TryGetDoubleProperty(progressArgs, "AverageBytesPerSecondSpeed", out var avg) && avg > 0) + speedBytes = avg; + else if (TryGetDoubleProperty(progressArgs, "BytesPerSecondSpeed", out var cur) && cur > 0) + speedBytes = cur; + + var speedText = speedBytes > 0 + ? $"{speedBytes / 1024.0 / 1024.0:F1} MB/s" + : ""; + + if (speedBytes <= 0 || + !TryGetLongProperty(progressArgs, "TotalBytesToReceive", out var totalBytes) || + !TryGetLongProperty(progressArgs, "ReceivedBytesSize", out var receivedBytes) || + totalBytes <= 0 || receivedBytes < 0 || receivedBytes >= totalBytes) + { + return speedText; + } + + var remainingBytes = totalBytes - receivedBytes; + var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); + var etaText = $"ETA {FormatEta(eta)}"; + + return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; + } + + private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) + { + if (watch == null || !watch.IsRunning || percent <= 1.0) + return ""; + + var elapsed = watch.Elapsed.TotalSeconds; + var total = elapsed / (percent / 100.0); + var remaining = Math.Max(0, total - elapsed); + return $"ETA {FormatEta(TimeSpan.FromSeconds(remaining))}"; + } + + private static string FormatEta(TimeSpan eta) + { + if (eta.TotalHours >= 1) + return eta.ToString(@"hh\:mm\:ss"); + return eta.ToString(@"mm\:ss"); + } + + private static bool TryGetDoubleProperty(object obj, string propertyName, out double value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToDouble(raw); + return true; + } + catch + { + return false; + } + } + + private static bool TryGetLongProperty(object obj, string propertyName, out long value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToInt64(raw); + return true; + } + catch + { + return false; + } + } + + // Minimal parser for launch options that supports quoted values. + private static IEnumerable ParseLaunchOptions(string options) + { + if (string.IsNullOrWhiteSpace(options)) + yield break; + + var current = new StringBuilder(); + bool inQuotes = false; + + foreach (var ch in options) + { + if (ch == '"') + { + inQuotes = !inQuotes; + continue; + } + + if (char.IsWhiteSpace(ch) && !inQuotes) + { + if (current.Length > 0) + { + yield return current.ToString(); + current.Clear(); + } + continue; + } + + current.Append(ch); + } + + if (current.Length > 0) + yield return current.ToString(); + } + + } +} From f4f923332a4abb4b84978e5d6f9cacf3adf98a97 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:16:20 -0400 Subject: [PATCH 18/51] fix protocol command compile check --- Wauncher/ViewModels/MainWindowViewModel.cs | 1132 ++++++++++---------- 1 file changed, 566 insertions(+), 566 deletions(-) diff --git a/Wauncher/ViewModels/MainWindowViewModel.cs b/Wauncher/ViewModels/MainWindowViewModel.cs index 7459c1b..15dc8ba 100644 --- a/Wauncher/ViewModels/MainWindowViewModel.cs +++ b/Wauncher/ViewModels/MainWindowViewModel.cs @@ -1,568 +1,568 @@ -using Avalonia.Threading; -using CommunityToolkit.Mvvm.ComponentModel; -using Wauncher.Utils; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Linq; -using System.Net.NetworkInformation; -using System.Text; -using System.Threading; -using FriendInfo = Wauncher.Utils.FriendInfo; - -namespace Wauncher.ViewModels -{ - public class ServerInfo : INotifyPropertyChanged - { - public event PropertyChangedEventHandler? PropertyChanged; - - public string Name { get; set; } = ""; - public string IpPort { get; set; } = ""; - - private int _players; - private int _maxPlayers; - private bool _isOnline; - private string _map = ""; - - public int Players - { - get => _players; - set - { - if (_players == value) return; - _players = value; - Notify(nameof(Players), nameof(PlayerCount)); - } - } - - public int MaxPlayers - { - get => _maxPlayers; - set - { - if (_maxPlayers == value) return; - _maxPlayers = value; - Notify(nameof(MaxPlayers), nameof(PlayerCount)); - } - } - - public bool IsOnline - { - get => _isOnline; - set - { - if (_isOnline == value) return; - _isOnline = value; - Notify(nameof(IsOnline), nameof(DotColor)); - } - } - - public string Map - { - get => _map; - set - { - if (_map == value) return; - _map = value; - Notify(nameof(Map), nameof(MapDisplay)); - } - } - - public bool IsNone => string.IsNullOrEmpty(IpPort); - - public string PlayerCount => IsNone ? "" : $"{Players}/{MaxPlayers}"; - public string DotColor => IsNone ? "Transparent" : (IsOnline ? "#4CAF50" : "#F44336"); - public string NameColor => IsNone ? "#66FFFFFF" : "White"; - public string MapDisplay => (!IsNone && !string.IsNullOrEmpty(Map)) ? Map : ""; - - private void Notify(params string[] names) - { - Dispatcher.UIThread.Post(() => - { - foreach (var name in names) - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); - }); - } - } - - public partial class MainWindowViewModel : ViewModelBase - { - [ObservableProperty] - private string _gameStatus = "Not Running"; - - [ObservableProperty] - private string _protocolManager = "None"; - - [ObservableProperty] - private string _profilePicture = "https://avatars.githubusercontent.com/u/75831703?v=4"; - - [ObservableProperty] - private string _usernameGreeting = "Hello, username"; - - [ObservableProperty] - private string _whitelistDotColor = "Gray"; - - [ObservableProperty] - private string _whitelistText = "Unknown"; - - [ObservableProperty] - private bool _isDropdownOpen = false; - - [ObservableProperty] - private string _activeRightTab = "Friends"; - - public bool IsFriendsTabActive => ActiveRightTab == "Friends"; - public bool IsPatchNotesTabActive => ActiveRightTab == "PatchNotes"; - - partial void OnActiveRightTabChanged(string value) - { - OnPropertyChanged(nameof(IsFriendsTabActive)); - OnPropertyChanged(nameof(IsPatchNotesTabActive)); - } - - [ObservableProperty] - private bool _isOfflineMode = false; - - public bool IsOnlineMode => !IsOfflineMode; - partial void OnIsOfflineModeChanged(bool value) => OnPropertyChanged(nameof(IsOnlineMode)); - - [ObservableProperty] - private bool _isUpdating = false; - - [ObservableProperty] - private bool _isInstalling = false; - - [ObservableProperty] - private bool _isNeedingInstall = false; - - [ObservableProperty] - private bool _isCheckingUpdates = false; - - public bool IsCheckingOrUpdating => IsCheckingUpdates || IsUpdating || IsInstalling; - public bool IsUpdatingOrInstalling => IsUpdating || IsInstalling; - - partial void OnIsCheckingUpdatesChanged(bool value) => OnPropertyChanged(nameof(IsCheckingOrUpdating)); - partial void OnIsInstallingChanged(bool value) - { - OnPropertyChanged(nameof(LaunchButtonText)); - OnPropertyChanged(nameof(IsCheckingOrUpdating)); - OnPropertyChanged(nameof(IsUpdatingOrInstalling)); - } - partial void OnIsNeedingInstallChanged(bool value) => OnPropertyChanged(nameof(LaunchButtonText)); - - [ObservableProperty] - private string _updateStatus = ""; - - [ObservableProperty] - private string _updateStatusFile = ""; - - [ObservableProperty] - private string _updateStatusSpeed = ""; - - [ObservableProperty] - private double _updateProgress = 0; - - [ObservableProperty] - private bool _updateIndeterminate = false; - - [ObservableProperty] - private bool _updateAvailable = false; - - public string LaunchButtonText => - IsInstalling ? "Installing Game..." : - IsUpdating ? "Updating..." : - IsNeedingInstall ? "Install Game" : - UpdateAvailable ? "Update" : - "Launch Game"; - - partial void OnIsUpdatingChanged(bool value) - { - OnPropertyChanged(nameof(LaunchButtonText)); - OnPropertyChanged(nameof(IsCheckingOrUpdating)); - OnPropertyChanged(nameof(IsUpdatingOrInstalling)); - } - partial void OnUpdateAvailableChanged(bool value) => OnPropertyChanged(nameof(LaunchButtonText)); - - [ObservableProperty] - private ServerInfo? _selectedServer; - - // What the SELECTED SERVER label shows - public string SelectedLabel => SelectedServer?.IsNone == false - ? SelectedServer.Name - : "Server not selected..."; - - public bool IsNoServerSelected => SelectedServer == null || SelectedServer.IsNone; - public bool IsServerSelected => SelectedServer != null && !SelectedServer.IsNone; - - public ObservableCollection Servers { get; } = new() - { - // ── None (clears selection) ────────────────────────────────────── - new ServerInfo { Name = "None", IpPort = "", IsOnline = false }, - - // ── Real servers ───────────────────────────────────────────────── - new ServerInfo { Name = "NA | PUG | 64 Tick", IpPort = "na.classiccounter.cc:27015", Players = 0, MaxPlayers = 10, IsOnline = true }, - new ServerInfo { Name = "NA | PUG-2 | 64 Tick", IpPort = "na.classiccounter.cc:27016", Players = 0, MaxPlayers = 10, IsOnline = true }, - new ServerInfo { Name = "EU | PUG | 64 Tick", IpPort = "eu.classiccounter.cc:27016", Players = 0, MaxPlayers = 10, IsOnline = true }, - new ServerInfo { Name = "EU | PUG | 128 Tick", IpPort = "eu.classiccounter.cc:27015", Players = 0, MaxPlayers = 10, IsOnline = true }, - new ServerInfo { Name = "EU | PUG-2 | 128 Tick",IpPort = "eu.classiccounter.cc:27022", Players = 0, MaxPlayers = 10, IsOnline = true }, - }; - - partial void OnSelectedServerChanged(ServerInfo? value) - { - // Update the label shown in the trigger button - ProtocolManager = (value == null || value.IsNone) ? "None" : value.Name; - OnPropertyChanged(nameof(SelectedLabel)); - OnPropertyChanged(nameof(IsNoServerSelected)); - OnPropertyChanged(nameof(IsServerSelected)); - } - - public MainWindowViewModel() - { - if (Argument.HasProtocolCommand()) - ProtocolManager = "Ready to Launch!"; - - _ = LoadSelfProfileAsync(); - - CheckWhitelistStatus(); - UpdateOfflineMode(); - NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; - - // Query servers immediately, then refresh every 30 seconds - _ = RefreshServersSafeAsync(); - _serverRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; - _serverRefreshTimer.Tick += async (_, _) => await RefreshServersSafeAsync(); - _serverRefreshTimer.Start(); - - // Query friends immediately, then refresh every 30 seconds - _ = RefreshFriendsSafeAsync(); - _friendsTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; - _friendsTimer.Tick += async (_, _) => await RefreshFriendsSafeAsync(); - _friendsTimer.Start(); - } - - private DispatcherTimer? _serverRefreshTimer; - private int _serverRefreshInProgress; - - // ── Friends ─────────────────────────────────────────────────────────────── - public ObservableCollection Friends { get; } = new(); - - [ObservableProperty] private bool _friendsShowStatus = true; - [ObservableProperty] private string _friendsStatus = "Loading..."; - [ObservableProperty] private bool _showNoFriendsState = false; - public bool ShowGenericFriendsStatus => FriendsShowStatus && !ShowNoFriendsState; - - partial void OnFriendsShowStatusChanged(bool value) => OnPropertyChanged(nameof(ShowGenericFriendsStatus)); - partial void OnShowNoFriendsStateChanged(bool value) => OnPropertyChanged(nameof(ShowGenericFriendsStatus)); - - private DispatcherTimer? _friendsTimer; - private int _friendsRefreshInProgress; - private string _lastRenderedFriendsSignature = string.Empty; - private string _lastKnownSteamId2 = string.Empty; - - private async Task LoadSelfProfileAsync() - { - try - { - bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); - if (!hasSteam || string.IsNullOrWhiteSpace(Steam.recentSteamID64)) - return; - - var rawSelfJson = await Api.Eddies.GetSelfInfo(Steam.recentSteamID64); - var self = Api.ParseSelfInfoPayload(rawSelfJson); - if (self == null) - return; - - Dispatcher.UIThread.Post(() => - { - if (!string.IsNullOrWhiteSpace(self.AvatarUrl)) - ProfilePicture = AvatarCache.GetDisplaySource(self.AvatarUrl); - - if (!string.IsNullOrWhiteSpace(self.Username)) - UsernameGreeting = $"Hello, {self.Username}"; - }); - } - catch - { - // Best-effort profile load; keep defaults on failure. - } - } - - private async Task RefreshServersAsync() - { - if (IsOfflineMode) - { - foreach (var s in Servers.Where(s => !s.IsNone)) - { - s.IsOnline = false; - s.Players = 0; - s.MaxPlayers = 0; - s.Map = ""; - } - return; - } - - await ServerQuery.RefreshServers(Servers.Where(s => !s.IsNone)); - - // Re-order by player count descending; None always stays at index 0 - var sorted = Servers.Where(s => !s.IsNone) - .OrderByDescending(s => s.Players) - .ToList(); - int insertAt = 1; - foreach (var server in sorted) - { - int from = Servers.IndexOf(server); - if (from != insertAt) - Servers.Move(from, insertAt); - insertAt++; - } - } - - private async Task RefreshServersSafeAsync() - { - if (Interlocked.Exchange(ref _serverRefreshInProgress, 1) == 1) - return; - - try - { - await RefreshServersAsync(); - } - finally - { - Interlocked.Exchange(ref _serverRefreshInProgress, 0); - } - } - - private async Task RefreshFriendsAsync() - { - try - { - if (IsOfflineMode) - { - var steamIdForCache = !string.IsNullOrWhiteSpace(_lastKnownSteamId2) - ? _lastKnownSteamId2 - : (Steam.recentSteamID2 ?? string.Empty); - - if (TryShowCachedFriends(steamIdForCache, forceOfflineStatus: true)) - return; - - Dispatcher.UIThread.Post(() => - { - Friends.Clear(); - _lastRenderedFriendsSignature = string.Empty; - ShowNoFriendsState = false; - FriendsStatus = "Offline mode"; - FriendsShowStatus = true; - }); - return; - } - - bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); - string steamId = Steam.recentSteamID2 ?? string.Empty; - if (!string.IsNullOrWhiteSpace(steamId)) - _lastKnownSteamId2 = steamId; - - if (!hasSteam) - { - Dispatcher.UIThread.Post(() => - { - ShowNoFriendsState = false; - FriendsStatus = "Steam is not installed."; - FriendsShowStatus = true; - }); - return; - } - - if (string.IsNullOrEmpty(Steam.recentSteamID2)) - { - Dispatcher.UIThread.Post(() => - { - ShowNoFriendsState = false; - FriendsStatus = "Sign in to Steam to see friends."; - FriendsShowStatus = true; - }); - return; - } - - string rawFriendsJson; - try - { - rawFriendsJson = await Api.Eddies.GetFriends(Steam.recentSteamID64 ?? string.Empty); - } - catch - { - rawFriendsJson = await Api.Eddies.GetFriendsBySteamId2(Steam.recentSteamID2 ?? string.Empty); - } - var apiFriends = Api.ParseFriendsPayload(rawFriendsJson) - .OrderBy(f => f.Status == "Offline" ? 1 : 0) - .ToList(); - - await FriendsCache.SaveAsync(steamId, apiFriends); - - Dispatcher.UIThread.Post(() => - { - var sorted = apiFriends; - - foreach (var f in sorted) - f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); - - var signature = BuildFriendsSignature(sorted); - if (!string.Equals(signature, _lastRenderedFriendsSignature, StringComparison.Ordinal)) - { - Friends.Clear(); - foreach (var f in sorted) - Friends.Add(f); - _lastRenderedFriendsSignature = signature; - } - - FriendsShowStatus = Friends.Count == 0; - ShowNoFriendsState = Friends.Count == 0; - FriendsStatus = Friends.Count == 0 ? "No friends found." : ""; - }); - } - catch - { - if (TryShowCachedFriends(Steam.recentSteamID2 ?? string.Empty, forceOfflineStatus: true)) - return; - - Dispatcher.UIThread.Post(() => - { - Friends.Clear(); - _lastRenderedFriendsSignature = string.Empty; - ShowNoFriendsState = false; - FriendsStatus = IsOfflineMode ? "Offline mode" : "Couldn't load friends right now."; - FriendsShowStatus = true; - }); - } - } - - private bool TryShowCachedFriends(string steamId, bool forceOfflineStatus) - { - var cached = FriendsCache.Load(steamId); - if (cached.Count == 0) - return false; - - var sorted = cached - .OrderBy(f => f.Status == "Offline" ? 1 : 0) - .ToList(); - - if (forceOfflineStatus) - { - foreach (var f in sorted) - f.Status = "Offline"; - } - - foreach (var f in sorted) - f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); - - Dispatcher.UIThread.Post(() => - { - var signature = BuildFriendsSignature(sorted); - if (!string.Equals(signature, _lastRenderedFriendsSignature, StringComparison.Ordinal)) - { - Friends.Clear(); - foreach (var f in sorted) - Friends.Add(f); - _lastRenderedFriendsSignature = signature; - } - - FriendsShowStatus = false; - ShowNoFriendsState = false; - FriendsStatus = ""; - }); - - return true; - } - - private static string BuildFriendsSignature(IEnumerable friends) - { - var sb = new StringBuilder(); - foreach (var f in friends) - { - sb.Append(f.Username ?? string.Empty) - .Append('\u001f') - .Append(f.AvatarUrl ?? string.Empty) - .Append('\u001f') - .Append(f.Status ?? "Offline") - .Append('\u001e'); - } - return sb.ToString(); - } - - private async Task RefreshFriendsSafeAsync() - { - if (Interlocked.Exchange(ref _friendsRefreshInProgress, 1) == 1) - return; - - try - { - await RefreshFriendsAsync(); - } - finally - { - Interlocked.Exchange(ref _friendsRefreshInProgress, 0); - } - } - - - - - - private void CheckWhitelistStatus() - { - Task.Run(async () => - { - try - { - bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); - if (!hasSteam) - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - WhitelistDotColor = "Gray"; - WhitelistText = "Unknown"; - }); - return; - } - - if (string.IsNullOrEmpty(Steam.recentSteamID2)) - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - WhitelistDotColor = "Gray"; - WhitelistText = "Unknown"; - }); - return; - } - - var response = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); - bool whitelisted = response?.Files != null && response.Files.Count > 0; - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - WhitelistDotColor = whitelisted ? "#4CAF50" : "#F44336"; - WhitelistText = whitelisted ? "Whitelisted" : "Not Whitelisted"; - }); - } - catch - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - WhitelistDotColor = "Gray"; - WhitelistText = "Unknown"; - }); - } - }); - } - - private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) - { - Dispatcher.UIThread.Post(UpdateOfflineMode); - } - - private void UpdateOfflineMode() - { - IsOfflineMode = !NetworkInterface.GetIsNetworkAvailable(); - } - } -} +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using Wauncher.Utils; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Net.NetworkInformation; +using System.Text; +using System.Threading; +using FriendInfo = Wauncher.Utils.FriendInfo; + +namespace Wauncher.ViewModels +{ + public class ServerInfo : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + + public string Name { get; set; } = ""; + public string IpPort { get; set; } = ""; + + private int _players; + private int _maxPlayers; + private bool _isOnline; + private string _map = ""; + + public int Players + { + get => _players; + set + { + if (_players == value) return; + _players = value; + Notify(nameof(Players), nameof(PlayerCount)); + } + } + + public int MaxPlayers + { + get => _maxPlayers; + set + { + if (_maxPlayers == value) return; + _maxPlayers = value; + Notify(nameof(MaxPlayers), nameof(PlayerCount)); + } + } + + public bool IsOnline + { + get => _isOnline; + set + { + if (_isOnline == value) return; + _isOnline = value; + Notify(nameof(IsOnline), nameof(DotColor)); + } + } + + public string Map + { + get => _map; + set + { + if (_map == value) return; + _map = value; + Notify(nameof(Map), nameof(MapDisplay)); + } + } + + public bool IsNone => string.IsNullOrEmpty(IpPort); + + public string PlayerCount => IsNone ? "" : $"{Players}/{MaxPlayers}"; + public string DotColor => IsNone ? "Transparent" : (IsOnline ? "#4CAF50" : "#F44336"); + public string NameColor => IsNone ? "#66FFFFFF" : "White"; + public string MapDisplay => (!IsNone && !string.IsNullOrEmpty(Map)) ? Map : ""; + + private void Notify(params string[] names) + { + Dispatcher.UIThread.Post(() => + { + foreach (var name in names) + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + }); + } + } + + public partial class MainWindowViewModel : ViewModelBase + { + [ObservableProperty] + private string _gameStatus = "Not Running"; + + [ObservableProperty] + private string _protocolManager = "None"; + + [ObservableProperty] + private string _profilePicture = "https://avatars.githubusercontent.com/u/75831703?v=4"; + + [ObservableProperty] + private string _usernameGreeting = "Hello, username"; + + [ObservableProperty] + private string _whitelistDotColor = "Gray"; + + [ObservableProperty] + private string _whitelistText = "Unknown"; + + [ObservableProperty] + private bool _isDropdownOpen = false; + + [ObservableProperty] + private string _activeRightTab = "Friends"; + + public bool IsFriendsTabActive => ActiveRightTab == "Friends"; + public bool IsPatchNotesTabActive => ActiveRightTab == "PatchNotes"; + + partial void OnActiveRightTabChanged(string value) + { + OnPropertyChanged(nameof(IsFriendsTabActive)); + OnPropertyChanged(nameof(IsPatchNotesTabActive)); + } + + [ObservableProperty] + private bool _isOfflineMode = false; + + public bool IsOnlineMode => !IsOfflineMode; + partial void OnIsOfflineModeChanged(bool value) => OnPropertyChanged(nameof(IsOnlineMode)); + + [ObservableProperty] + private bool _isUpdating = false; + + [ObservableProperty] + private bool _isInstalling = false; + + [ObservableProperty] + private bool _isNeedingInstall = false; + + [ObservableProperty] + private bool _isCheckingUpdates = false; + + public bool IsCheckingOrUpdating => IsCheckingUpdates || IsUpdating || IsInstalling; + public bool IsUpdatingOrInstalling => IsUpdating || IsInstalling; + + partial void OnIsCheckingUpdatesChanged(bool value) => OnPropertyChanged(nameof(IsCheckingOrUpdating)); + partial void OnIsInstallingChanged(bool value) + { + OnPropertyChanged(nameof(LaunchButtonText)); + OnPropertyChanged(nameof(IsCheckingOrUpdating)); + OnPropertyChanged(nameof(IsUpdatingOrInstalling)); + } + partial void OnIsNeedingInstallChanged(bool value) => OnPropertyChanged(nameof(LaunchButtonText)); + + [ObservableProperty] + private string _updateStatus = ""; + + [ObservableProperty] + private string _updateStatusFile = ""; + + [ObservableProperty] + private string _updateStatusSpeed = ""; + + [ObservableProperty] + private double _updateProgress = 0; + + [ObservableProperty] + private bool _updateIndeterminate = false; + + [ObservableProperty] + private bool _updateAvailable = false; + + public string LaunchButtonText => + IsInstalling ? "Installing Game..." : + IsUpdating ? "Updating..." : + IsNeedingInstall ? "Install Game" : + UpdateAvailable ? "Update" : + "Launch Game"; + + partial void OnIsUpdatingChanged(bool value) + { + OnPropertyChanged(nameof(LaunchButtonText)); + OnPropertyChanged(nameof(IsCheckingOrUpdating)); + OnPropertyChanged(nameof(IsUpdatingOrInstalling)); + } + partial void OnUpdateAvailableChanged(bool value) => OnPropertyChanged(nameof(LaunchButtonText)); + + [ObservableProperty] + private ServerInfo? _selectedServer; + + // What the SELECTED SERVER label shows + public string SelectedLabel => SelectedServer?.IsNone == false + ? SelectedServer.Name + : "Server not selected..."; + + public bool IsNoServerSelected => SelectedServer == null || SelectedServer.IsNone; + public bool IsServerSelected => SelectedServer != null && !SelectedServer.IsNone; + + public ObservableCollection Servers { get; } = new() + { + // ── None (clears selection) ────────────────────────────────────── + new ServerInfo { Name = "None", IpPort = "", IsOnline = false }, + + // ── Real servers ───────────────────────────────────────────────── + new ServerInfo { Name = "NA | PUG | 64 Tick", IpPort = "na.classiccounter.cc:27015", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "NA | PUG-2 | 64 Tick", IpPort = "na.classiccounter.cc:27016", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "EU | PUG | 64 Tick", IpPort = "eu.classiccounter.cc:27016", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "EU | PUG | 128 Tick", IpPort = "eu.classiccounter.cc:27015", Players = 0, MaxPlayers = 10, IsOnline = true }, + new ServerInfo { Name = "EU | PUG-2 | 128 Tick",IpPort = "eu.classiccounter.cc:27022", Players = 0, MaxPlayers = 10, IsOnline = true }, + }; + + partial void OnSelectedServerChanged(ServerInfo? value) + { + // Update the label shown in the trigger button + ProtocolManager = (value == null || value.IsNone) ? "None" : value.Name; + OnPropertyChanged(nameof(SelectedLabel)); + OnPropertyChanged(nameof(IsNoServerSelected)); + OnPropertyChanged(nameof(IsServerSelected)); + } + + public MainWindowViewModel() + { + if (Argument.HasProtocolCommand()) + ProtocolManager = "Ready to Launch!"; + + _ = LoadSelfProfileAsync(); + + CheckWhitelistStatus(); + UpdateOfflineMode(); + NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; + + // Query servers immediately, then refresh every 30 seconds + _ = RefreshServersSafeAsync(); + _serverRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; + _serverRefreshTimer.Tick += async (_, _) => await RefreshServersSafeAsync(); + _serverRefreshTimer.Start(); + + // Query friends immediately, then refresh every 30 seconds + _ = RefreshFriendsSafeAsync(); + _friendsTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; + _friendsTimer.Tick += async (_, _) => await RefreshFriendsSafeAsync(); + _friendsTimer.Start(); + } + + private DispatcherTimer? _serverRefreshTimer; + private int _serverRefreshInProgress; + + // ── Friends ─────────────────────────────────────────────────────────────── + public ObservableCollection Friends { get; } = new(); + + [ObservableProperty] private bool _friendsShowStatus = true; + [ObservableProperty] private string _friendsStatus = "Loading..."; + [ObservableProperty] private bool _showNoFriendsState = false; + public bool ShowGenericFriendsStatus => FriendsShowStatus && !ShowNoFriendsState; + + partial void OnFriendsShowStatusChanged(bool value) => OnPropertyChanged(nameof(ShowGenericFriendsStatus)); + partial void OnShowNoFriendsStateChanged(bool value) => OnPropertyChanged(nameof(ShowGenericFriendsStatus)); + + private DispatcherTimer? _friendsTimer; + private int _friendsRefreshInProgress; + private string _lastRenderedFriendsSignature = string.Empty; + private string _lastKnownSteamId2 = string.Empty; + + private async Task LoadSelfProfileAsync() + { + try + { + bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); + if (!hasSteam || string.IsNullOrWhiteSpace(Steam.recentSteamID64)) + return; + + var rawSelfJson = await Api.Eddies.GetSelfInfo(Steam.recentSteamID64); + var self = Api.ParseSelfInfoPayload(rawSelfJson); + if (self == null) + return; + + Dispatcher.UIThread.Post(() => + { + if (!string.IsNullOrWhiteSpace(self.AvatarUrl)) + ProfilePicture = AvatarCache.GetDisplaySource(self.AvatarUrl); + + if (!string.IsNullOrWhiteSpace(self.Username)) + UsernameGreeting = $"Hello, {self.Username}"; + }); + } + catch + { + // Best-effort profile load; keep defaults on failure. + } + } + + private async Task RefreshServersAsync() + { + if (IsOfflineMode) + { + foreach (var s in Servers.Where(s => !s.IsNone)) + { + s.IsOnline = false; + s.Players = 0; + s.MaxPlayers = 0; + s.Map = ""; + } + return; + } + + await ServerQuery.RefreshServers(Servers.Where(s => !s.IsNone)); + + // Re-order by player count descending; None always stays at index 0 + var sorted = Servers.Where(s => !s.IsNone) + .OrderByDescending(s => s.Players) + .ToList(); + int insertAt = 1; + foreach (var server in sorted) + { + int from = Servers.IndexOf(server); + if (from != insertAt) + Servers.Move(from, insertAt); + insertAt++; + } + } + + private async Task RefreshServersSafeAsync() + { + if (Interlocked.Exchange(ref _serverRefreshInProgress, 1) == 1) + return; + + try + { + await RefreshServersAsync(); + } + finally + { + Interlocked.Exchange(ref _serverRefreshInProgress, 0); + } + } + + private async Task RefreshFriendsAsync() + { + try + { + if (IsOfflineMode) + { + var steamIdForCache = !string.IsNullOrWhiteSpace(_lastKnownSteamId2) + ? _lastKnownSteamId2 + : (Steam.recentSteamID2 ?? string.Empty); + + if (TryShowCachedFriends(steamIdForCache, forceOfflineStatus: true)) + return; + + Dispatcher.UIThread.Post(() => + { + Friends.Clear(); + _lastRenderedFriendsSignature = string.Empty; + ShowNoFriendsState = false; + FriendsStatus = "Offline mode"; + FriendsShowStatus = true; + }); + return; + } + + bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); + string steamId = Steam.recentSteamID2 ?? string.Empty; + if (!string.IsNullOrWhiteSpace(steamId)) + _lastKnownSteamId2 = steamId; + + if (!hasSteam) + { + Dispatcher.UIThread.Post(() => + { + ShowNoFriendsState = false; + FriendsStatus = "Steam is not installed."; + FriendsShowStatus = true; + }); + return; + } + + if (string.IsNullOrEmpty(Steam.recentSteamID2)) + { + Dispatcher.UIThread.Post(() => + { + ShowNoFriendsState = false; + FriendsStatus = "Sign in to Steam to see friends."; + FriendsShowStatus = true; + }); + return; + } + + string rawFriendsJson; + try + { + rawFriendsJson = await Api.Eddies.GetFriends(Steam.recentSteamID64 ?? string.Empty); + } + catch + { + rawFriendsJson = await Api.Eddies.GetFriendsBySteamId2(Steam.recentSteamID2 ?? string.Empty); + } + var apiFriends = Api.ParseFriendsPayload(rawFriendsJson) + .OrderBy(f => f.Status == "Offline" ? 1 : 0) + .ToList(); + + await FriendsCache.SaveAsync(steamId, apiFriends); + + Dispatcher.UIThread.Post(() => + { + var sorted = apiFriends; + + foreach (var f in sorted) + f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); + + var signature = BuildFriendsSignature(sorted); + if (!string.Equals(signature, _lastRenderedFriendsSignature, StringComparison.Ordinal)) + { + Friends.Clear(); + foreach (var f in sorted) + Friends.Add(f); + _lastRenderedFriendsSignature = signature; + } + + FriendsShowStatus = Friends.Count == 0; + ShowNoFriendsState = Friends.Count == 0; + FriendsStatus = Friends.Count == 0 ? "No friends found." : ""; + }); + } + catch + { + if (TryShowCachedFriends(Steam.recentSteamID2 ?? string.Empty, forceOfflineStatus: true)) + return; + + Dispatcher.UIThread.Post(() => + { + Friends.Clear(); + _lastRenderedFriendsSignature = string.Empty; + ShowNoFriendsState = false; + FriendsStatus = IsOfflineMode ? "Offline mode" : "Couldn't load friends right now."; + FriendsShowStatus = true; + }); + } + } + + private bool TryShowCachedFriends(string steamId, bool forceOfflineStatus) + { + var cached = FriendsCache.Load(steamId); + if (cached.Count == 0) + return false; + + var sorted = cached + .OrderBy(f => f.Status == "Offline" ? 1 : 0) + .ToList(); + + if (forceOfflineStatus) + { + foreach (var f in sorted) + f.Status = "Offline"; + } + + foreach (var f in sorted) + f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); + + Dispatcher.UIThread.Post(() => + { + var signature = BuildFriendsSignature(sorted); + if (!string.Equals(signature, _lastRenderedFriendsSignature, StringComparison.Ordinal)) + { + Friends.Clear(); + foreach (var f in sorted) + Friends.Add(f); + _lastRenderedFriendsSignature = signature; + } + + FriendsShowStatus = false; + ShowNoFriendsState = false; + FriendsStatus = ""; + }); + + return true; + } + + private static string BuildFriendsSignature(IEnumerable friends) + { + var sb = new StringBuilder(); + foreach (var f in friends) + { + sb.Append(f.Username ?? string.Empty) + .Append('\u001f') + .Append(f.AvatarUrl ?? string.Empty) + .Append('\u001f') + .Append(f.Status ?? "Offline") + .Append('\u001e'); + } + return sb.ToString(); + } + + private async Task RefreshFriendsSafeAsync() + { + if (Interlocked.Exchange(ref _friendsRefreshInProgress, 1) == 1) + return; + + try + { + await RefreshFriendsAsync(); + } + finally + { + Interlocked.Exchange(ref _friendsRefreshInProgress, 0); + } + } + + + + + + private void CheckWhitelistStatus() + { + Task.Run(async () => + { + try + { + bool hasSteam = await Steam.GetRecentLoggedInSteamID(false); + if (!hasSteam) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = "Gray"; + WhitelistText = "Unknown"; + }); + return; + } + + if (string.IsNullOrEmpty(Steam.recentSteamID2)) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = "Gray"; + WhitelistText = "Unknown"; + }); + return; + } + + var response = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); + bool whitelisted = response?.Files != null && response.Files.Count > 0; + + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = whitelisted ? "#4CAF50" : "#F44336"; + WhitelistText = whitelisted ? "Whitelisted" : "Not Whitelisted"; + }); + } + catch + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + WhitelistDotColor = "Gray"; + WhitelistText = "Unknown"; + }); + } + }); + } + + private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) + { + Dispatcher.UIThread.Post(UpdateOfflineMode); + } + + private void UpdateOfflineMode() + { + IsOfflineMode = !NetworkInterface.GetIsNetworkAvailable(); + } + } +} From c20d89097e3bcb2a6480821f1ad671c73948b286 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:16:54 -0400 Subject: [PATCH 19/51] restore launch options field in settings --- Wauncher/Views/SettingsWindow.axaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Wauncher/Views/SettingsWindow.axaml b/Wauncher/Views/SettingsWindow.axaml index cb9ba0f..b04c0a1 100644 --- a/Wauncher/Views/SettingsWindow.axaml +++ b/Wauncher/Views/SettingsWindow.axaml @@ -173,6 +173,30 @@ OnContent="" /> + + + + + + From b98fe6b9aa99f2ab2c1f67cd87b0c74902337a97 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:17:35 -0400 Subject: [PATCH 20/51] restore launch options and move settings path --- .../ViewModels/SettingsWindowViewModel.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Wauncher/ViewModels/SettingsWindowViewModel.cs b/Wauncher/ViewModels/SettingsWindowViewModel.cs index d2f10e2..f415548 100644 --- a/Wauncher/ViewModels/SettingsWindowViewModel.cs +++ b/Wauncher/ViewModels/SettingsWindowViewModel.cs @@ -16,6 +16,9 @@ public partial class SettingsWindowViewModel : ViewModelBase [ObservableProperty] private bool _skipUpdates = false; + [ObservableProperty] + private string _launchOptions = string.Empty; + public SettingsWindowViewModel() { Load(); @@ -23,6 +26,7 @@ public SettingsWindowViewModel() partial void OnMinimizeToTrayChanged(bool value) => Save(); partial void OnSkipUpdatesChanged(bool value) => Save(); + partial void OnLaunchOptionsChanged(string value) => Save(); partial void OnDiscordRpcChanged(bool value) { @@ -51,6 +55,7 @@ private void Load() case "MinimizeToTray": MinimizeToTray = value.Trim() == "true"; break; case "DiscordRpc": DiscordRpc = value.Trim() == "true"; break; case "SkipUpdates": SkipUpdates = value.Trim() == "true"; break; + case "LaunchOptions": LaunchOptions = value; break; } } } @@ -66,6 +71,7 @@ public void Save() $"MinimizeToTray={MinimizeToTray.ToString().ToLower()}", $"DiscordRpc={DiscordRpc.ToString().ToLower()}", $"SkipUpdates={SkipUpdates.ToString().ToLower()}", + $"LaunchOptions={LaunchOptions}", }); } catch { } @@ -73,7 +79,25 @@ public void Save() public static SettingsWindowViewModel LoadGlobal() => new(); - public static string SettingsPath() => - Path.Combine(new FileInfo(System.Environment.ProcessPath ?? "").Directory?.FullName ?? Directory.GetCurrentDirectory(), "wauncher_settings.cfg"); + public static string SettingsPath() + { + var configDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "config"); + var newPath = Path.Combine(configDir, "wauncher_settings.cfg"); + + try + { + Directory.CreateDirectory(configDir); + } + catch + { + // Fall back to returning the new path even if folder creation fails. + } + + return newPath; + } } } From d0f8ad8ca16707f9f6cae8eaf64768d7dbc01890 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:52:43 -0400 Subject: [PATCH 21/51] only recheck updates when skip-updates changes From f6ffee8d01dcbf181d58d9ea552267e9b0a0a1c5 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:54:34 -0400 Subject: [PATCH 22/51] only recheck updates when skip-updates changes --- Wauncher/Views/MainWindow.axaml.cs | 2630 ++++++++++++++-------------- 1 file changed, 1316 insertions(+), 1314 deletions(-) diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index ccb29be..c905bf5 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -1,64 +1,64 @@ -using System.IO; -using System.Net.Http; -using System.Linq; -using System.Net.NetworkInformation; -using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Text.Json; -using System.Text; -using System.Text.RegularExpressions; -using Avalonia.Animation; -using Avalonia.Animation.Easings; -using Avalonia; +using System.IO; +using System.Net.Http; +using System.Linq; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Text.Json; +using System.Text; +using System.Text.RegularExpressions; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Threading; -using Wauncher.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Wauncher.ViewModels; -using Wauncher.Views; +using Avalonia.Platform; +using Avalonia.Threading; +using Wauncher.Utils; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Wauncher.ViewModels; +using Wauncher.Views; namespace Wauncher.Views { - public partial class MainWindow : Window - { - private InfoWindow? _infoWindow = null; - private SettingsWindow? _settingsWindow = null; - private SettingsWindowViewModel _settings; - private int _launchInProgress; - private int _updateInProgress; - private int _installInProgress; - - private bool _dropdownOpen = false; + public partial class MainWindow : Window + { + private InfoWindow? _infoWindow = null; + private SettingsWindow? _settingsWindow = null; + private SettingsWindowViewModel _settings; + private int _launchInProgress; + private int _updateInProgress; + private int _installInProgress; + + private bool _dropdownOpen = false; private const double HeightClosed = 720; private const double HeightOpen = 720; // ── Image carousel (center content area) ────────────────────────────────── - private Image[] _carouselImages = Array.Empty(); - private DispatcherTimer? _carouselTimer; - private int _currentCarouselIndex = 0; - private const int CarouselRotationIntervalSeconds = 5; - private readonly List _zoomCts = new(); - private static string WauncherDirectory => - Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); + private Image[] _carouselImages = Array.Empty(); + private DispatcherTimer? _carouselTimer; + private int _currentCarouselIndex = 0; + private const int CarouselRotationIntervalSeconds = 5; + private readonly List _zoomCts = new(); + private static string WauncherDirectory => + Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); public MainWindow() { InitializeComponent(); _settings = SettingsWindowViewModel.LoadGlobal(); - this.Loaded += (_, _) => - { - var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = buttonColor; - ArrowButton.Background = buttonColor; - LaunchUpdateButton.IsEnabled = true; - }; + this.Loaded += (_, _) => + { + var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = buttonColor; + ArrowButton.Background = buttonColor; + LaunchUpdateButton.IsEnabled = true; + }; this.Opened += (_, _) => { @@ -93,72 +93,72 @@ public MainWindow() this.Closed += (_, _) => TeardownCarousel(); } - // ── Image carousel (center content area) ────────────────────────────────── - private static readonly HttpClient _http = new(); - private static string PatchNotesCachePath => - Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "cache", - "patchnotes.md"); - - private async Task SetupCarouselAsync() - { - try - { - TeardownCarousel(); - - var carouselContainer = this.FindControl("CarouselContainer"); - var offlinePanel = this.FindControl("CarouselOfflinePanel"); - var offlineSubText = this.FindControl("CarouselOfflineSubText"); - if (carouselContainer == null) - return; - - bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); - var bitmaps = hasInternet - ? await LoadCarouselFromGitHubAsync() - : null; - - if (bitmaps == null || bitmaps.Count == 0) - { - if (offlinePanel != null) - offlinePanel.IsVisible = true; - if (offlineSubText != null) - { - offlineSubText.Text = hasInternet - ? "Carousel is temporarily unavailable." - : "Connect to Wi-Fi or Ethernet to load the carousel."; - } - return; - } - - if (offlinePanel != null) - offlinePanel.IsVisible = false; - - _carouselImages = CreateCarouselImages(bitmaps); - EnsureZoomSlots(_carouselImages.Length); - - foreach (var existingImage in carouselContainer.Children.OfType().ToList()) - carouselContainer.Children.Remove(existingImage); - - int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; - for (int i = 0; i < _carouselImages.Length; i++) - { - if (overlayIndex >= 0) - { - carouselContainer.Children.Insert(overlayIndex, _carouselImages[i]); - overlayIndex++; - } - else - { - carouselContainer.Children.Add(_carouselImages[i]); - } - } - - _currentCarouselIndex = 0; - _carouselImages[0].Opacity = 1.0; - StartZoomOut(_carouselImages[0], 0); + // ── Image carousel (center content area) ────────────────────────────────── + private static readonly HttpClient _http = new(); + private static string PatchNotesCachePath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache", + "patchnotes.md"); + + private async Task SetupCarouselAsync() + { + try + { + TeardownCarousel(); + + var carouselContainer = this.FindControl("CarouselContainer"); + var offlinePanel = this.FindControl("CarouselOfflinePanel"); + var offlineSubText = this.FindControl("CarouselOfflineSubText"); + if (carouselContainer == null) + return; + + bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); + var bitmaps = hasInternet + ? await LoadCarouselFromGitHubAsync() + : null; + + if (bitmaps == null || bitmaps.Count == 0) + { + if (offlinePanel != null) + offlinePanel.IsVisible = true; + if (offlineSubText != null) + { + offlineSubText.Text = hasInternet + ? "Carousel is temporarily unavailable." + : "Connect to Wi-Fi or Ethernet to load the carousel."; + } + return; + } + + if (offlinePanel != null) + offlinePanel.IsVisible = false; + + _carouselImages = CreateCarouselImages(bitmaps); + EnsureZoomSlots(_carouselImages.Length); + + foreach (var existingImage in carouselContainer.Children.OfType().ToList()) + carouselContainer.Children.Remove(existingImage); + + int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; + for (int i = 0; i < _carouselImages.Length; i++) + { + if (overlayIndex >= 0) + { + carouselContainer.Children.Insert(overlayIndex, _carouselImages[i]); + overlayIndex++; + } + else + { + carouselContainer.Children.Add(_carouselImages[i]); + } + } + + _currentCarouselIndex = 0; + _carouselImages[0].Opacity = 1.0; + StartZoomOut(_carouselImages[0], 0); _carouselTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CarouselRotationIntervalSeconds) }; _carouselTimer.Tick += (_, _) => RotateCarousel(); @@ -170,35 +170,35 @@ private async Task SetupCarouselAsync() } } - private async Task?> LoadCarouselFromGitHubAsync() - { - try - { - var json = await Api.GitHub.GetCarouselAssetsWauncher(); - var assets = JsonConvert.DeserializeObject>(json); - if (assets == null || assets.Count == 0) - return null; - - var urls = assets - .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) - .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) - .OrderBy(a => GetCarouselSortIndex(a.Name)) - .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) - .Select(a => a.DownloadUrl!) - .ToList(); - - if (urls.Count == 0) - return null; - - var bitmaps = new List(); - foreach (var url in urls) - { - try + private async Task?> LoadCarouselFromGitHubAsync() + { + try + { + var json = await Api.GitHub.GetCarouselAssetsWauncher(); + var assets = JsonConvert.DeserializeObject>(json); + if (assets == null || assets.Count == 0) + return null; + + var urls = assets + .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) + .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) + .OrderBy(a => GetCarouselSortIndex(a.Name)) + .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) + .Select(a => a.DownloadUrl!) + .ToList(); + + if (urls.Count == 0) + return null; + + var bitmaps = new List(); + foreach (var url in urls) + { + try { var bytes = await _http.GetByteArrayAsync(url); using var ms = new MemoryStream(bytes); @@ -207,84 +207,84 @@ private async Task SetupCarouselAsync() catch { } } return bitmaps.Count > 0 ? bitmaps : null; - } - catch { return null; } - } - - private static int GetCarouselSortIndex(string name) - { - var match = Regex.Match(name, @"^carousel_(\d+)", RegexOptions.IgnoreCase); - if (match.Success && int.TryParse(match.Groups[1].Value, out var index)) - return index; - return int.MaxValue; - } - - private sealed class GitHubAssetEntry - { - [JsonProperty("name")] - public string Name { get; set; } = string.Empty; - - [JsonProperty("type")] - public string Type { get; set; } = string.Empty; - - [JsonProperty("download_url")] - public string? DownloadUrl { get; set; } - } - - private static Image[] CreateCarouselImages(IReadOnlyList bitmaps) - { - var images = new Image[bitmaps.Count]; - for (int i = 0; i < bitmaps.Count; i++) - { - images[i] = new Image - { - Source = bitmaps[i], - Stretch = Stretch.UniformToFill, - Opacity = 0.0, - Transitions = new Transitions - { - new DoubleTransition - { - Property = Visual.OpacityProperty, - Duration = TimeSpan.FromSeconds(1.5), - Easing = new CubicEaseInOut() - } - } - }; - } - - return images; - } - - private void EnsureZoomSlots(int count) - { - while (_zoomCts.Count < count) - _zoomCts.Add(null); - } - - private void RotateCarousel() - { - if (_carouselImages.Length == 0) - return; - - // Fade out current image (zoom continues through the crossfade) - _carouselImages[_currentCarouselIndex].Opacity = 0.0; - - // Move to next image - _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; - - // Fade in next image and start fresh zoom-out - StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); - _carouselImages[_currentCarouselIndex].Opacity = 1.0; - } - - private void TeardownCarousel() - { - _carouselTimer?.Stop(); - _carouselTimer = null; - for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); - _carouselImages = Array.Empty(); - } + } + catch { return null; } + } + + private static int GetCarouselSortIndex(string name) + { + var match = Regex.Match(name, @"^carousel_(\d+)", RegexOptions.IgnoreCase); + if (match.Success && int.TryParse(match.Groups[1].Value, out var index)) + return index; + return int.MaxValue; + } + + private sealed class GitHubAssetEntry + { + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("type")] + public string Type { get; set; } = string.Empty; + + [JsonProperty("download_url")] + public string? DownloadUrl { get; set; } + } + + private static Image[] CreateCarouselImages(IReadOnlyList bitmaps) + { + var images = new Image[bitmaps.Count]; + for (int i = 0; i < bitmaps.Count; i++) + { + images[i] = new Image + { + Source = bitmaps[i], + Stretch = Stretch.UniformToFill, + Opacity = 0.0, + Transitions = new Transitions + { + new DoubleTransition + { + Property = Visual.OpacityProperty, + Duration = TimeSpan.FromSeconds(1.5), + Easing = new CubicEaseInOut() + } + } + }; + } + + return images; + } + + private void EnsureZoomSlots(int count) + { + while (_zoomCts.Count < count) + _zoomCts.Add(null); + } + + private void RotateCarousel() + { + if (_carouselImages.Length == 0) + return; + + // Fade out current image (zoom continues through the crossfade) + _carouselImages[_currentCarouselIndex].Opacity = 0.0; + + // Move to next image + _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; + + // Fade in next image and start fresh zoom-out + StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); + _carouselImages[_currentCarouselIndex].Opacity = 1.0; + } + + private void TeardownCarousel() + { + _carouselTimer?.Stop(); + _carouselTimer = null; + for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); + _carouselImages = Array.Empty(); + } private void StartZoomOut(Image img, int slot) { @@ -314,28 +314,28 @@ private void StartZoomOut(Image img, int slot) zoomTimer.Start(); } - private void StopZoom(int slot) - { - if (slot < 0 || slot >= _zoomCts.Count) - return; - - _zoomCts[slot]?.Cancel(); - _zoomCts[slot] = null; - } + private void StopZoom(int slot) + { + if (slot < 0 || slot >= _zoomCts.Count) + return; + + _zoomCts[slot]?.Cancel(); + _zoomCts[slot] = null; + } // ── Server dropdown ─────────────────────────────────────────── - private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) - { - CloseDropdown(); - return; - } - - _dropdownOpen = !_dropdownOpen; - - if (DataContext is MainWindowViewModel vm) - vm.IsDropdownOpen = _dropdownOpen; + private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) + { + CloseDropdown(); + return; + } + + _dropdownOpen = !_dropdownOpen; + + if (DataContext is MainWindowViewModel vm) + vm.IsDropdownOpen = _dropdownOpen; if (_dropdownOpen) { @@ -367,54 +367,54 @@ private void CloseDropdown() } // ── Game launch ─────────────────────────────────────────── - private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - - if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || Volatile.Read(ref _launchInProgress) == 1) - return; - else if (vm.IsNeedingInstall) - _ = InstallGameFromCdnAsync(); - else if (_settings.SkipUpdates) - _ = LaunchGameAsync(); - else if (vm.UpdateAvailable) - { - if (_selfUpdateAvailable) - _ = Button_SelfUpdateAsync(); - else - Button_Update(sender, e); - } - else - _ = LaunchGameAsync(); - } - - private async Task LaunchGameAsync() - { - if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) - return; - - var vm = DataContext as MainWindowViewModel; - try - { - _settings = SettingsWindowViewModel.LoadGlobal(); - + private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || Volatile.Read(ref _launchInProgress) == 1) + return; + else if (vm.IsNeedingInstall) + _ = InstallGameFromCdnAsync(); + else if (_settings.SkipUpdates) + _ = LaunchGameAsync(); + else if (vm.UpdateAvailable) + { + if (_selfUpdateAvailable) + _ = Button_SelfUpdateAsync(); + else + Button_Update(sender, e); + } + else + _ = LaunchGameAsync(); + } + + private async Task LaunchGameAsync() + { + if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) + return; + + var vm = DataContext as MainWindowViewModel; + try + { + _settings = SettingsWindowViewModel.LoadGlobal(); + if (vm != null) vm.GameStatus = "Running"; - // Clear any arguments left over from a previous launch before adding new ones. - Argument.ClearAdditionalArguments(); - - Argument.AddArgument("-novid"); - if (!string.IsNullOrWhiteSpace(_settings.LaunchOptions)) - { - foreach (var arg in ParseLaunchOptions(_settings.LaunchOptions)) - Argument.AddArgument(arg); - } - - var selected = vm?.SelectedServer; - if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) - { + // Clear any arguments left over from a previous launch before adding new ones. + Argument.ClearAdditionalArguments(); + + Argument.AddArgument("-novid"); + if (!string.IsNullOrWhiteSpace(_settings.LaunchOptions)) + { + foreach (var arg in ParseLaunchOptions(_settings.LaunchOptions)) + Argument.AddArgument(arg); + } + + var selected = vm?.SelectedServer; + if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) + { Argument.AddArgument("+connect"); Argument.AddArgument(selected.IpPort); } @@ -432,16 +432,16 @@ private async Task LaunchGameAsync() await Game.Monitor(); } - catch (Exception ex) - { - Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); - } - finally - { - if (vm != null) vm.GameStatus = "Not Running"; - Interlocked.Exchange(ref _launchInProgress, 0); - } - } + catch (Exception ex) + { + Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); + } + finally + { + if (vm != null) vm.GameStatus = "Not Running"; + Interlocked.Exchange(ref _launchInProgress, 0); + } + } // ── Window chrome ─────────────────────────────────────────── private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) @@ -471,215 +471,215 @@ public void ForceQuit() Close(); } - private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var dir = WauncherDirectory; - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = dir, - UseShellExecute = true - }); - } - - private void VerifyGameFiles_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is not MainWindowViewModel vm) - return; - - if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || vm.IsNeedingInstall) - return; - - _forceValidateAllOnce = true; - _cachedPatches = null; - Button_Update(sender, e); - } - - // ── Update ───────────────────────────────────────────────────── - private CancellationTokenSource? _updateCts; - private Patches? _cachedPatches; - private bool _selfUpdateAvailable; - private string _selfUpdateDownloadUrl = string.Empty; - private string _selfUpdateVersion = string.Empty; - private int _autoSelfUpdateTriggered; - private bool _forceValidateAllOnce; + private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var dir = WauncherDirectory; + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = dir, + UseShellExecute = true + }); + } + + private void VerifyGameFiles_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is not MainWindowViewModel vm) + return; + + if (vm.IsCheckingUpdates || vm.IsUpdating || vm.IsInstalling || vm.IsNeedingInstall) + return; + + _forceValidateAllOnce = true; + _cachedPatches = null; + Button_Update(sender, e); + } + + // ── Update ───────────────────────────────────────────────────── + private CancellationTokenSource? _updateCts; + private Patches? _cachedPatches; + private bool _selfUpdateAvailable; + private string _selfUpdateDownloadUrl = string.Empty; + private string _selfUpdateVersion = string.Empty; + private int _autoSelfUpdateTriggered; + private bool _forceValidateAllOnce; /// /// Called on window open. If csgo.exe is missing, triggers a full CDN install. /// Otherwise runs the normal patch update check. /// - private async Task StartupAsync() - { - // Yield to let Avalonia finish its initial layout/styling pass - // (Loaded sets the button disabled/gray; we need that to settle before overriding) - await Task.Delay(50); - - LaunchUpdateButton.IsEnabled = true; - - string csgoExe = Path.Combine(WauncherDirectory, "csgo.exe"); - if (DataContext is not MainWindowViewModel vm) - return; - - if (!File.Exists(csgoExe)) - { - vm.IsNeedingInstall = true; - var blue = new SolidColorBrush(Color.Parse("#2196F3")); - LaunchUpdateButton.Background = blue; - ArrowButton.Background = blue; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - if (vm?.IsOfflineMode == true) - { - vm.IsNeedingInstall = false; - vm.UpdateAvailable = false; - var green = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = green; - ArrowButton.Background = green; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - _settings = SettingsWindowViewModel.LoadGlobal(); - if (_settings.SkipUpdates) - { - vm!.IsNeedingInstall = false; - vm.UpdateAvailable = false; - vm.IsCheckingUpdates = false; - var green = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = green; - ArrowButton.Background = green; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); - LaunchUpdateButton.IsEnabled = true; - return; - } - - await CheckForUpdatesAsync(); - } - - private async Task InstallGameFromCdnAsync() - { - if (DataContext is not MainWindowViewModel vm) return; - if (Interlocked.Exchange(ref _installInProgress, 1) == 1) - return; - - bool installSucceeded = false; - vm.IsNeedingInstall = false; - vm.IsInstalling = true; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = "Connecting..."; + private async Task StartupAsync() + { + // Yield to let Avalonia finish its initial layout/styling pass + // (Loaded sets the button disabled/gray; we need that to settle before overriding) + await Task.Delay(50); + + LaunchUpdateButton.IsEnabled = true; + + string csgoExe = Path.Combine(WauncherDirectory, "csgo.exe"); + if (DataContext is not MainWindowViewModel vm) + return; + + if (!File.Exists(csgoExe)) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + if (vm?.IsOfflineMode == true) + { + vm.IsNeedingInstall = false; + vm.UpdateAvailable = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + _settings = SettingsWindowViewModel.LoadGlobal(); + if (_settings.SkipUpdates) + { + vm!.IsNeedingInstall = false; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var green = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = green; + ArrowButton.Background = green; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #554CAF50"); + LaunchUpdateButton.IsEnabled = true; + return; + } + + await CheckForUpdatesAsync(); + } + + private async Task InstallGameFromCdnAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + if (Interlocked.Exchange(ref _installInProgress, 1) == 1) + return; + + bool installSucceeded = false; + vm.IsNeedingInstall = false; + vm.IsInstalling = true; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Connecting..."; vm.UpdateStatusSpeed = ""; - try - { - await DownloadManager.InstallFullGame( - onProgress: (file, speed, percent) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = $"Installing {ShortFileName(file)} {percent:F0}%"; - vm.UpdateStatusSpeed = string.IsNullOrWhiteSpace(speed) ? "" : speed; - vm.UpdateProgress = percent; - vm.UpdateIndeterminate = false; - }); - }, - onStatus: status => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = status; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); - if (!vm.UpdateIndeterminate) - vm.UpdateProgress = 0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = extractPercent; - }); - }); - - // Immediately apply any post-install patches so first-time installs - // end in a launch-ready state without requiring a second manual update. - Dispatcher.UIThread.Post(() => - { - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = "Checking post-install patches..."; - vm.UpdateStatusSpeed = ""; - }); - - var patches = await Task.Run(() => PatchManager.ValidatePatches()); - var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); - if (allPatches.Count > 0) - { - int totalFiles = allPatches.Count; - int completed = 0; - - foreach (var patch in allPatches) - { - var extractWatch = new System.Diagnostics.Stopwatch(); - await DownloadManager.DownloadPatch( - patch, - onProgress: (p) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; - vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); - vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; - }); - }, - onExtract: () => - { - extractWatch.Restart(); - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); - vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; - }); - }); - - completed++; - vm.UpdateProgress = (double)completed / totalFiles * 100.0; - } - } - - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = "Game installed and fully updated!"; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = false; - vm.UpdateProgress = 100; - }); - installSucceeded = true; - await Task.Delay(1500); - } - catch (Exception ex) - { - vm.UpdateStatusFile = $"Install error: {ex.Message}"; + try + { + await DownloadManager.InstallFullGame( + onProgress: (file, speed, percent) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = $"Installing {ShortFileName(file)} {percent:F0}%"; + vm.UpdateStatusSpeed = string.IsNullOrWhiteSpace(speed) ? "" : speed; + vm.UpdateProgress = percent; + vm.UpdateIndeterminate = false; + }); + }, + onStatus: status => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = status; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); + if (!vm.UpdateIndeterminate) + vm.UpdateProgress = 0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = extractPercent; + }); + }); + + // Immediately apply any post-install patches so first-time installs + // end in a launch-ready state without requiring a second manual update. + Dispatcher.UIThread.Post(() => + { + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = "Checking post-install patches..."; + vm.UpdateStatusSpeed = ""; + }); + + var patches = await Task.Run(() => PatchManager.ValidatePatches()); + var allPatches = patches.Missing.Concat(patches.Outdated).ToList(); + if (allPatches.Count > 0) + { + int totalFiles = allPatches.Count; + int completed = 0; + + foreach (var patch in allPatches) + { + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); + + completed++; + vm.UpdateProgress = (double)completed / totalFiles * 100.0; + } + } + + Dispatcher.UIThread.Post(() => + { + vm.UpdateStatusFile = "Game installed and fully updated!"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + vm.UpdateProgress = 100; + }); + installSucceeded = true; + await Task.Delay(1500); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Install error: {ex.Message}"; vm.UpdateStatusSpeed = ""; await Task.Delay(4000); } @@ -687,196 +687,196 @@ await DownloadManager.DownloadPatch( { vm.IsInstalling = false; vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - - _cachedPatches = null; - - if (!installSucceeded && !File.Exists(Path.Combine(WauncherDirectory, "csgo.exe"))) - { - vm.IsNeedingInstall = true; - var blue = new SolidColorBrush(Color.Parse("#2196F3")); - LaunchUpdateButton.Background = blue; - ArrowButton.Background = blue; - LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); - } - else - { - try - { - await CheckForUpdatesAsync(); - } - catch { } - } - - LaunchUpdateButton.IsEnabled = true; - Interlocked.Exchange(ref _installInProgress, 0); - } - } - - private async Task LoadPatchNotesAsync() - { - try - { - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Loading latest patch notes..."; - PatchNotesVersion.IsVisible = true; - }); - - if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) - { - Dispatcher.UIThread.Post(() => - { - var cachedItems = LoadCachedPatchNotes(); - if (cachedItems.Count > 0) - { - PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; - PatchNotesList.ItemsSource = cachedItems; - } - else - { - PatchNotesVersion.Text = "Patch notes are unavailable offline."; - PatchNotesList.ItemsSource = new List(); - } - - PatchNotesVersion.IsVisible = true; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - return; - } - - var md = await Api.GitHub.GetPatchNotesWauncher(); - var items = ParsePatchNotes(md); - SavePatchNotesCache(md); - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = items.Count > 0 - ? $"Updated {DateTime.Now:MMM d, h:mm tt}" - : "Patch notes are currently empty."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - } - catch - { - var items = LoadCachedPatchNotes(); - if (items.Count == 0) - { - items = BuildFallbackPatchNotes(); - } - - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Using fallback patch notes."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); - } - } - - private static void SavePatchNotesCache(string markdown) - { - try - { - var directory = Path.GetDirectoryName(PatchNotesCachePath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(PatchNotesCachePath, markdown); - } - catch - { - // Caching is best-effort; keep patch notes functional if disk write fails. - } - } - - private static List LoadCachedPatchNotes() - { - try - { - if (!File.Exists(PatchNotesCachePath)) - { - return new List(); - } - - var markdown = File.ReadAllText(PatchNotesCachePath); - return ParsePatchNotes(markdown); - } - catch - { - return new List(); - } - } - - private static List BuildFallbackPatchNotes() - { - return new List - { - new() { Text = "Anniversary Update", IsMajorHeader = true }, - new() { Text = "What's Changed", IsHeader = true }, - new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, - new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, - new() { Text = "Bug fixes and security improvements.", IsBullet = true }, - }; - } - - private static List ParsePatchNotes(string markdown) - { - var items = new List(); - foreach (var raw in markdown.Split('\n')) - { - var line = raw.TrimEnd(); - if (string.IsNullOrWhiteSpace(line)) continue; - - line = line.Trim(); - line = line.Replace("**", "").Replace("__", ""); - line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); - line = Regex.Replace(line, @"`([^`]*)`", "$1"); - - if (line.StartsWith("# ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsMajorHeader = true - }); - } - else if (line.StartsWith("## ") || line.StartsWith("### ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsHeader = true + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + + _cachedPatches = null; + + if (!installSucceeded && !File.Exists(Path.Combine(WauncherDirectory, "csgo.exe"))) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; + ArrowButton.Background = blue; + LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); + } + else + { + try + { + await CheckForUpdatesAsync(); + } + catch { } + } + + LaunchUpdateButton.IsEnabled = true; + Interlocked.Exchange(ref _installInProgress, 0); + } + } + + private async Task LoadPatchNotesAsync() + { + try + { + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Loading latest patch notes..."; + PatchNotesVersion.IsVisible = true; + }); + + if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) + { + Dispatcher.UIThread.Post(() => + { + var cachedItems = LoadCachedPatchNotes(); + if (cachedItems.Count > 0) + { + PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; + PatchNotesList.ItemsSource = cachedItems; + } + else + { + PatchNotesVersion.Text = "Patch notes are unavailable offline."; + PatchNotesList.ItemsSource = new List(); + } + + PatchNotesVersion.IsVisible = true; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + return; + } + + var md = await Api.GitHub.GetPatchNotesWauncher(); + var items = ParsePatchNotes(md); + SavePatchNotesCache(md); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = items.Count > 0 + ? $"Updated {DateTime.Now:MMM d, h:mm tt}" + : "Patch notes are currently empty."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + catch + { + var items = LoadCachedPatchNotes(); + if (items.Count == 0) + { + items = BuildFallbackPatchNotes(); + } + + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.Text = "Using fallback patch notes."; + PatchNotesVersion.IsVisible = true; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); + } + } + + private static void SavePatchNotesCache(string markdown) + { + try + { + var directory = Path.GetDirectoryName(PatchNotesCachePath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(PatchNotesCachePath, markdown); + } + catch + { + // Caching is best-effort; keep patch notes functional if disk write fails. + } + } + + private static List LoadCachedPatchNotes() + { + try + { + if (!File.Exists(PatchNotesCachePath)) + { + return new List(); + } + + var markdown = File.ReadAllText(PatchNotesCachePath); + return ParsePatchNotes(markdown); + } + catch + { + return new List(); + } + } + + private static List BuildFallbackPatchNotes() + { + return new List + { + new() { Text = "Anniversary Update", IsMajorHeader = true }, + new() { Text = "What's Changed", IsHeader = true }, + new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, + new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, + new() { Text = "Bug fixes and security improvements.", IsBullet = true }, + }; + } + + private static List ParsePatchNotes(string markdown) + { + var items = new List(); + foreach (var raw in markdown.Split('\n')) + { + var line = raw.TrimEnd(); + if (string.IsNullOrWhiteSpace(line)) continue; + + line = line.Trim(); + line = line.Replace("**", "").Replace("__", ""); + line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); + line = Regex.Replace(line, @"`([^`]*)`", "$1"); + + if (line.StartsWith("# ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsMajorHeader = true + }); + } + else if (line.StartsWith("## ") || line.StartsWith("### ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsHeader = true + }); + } + else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Substring(2).Trim(), + IsBullet = true + }); + } + else if (Regex.IsMatch(line, @"^\d+\.\s+")) + { + var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); + items.Add(new ViewModels.PatchNoteItem + { + Text = bulletText, + IsBullet = true }); } - else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Substring(2).Trim(), - IsBullet = true - }); - } - else if (Regex.IsMatch(line, @"^\d+\.\s+")) - { - var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); - items.Add(new ViewModels.PatchNoteItem - { - Text = bulletText, - IsBullet = true - }); - } - else if (line.StartsWith("**") && line.EndsWith("**")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Trim('*', ' '), + else if (line.StartsWith("**") && line.EndsWith("**")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Trim('*', ' '), IsHeader = true }); } @@ -889,329 +889,329 @@ private static void SavePatchNotesCache(string markdown) }); } } - return items; - } - - private sealed class GitHubRelease - { - [JsonProperty("tag_name")] - public string? TagName { get; set; } - - [JsonProperty("assets")] - public List? Assets { get; set; } - } - - private sealed class GitHubReleaseAsset - { - [JsonProperty("name")] - public string Name { get; set; } = string.Empty; - - [JsonProperty("browser_download_url")] - public string DownloadUrl { get; set; } = string.Empty; - } - - private static string NormalizeVersionToken(string? version) - { - if (string.IsNullOrWhiteSpace(version)) - return "0.0.0"; - - var cleaned = version.Trim(); - if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase)) - cleaned = cleaned[1..]; - - cleaned = Regex.Replace(cleaned, @"[^0-9\.]", string.Empty); - return string.IsNullOrWhiteSpace(cleaned) ? "0.0.0" : cleaned; - } - - private static bool TryParseVersion(string value, out global::System.Version parsed) - { - if (global::System.Version.TryParse(value, out parsed!)) - return true; - - var tokens = value.Split('.', StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length == 0) - { - parsed = new global::System.Version(0, 0, 0); - return false; - } - - while (tokens.Length < 3) - tokens = tokens.Append("0").ToArray(); - - return global::System.Version.TryParse(string.Join('.', tokens.Take(4)), out parsed!); - } - - private async Task CheckForSelfUpdateAsync() - { - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - - try - { - var latestReleaseJson = await Api.GitHub.GetLatestRelease(); - var release = JsonConvert.DeserializeObject(latestReleaseJson); - if (release == null) - return false; - - var currentVersion = NormalizeVersionToken(Wauncher.Utils.Version.Current); - var latestVersion = NormalizeVersionToken(release.TagName); - if (!TryParseVersion(currentVersion, out var current) || !TryParseVersion(latestVersion, out var latest)) - return false; - - if (latest <= current) - return false; - - var assets = release.Assets ?? new List(); - var preferred = assets.FirstOrDefault(a => - !string.IsNullOrWhiteSpace(a.DownloadUrl) && - string.Equals(a.Name, "wauncher.exe", StringComparison.OrdinalIgnoreCase)); - - preferred ??= assets.FirstOrDefault(a => - !string.IsNullOrWhiteSpace(a.DownloadUrl) && - a.Name.Contains("wauncher", StringComparison.OrdinalIgnoreCase) && - a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); - - if (preferred == null) - return false; - - _selfUpdateAvailable = true; - _selfUpdateDownloadUrl = preferred.DownloadUrl; - _selfUpdateVersion = latestVersion; - return true; - } - catch - { - return false; - } - } - - private async Task DownloadFileWithProgressAsync(string url, string destination, Action? onProgress, CancellationToken token) - { - using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); - response.EnsureSuccessStatusCode(); - - var totalBytes = response.Content.Headers.ContentLength; - await using var input = await response.Content.ReadAsStreamAsync(token); - await using var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true); - - var buffer = new byte[81920]; - long received = 0; - while (true) - { - int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), token); - if (read == 0) - break; - - await output.WriteAsync(buffer.AsMemory(0, read), token); - received += read; - if (totalBytes.HasValue && totalBytes.Value > 0) - { - onProgress?.Invoke((double)received / totalBytes.Value * 100.0); - } - } - - onProgress?.Invoke(100.0); - } - - private static string BuildSelfUpdateScript(string stagedExePath, string currentExePath) - { - return -$@"@echo off -setlocal -set ""SRC={stagedExePath}"" -set ""DST={currentExePath}"" - -for /L %%i in (1,1,60) do ( - copy /Y ""%SRC%"" ""%DST%"" >nul 2>nul && goto copied - timeout /t 1 /nobreak >nul -) - -exit /b 1 - -:copied -start """" ""%DST%"" -del /Q ""%SRC%"" >nul 2>nul -del /Q ""%~f0"" >nul 2>nul -exit /b 0 -"; - } - - private async Task Button_SelfUpdateAsync() - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) - return; - - if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) - return; - - _updateCts?.Dispose(); - _updateCts = new CancellationTokenSource(); - var token = _updateCts.Token; - - vm.IsUpdating = true; - vm.UpdateAvailable = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = true; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = "Downloading Wauncher update..."; - vm.UpdateStatusSpeed = ""; - - try - { - if (string.IsNullOrWhiteSpace(_selfUpdateDownloadUrl)) - throw new Exception("No self-update package URL found."); - - var updatesDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "self-update"); - Directory.CreateDirectory(updatesDir); - - var safeVersion = Regex.Replace(_selfUpdateVersion, @"[^0-9A-Za-z\.\-_]", string.Empty); - if (string.IsNullOrWhiteSpace(safeVersion)) - safeVersion = "latest"; - - var stagedExePath = Path.Combine(updatesDir, $"wauncher_{safeVersion}.exe"); - await DownloadFileWithProgressAsync(_selfUpdateDownloadUrl, stagedExePath, percent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateProgress = percent; - vm.UpdateStatusFile = $"Downloading Wauncher update... {percent:F0}%"; - }); - }, token); - - var currentExePath = Services.GetExePath(); - if (string.IsNullOrWhiteSpace(currentExePath)) - throw new Exception("Could not locate current Wauncher executable."); - - var scriptPath = Path.Combine(updatesDir, "apply_wauncher_update.cmd"); - var script = BuildSelfUpdateScript(stagedExePath, currentExePath); - File.WriteAllText(scriptPath, script, Encoding.ASCII); - - Process.Start(new ProcessStartInfo - { - FileName = "cmd.exe", - Arguments = $"/c \"{scriptPath}\"", - WorkingDirectory = updatesDir, - CreateNoWindow = true, - UseShellExecute = false, - }); - - vm.UpdateStatusFile = "Restarting Wauncher to apply update..."; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = 100; - await Task.Delay(500, token); - ForceQuit(); - } - catch (OperationCanceledException) - { - vm.UpdateStatusFile = "Update cancelled."; - vm.UpdateStatusSpeed = ""; - await Task.Delay(800); - } - catch (Exception ex) - { - vm.UpdateStatusFile = $"Self-update failed: {ex.Message}"; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = false; - await Task.Delay(2500); - } - finally - { - if (!_forceClose) - { - vm.IsUpdating = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - _updateCts?.Dispose(); - _updateCts = null; - - try - { - await CheckForUpdatesAsync(); - } - catch - { - // keep UI responsive even if refresh fails - } - } - - Interlocked.Exchange(ref _updateInProgress, 0); - } - } - - private async Task CheckForUpdatesAsync() - { - if (DataContext is not MainWindowViewModel vm) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - - if (vm.IsOfflineMode) - { - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - _cachedPatches = null; - vm.UpdateAvailable = false; - vm.IsCheckingUpdates = false; - var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = launchColor; - ArrowButton.Background = launchColor; - LaunchUpdateButton.IsEnabled = true; - return; - } - - vm.IsCheckingUpdates = true; - LaunchUpdateButton.Background = new SolidColorBrush(Color.Parse("#555555")); - ArrowButton.Background = new SolidColorBrush(Color.Parse("#555555")); - LaunchUpdateButton.IsEnabled = false; - try - { - bool hasSelfUpdate = await CheckForSelfUpdateAsync(); - if (hasSelfUpdate) - { - _cachedPatches = null; - vm.UpdateAvailable = true; - var selfUpdateColor = new SolidColorBrush(Color.Parse("#FFC107")); - LaunchUpdateButton.Background = selfUpdateColor; - ArrowButton.Background = selfUpdateColor; - - if (Interlocked.Exchange(ref _autoSelfUpdateTriggered, 1) == 0) - { - _ = Button_SelfUpdateAsync(); - } - - return; - } - - if (_settings.SkipUpdates) - { - _cachedPatches = null; - vm.UpdateAvailable = false; - var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = launchColor; - ArrowButton.Background = launchColor; - return; - } - - var patches = await Task.Run(() => PatchManager.ValidatePatches(deleteOutdatedFiles: false)); - bool hasUpdates = patches.Missing.Count > 0 || patches.Outdated.Count > 0; - - // Cache the result so Button_Update can consume it without re-validating. - _cachedPatches = patches; - _selfUpdateAvailable = false; - _selfUpdateDownloadUrl = string.Empty; - _selfUpdateVersion = string.Empty; - vm.UpdateAvailable = hasUpdates; - var buttonColor = new SolidColorBrush( - Color.Parse(hasUpdates ? "#FFC107" : "#4CAF50")); + return items; + } + + private sealed class GitHubRelease + { + [JsonProperty("tag_name")] + public string? TagName { get; set; } + + [JsonProperty("assets")] + public List? Assets { get; set; } + } + + private sealed class GitHubReleaseAsset + { + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("browser_download_url")] + public string DownloadUrl { get; set; } = string.Empty; + } + + private static string NormalizeVersionToken(string? version) + { + if (string.IsNullOrWhiteSpace(version)) + return "0.0.0"; + + var cleaned = version.Trim(); + if (cleaned.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + cleaned = cleaned[1..]; + + cleaned = Regex.Replace(cleaned, @"[^0-9\.]", string.Empty); + return string.IsNullOrWhiteSpace(cleaned) ? "0.0.0" : cleaned; + } + + private static bool TryParseVersion(string value, out global::System.Version parsed) + { + if (global::System.Version.TryParse(value, out parsed!)) + return true; + + var tokens = value.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 0) + { + parsed = new global::System.Version(0, 0, 0); + return false; + } + + while (tokens.Length < 3) + tokens = tokens.Append("0").ToArray(); + + return global::System.Version.TryParse(string.Join('.', tokens.Take(4)), out parsed!); + } + + private async Task CheckForSelfUpdateAsync() + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + + try + { + var latestReleaseJson = await Api.GitHub.GetLatestRelease(); + var release = JsonConvert.DeserializeObject(latestReleaseJson); + if (release == null) + return false; + + var currentVersion = NormalizeVersionToken(Wauncher.Utils.Version.Current); + var latestVersion = NormalizeVersionToken(release.TagName); + if (!TryParseVersion(currentVersion, out var current) || !TryParseVersion(latestVersion, out var latest)) + return false; + + if (latest <= current) + return false; + + var assets = release.Assets ?? new List(); + var preferred = assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + string.Equals(a.Name, "wauncher.exe", StringComparison.OrdinalIgnoreCase)); + + preferred ??= assets.FirstOrDefault(a => + !string.IsNullOrWhiteSpace(a.DownloadUrl) && + a.Name.Contains("wauncher", StringComparison.OrdinalIgnoreCase) && + a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); + + if (preferred == null) + return false; + + _selfUpdateAvailable = true; + _selfUpdateDownloadUrl = preferred.DownloadUrl; + _selfUpdateVersion = latestVersion; + return true; + } + catch + { + return false; + } + } + + private async Task DownloadFileWithProgressAsync(string url, string destination, Action? onProgress, CancellationToken token) + { + using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); + response.EnsureSuccessStatusCode(); + + var totalBytes = response.Content.Headers.ContentLength; + await using var input = await response.Content.ReadAsStreamAsync(token); + await using var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true); + + var buffer = new byte[81920]; + long received = 0; + while (true) + { + int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), token); + if (read == 0) + break; + + await output.WriteAsync(buffer.AsMemory(0, read), token); + received += read; + if (totalBytes.HasValue && totalBytes.Value > 0) + { + onProgress?.Invoke((double)received / totalBytes.Value * 100.0); + } + } + + onProgress?.Invoke(100.0); + } + + private static string BuildSelfUpdateScript(string stagedExePath, string currentExePath) + { + return +$@"@echo off +setlocal +set ""SRC={stagedExePath}"" +set ""DST={currentExePath}"" + +for /L %%i in (1,1,60) do ( + copy /Y ""%SRC%"" ""%DST%"" >nul 2>nul && goto copied + timeout /t 1 /nobreak >nul +) + +exit /b 1 + +:copied +start """" ""%DST%"" +del /Q ""%SRC%"" >nul 2>nul +del /Q ""%~f0"" >nul 2>nul +exit /b 0 +"; + } + + private async Task Button_SelfUpdateAsync() + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) + return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = true; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = "Downloading Wauncher update..."; + vm.UpdateStatusSpeed = ""; + + try + { + if (string.IsNullOrWhiteSpace(_selfUpdateDownloadUrl)) + throw new Exception("No self-update package URL found."); + + var updatesDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "self-update"); + Directory.CreateDirectory(updatesDir); + + var safeVersion = Regex.Replace(_selfUpdateVersion, @"[^0-9A-Za-z\.\-_]", string.Empty); + if (string.IsNullOrWhiteSpace(safeVersion)) + safeVersion = "latest"; + + var stagedExePath = Path.Combine(updatesDir, $"wauncher_{safeVersion}.exe"); + await DownloadFileWithProgressAsync(_selfUpdateDownloadUrl, stagedExePath, percent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateProgress = percent; + vm.UpdateStatusFile = $"Downloading Wauncher update... {percent:F0}%"; + }); + }, token); + + var currentExePath = Services.GetExePath(); + if (string.IsNullOrWhiteSpace(currentExePath)) + throw new Exception("Could not locate current Wauncher executable."); + + var scriptPath = Path.Combine(updatesDir, "apply_wauncher_update.cmd"); + var script = BuildSelfUpdateScript(stagedExePath, currentExePath); + File.WriteAllText(scriptPath, script, Encoding.ASCII); + + Process.Start(new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/c \"{scriptPath}\"", + WorkingDirectory = updatesDir, + CreateNoWindow = true, + UseShellExecute = false, + }); + + vm.UpdateStatusFile = "Restarting Wauncher to apply update..."; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 100; + await Task.Delay(500, token); + ForceQuit(); + } + catch (OperationCanceledException) + { + vm.UpdateStatusFile = "Update cancelled."; + vm.UpdateStatusSpeed = ""; + await Task.Delay(800); + } + catch (Exception ex) + { + vm.UpdateStatusFile = $"Self-update failed: {ex.Message}"; + vm.UpdateStatusSpeed = ""; + vm.UpdateIndeterminate = false; + await Task.Delay(2500); + } + finally + { + if (!_forceClose) + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch + { + // keep UI responsive even if refresh fails + } + } + + Interlocked.Exchange(ref _updateInProgress, 0); + } + } + + private async Task CheckForUpdatesAsync() + { + if (DataContext is not MainWindowViewModel vm) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + + if (vm.IsOfflineMode) + { + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + _cachedPatches = null; + vm.UpdateAvailable = false; + vm.IsCheckingUpdates = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + LaunchUpdateButton.IsEnabled = true; + return; + } + + vm.IsCheckingUpdates = true; + LaunchUpdateButton.Background = new SolidColorBrush(Color.Parse("#555555")); + ArrowButton.Background = new SolidColorBrush(Color.Parse("#555555")); + LaunchUpdateButton.IsEnabled = false; + try + { + bool hasSelfUpdate = await CheckForSelfUpdateAsync(); + if (hasSelfUpdate) + { + _cachedPatches = null; + vm.UpdateAvailable = true; + var selfUpdateColor = new SolidColorBrush(Color.Parse("#FFC107")); + LaunchUpdateButton.Background = selfUpdateColor; + ArrowButton.Background = selfUpdateColor; + + if (Interlocked.Exchange(ref _autoSelfUpdateTriggered, 1) == 0) + { + _ = Button_SelfUpdateAsync(); + } + + return; + } + + if (_settings.SkipUpdates) + { + _cachedPatches = null; + vm.UpdateAvailable = false; + var launchColor = new SolidColorBrush(Color.Parse("#4CAF50")); + LaunchUpdateButton.Background = launchColor; + ArrowButton.Background = launchColor; + return; + } + + var patches = await Task.Run(() => PatchManager.ValidatePatches(deleteOutdatedFiles: false)); + bool hasUpdates = patches.Missing.Count > 0 || patches.Outdated.Count > 0; + + // Cache the result so Button_Update can consume it without re-validating. + _cachedPatches = patches; + _selfUpdateAvailable = false; + _selfUpdateDownloadUrl = string.Empty; + _selfUpdateVersion = string.Empty; + vm.UpdateAvailable = hasUpdates; + var buttonColor = new SolidColorBrush( + Color.Parse(hasUpdates ? "#FFC107" : "#4CAF50")); LaunchUpdateButton.Background = buttonColor; ArrowButton.Background = buttonColor; } @@ -1228,44 +1228,44 @@ private async Task CheckForUpdatesAsync() } } - private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var vm = DataContext as MainWindowViewModel; - if (vm == null) return; - - if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) - return; - - _updateCts?.Dispose(); - _updateCts = new CancellationTokenSource(); - var token = _updateCts.Token; - vm.IsUpdating = true; - vm.UpdateAvailable = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; + private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var vm = DataContext as MainWindowViewModel; + if (vm == null) return; + + if (Interlocked.Exchange(ref _updateInProgress, 1) == 1) + return; + + _updateCts?.Dispose(); + _updateCts = new CancellationTokenSource(); + var token = _updateCts.Token; + vm.IsUpdating = true; + vm.UpdateAvailable = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; vm.UpdateStatus = ""; - try - { - // Use the result already computed by CheckForUpdatesAsync when available, - // to avoid a redundant full validation on every update click. - bool validateAll = _forceValidateAllOnce; - _forceValidateAllOnce = false; - bool usingCachedPatches = _cachedPatches != null; - - if (!usingCachedPatches) - { - vm.UpdateIndeterminate = true; - vm.UpdateStatusFile = validateAll - ? "Verifying all game files..." - : "Checking game files..."; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = 0; - } - - var patches = _cachedPatches ?? await Task.Run(() => PatchManager.ValidatePatches(validateAll: validateAll), token); - _cachedPatches = null; // consumed — force fresh check next time - if (token.IsCancellationRequested) return; + try + { + // Use the result already computed by CheckForUpdatesAsync when available, + // to avoid a redundant full validation on every update click. + bool validateAll = _forceValidateAllOnce; + _forceValidateAllOnce = false; + bool usingCachedPatches = _cachedPatches != null; + + if (!usingCachedPatches) + { + vm.UpdateIndeterminate = true; + vm.UpdateStatusFile = validateAll + ? "Verifying all game files..." + : "Checking game files..."; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 0; + } + + var patches = _cachedPatches ?? await Task.Run(() => PatchManager.ValidatePatches(validateAll: validateAll), token); + _cachedPatches = null; // consumed — force fresh check next time + if (token.IsCancellationRequested) return; bool hasPatches = patches.Missing.Count > 0 || patches.Outdated.Count > 0; if (!hasPatches) @@ -1281,44 +1281,44 @@ private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEv int totalFiles = allPatches.Count; int completed = 0; - foreach (var patch in allPatches) - { - if (token.IsCancellationRequested) break; - - var extractWatch = new System.Diagnostics.Stopwatch(); - await DownloadManager.DownloadPatch( - patch, - onProgress: (p) => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; - vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); - vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; - }); - }, - onExtract: () => - { - extractWatch.Restart(); - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); - vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; - }); - }); + foreach (var patch in allPatches) + { + if (token.IsCancellationRequested) break; + + var extractWatch = new System.Diagnostics.Stopwatch(); + await DownloadManager.DownloadPatch( + patch, + onProgress: (p) => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Installing {ShortFileName(patch.File)} {p.ProgressPercentage:F0}%"; + vm.UpdateStatusSpeed = FormatDownloadSpeedAndEta(p); + vm.UpdateProgress = ((completed + p.ProgressPercentage / 100.0) / totalFiles) * 100.0; + }); + }, + onExtract: () => + { + extractWatch.Restart(); + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... 0%"; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = ((double)completed / totalFiles) * 100.0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = $"Extracting {ShortFileName(patch.File)}... {extractPercent:F0}%"; + vm.UpdateStatusSpeed = FormatExtractEta(extractWatch, extractPercent); + vm.UpdateProgress = ((completed + extractPercent / 100.0) / totalFiles) * 100.0; + }); + }); completed++; vm.UpdateProgress = (double)completed / totalFiles * 100.0; @@ -1345,27 +1345,27 @@ await DownloadManager.DownloadPatch( vm.UpdateIndeterminate = false; await Task.Delay(3000); } - finally - { - vm.IsUpdating = false; - vm.UpdateProgress = 0; - vm.UpdateIndeterminate = false; - vm.UpdateStatus = ""; - vm.UpdateStatusFile = ""; - vm.UpdateStatusSpeed = ""; - _cachedPatches = null; - _updateCts?.Dispose(); - _updateCts = null; - - try - { - await CheckForUpdatesAsync(); - } - catch { } - - Interlocked.Exchange(ref _updateInProgress, 0); - } - } + finally + { + vm.IsUpdating = false; + vm.UpdateProgress = 0; + vm.UpdateIndeterminate = false; + vm.UpdateStatus = ""; + vm.UpdateStatusFile = ""; + vm.UpdateStatusSpeed = ""; + _cachedPatches = null; + _updateCts?.Dispose(); + _updateCts = null; + + try + { + await CheckForUpdatesAsync(); + } + catch { } + + Interlocked.Exchange(ref _updateInProgress, 0); + } + } private void Button_CancelUpdate(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { @@ -1378,79 +1378,81 @@ private void FriendsTab_Click(object? sender, Avalonia.Interactivity.RoutedEvent if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "Friends"; } - private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "PatchNotes"; - PatchNotesScroll.Offset = new Vector(0, 0); - } - - private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (sender is not MenuItem { Tag: FriendInfo friend }) - return; - - var profileId = ResolveProfileSteamId(friend.SteamId); - if (string.IsNullOrWhiteSpace(profileId)) - return; - - try - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = $"https://eddies.cc/profiles/{profileId}", - UseShellExecute = true - }); - } - catch - { - // Best-effort open. - } - } - - private static string ResolveProfileSteamId(string? steamId) - { - if (string.IsNullOrWhiteSpace(steamId)) - return string.Empty; - - var value = steamId.Trim(); - if (ulong.TryParse(value, out _)) - return value; - - if (TryConvertSteamId2To64(value, out var steamId64)) - return steamId64.ToString(); - - return string.Empty; - } - - private static bool TryConvertSteamId2To64(string steamId2, out ulong steamId64) - { - steamId64 = 0; - var match = Regex.Match(steamId2, @"^STEAM_[0-5]:([0-1]):(\d+)$", RegexOptions.IgnoreCase); - if (!match.Success) - return false; - - if (!ulong.TryParse(match.Groups[1].Value, out var y)) - return false; - if (!ulong.TryParse(match.Groups[2].Value, out var z)) - return false; - - steamId64 = 76561197960265728UL + (z * 2UL) + y; - return true; - } - - private void Button_Settings(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { + private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "PatchNotes"; + PatchNotesScroll.Offset = new Vector(0, 0); + } + + private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: FriendInfo friend }) + return; + + var profileId = ResolveProfileSteamId(friend.SteamId); + if (string.IsNullOrWhiteSpace(profileId)) + return; + + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = $"https://eddies.cc/profiles/{profileId}", + UseShellExecute = true + }); + } + catch + { + // Best-effort open. + } + } + + private static string ResolveProfileSteamId(string? steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) + return string.Empty; + + var value = steamId.Trim(); + if (ulong.TryParse(value, out _)) + return value; + + if (TryConvertSteamId2To64(value, out var steamId64)) + return steamId64.ToString(); + + return string.Empty; + } + + private static bool TryConvertSteamId2To64(string steamId2, out ulong steamId64) + { + steamId64 = 0; + var match = Regex.Match(steamId2, @"^STEAM_[0-5]:([0-1]):(\d+)$", RegexOptions.IgnoreCase); + if (!match.Success) + return false; + + if (!ulong.TryParse(match.Groups[1].Value, out var y)) + return false; + if (!ulong.TryParse(match.Groups[2].Value, out var z)) + return false; + + steamId64 = 76561197960265728UL + (z * 2UL) + y; + return true; + } + + private void Button_Settings(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { if (_settingsWindow == null) { - _settingsWindow = new SettingsWindow(); - _settingsWindow.Closed += (s, e) => - { - _settingsWindow = null; - _settings = SettingsWindowViewModel.LoadGlobal(); - _ = CheckForUpdatesAsync(); - }; - _settingsWindow.Show(this); - } + var skipUpdatesBeforeOpen = _settings.SkipUpdates; + _settingsWindow = new SettingsWindow(); + _settingsWindow.Closed += (s, e) => + { + _settingsWindow = null; + _settings = SettingsWindowViewModel.LoadGlobal(); + if (skipUpdatesBeforeOpen != _settings.SkipUpdates) + _ = CheckForUpdatesAsync(); + }; + _settingsWindow.Show(this); + } else _settingsWindow.Activate(); } @@ -1466,148 +1468,148 @@ private void Button_Info(object? sender, Avalonia.Interactivity.RoutedEventArgs } // ── Launch button glow + color ──────────────────────────────────────────── - private void SetLaunchGlow(bool updating) - { - var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); - LaunchUpdateButton.Background = brush; - ArrowButton.Background = brush; - LaunchButtonGlow.BoxShadow = updating - ? BoxShadows.Parse("0 0 18 2 #55FFC107") - : BoxShadows.Parse("0 0 18 2 #554CAF50"); - } - - private static string ShortFileName(string path) - { - if (string.IsNullOrWhiteSpace(path)) - return path; - - var normalized = path.Replace('\\', '/'); - if (normalized.Length <= 42) - return normalized; - - var fileName = Path.GetFileName(normalized); - if (fileName.Length <= 30) - return fileName; - - return fileName[..27] + "..."; - } - - private static string FormatDownloadSpeedAndEta(object progressArgs) - { - double speedBytes = 0; - if (TryGetDoubleProperty(progressArgs, "AverageBytesPerSecondSpeed", out var avg) && avg > 0) - speedBytes = avg; - else if (TryGetDoubleProperty(progressArgs, "BytesPerSecondSpeed", out var cur) && cur > 0) - speedBytes = cur; - - var speedText = speedBytes > 0 - ? $"{speedBytes / 1024.0 / 1024.0:F1} MB/s" - : ""; - - if (speedBytes <= 0 || - !TryGetLongProperty(progressArgs, "TotalBytesToReceive", out var totalBytes) || - !TryGetLongProperty(progressArgs, "ReceivedBytesSize", out var receivedBytes) || - totalBytes <= 0 || receivedBytes < 0 || receivedBytes >= totalBytes) - { - return speedText; - } - - var remainingBytes = totalBytes - receivedBytes; - var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); - var etaText = $"ETA {FormatEta(eta)}"; - - return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; - } - - private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) - { - if (watch == null || !watch.IsRunning || percent <= 1.0) - return ""; - - var elapsed = watch.Elapsed.TotalSeconds; - var total = elapsed / (percent / 100.0); - var remaining = Math.Max(0, total - elapsed); - return $"ETA {FormatEta(TimeSpan.FromSeconds(remaining))}"; - } - - private static string FormatEta(TimeSpan eta) - { - if (eta.TotalHours >= 1) - return eta.ToString(@"hh\:mm\:ss"); - return eta.ToString(@"mm\:ss"); - } - - private static bool TryGetDoubleProperty(object obj, string propertyName, out double value) - { - value = 0; - var prop = obj.GetType().GetProperty(propertyName); - if (prop == null) return false; - var raw = prop.GetValue(obj); - if (raw == null) return false; - try - { - value = Convert.ToDouble(raw); - return true; - } - catch - { - return false; - } - } - - private static bool TryGetLongProperty(object obj, string propertyName, out long value) - { - value = 0; - var prop = obj.GetType().GetProperty(propertyName); - if (prop == null) return false; - var raw = prop.GetValue(obj); - if (raw == null) return false; - try - { - value = Convert.ToInt64(raw); - return true; - } - catch - { - return false; - } - } - - // Minimal parser for launch options that supports quoted values. - private static IEnumerable ParseLaunchOptions(string options) - { - if (string.IsNullOrWhiteSpace(options)) - yield break; - - var current = new StringBuilder(); - bool inQuotes = false; - - foreach (var ch in options) - { - if (ch == '"') - { - inQuotes = !inQuotes; - continue; - } - - if (char.IsWhiteSpace(ch) && !inQuotes) - { - if (current.Length > 0) - { - yield return current.ToString(); - current.Clear(); - } - continue; - } - - current.Append(ch); - } - - if (current.Length > 0) - yield return current.ToString(); - } - - } -} + private void SetLaunchGlow(bool updating) + { + var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); + LaunchUpdateButton.Background = brush; + ArrowButton.Background = brush; + LaunchButtonGlow.BoxShadow = updating + ? BoxShadows.Parse("0 0 18 2 #55FFC107") + : BoxShadows.Parse("0 0 18 2 #554CAF50"); + } + + private static string ShortFileName(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return path; + + var normalized = path.Replace('\\', '/'); + if (normalized.Length <= 42) + return normalized; + + var fileName = Path.GetFileName(normalized); + if (fileName.Length <= 30) + return fileName; + + return fileName[..27] + "..."; + } + + private static string FormatDownloadSpeedAndEta(object progressArgs) + { + double speedBytes = 0; + if (TryGetDoubleProperty(progressArgs, "AverageBytesPerSecondSpeed", out var avg) && avg > 0) + speedBytes = avg; + else if (TryGetDoubleProperty(progressArgs, "BytesPerSecondSpeed", out var cur) && cur > 0) + speedBytes = cur; + + var speedText = speedBytes > 0 + ? $"{speedBytes / 1024.0 / 1024.0:F1} MB/s" + : ""; + + if (speedBytes <= 0 || + !TryGetLongProperty(progressArgs, "TotalBytesToReceive", out var totalBytes) || + !TryGetLongProperty(progressArgs, "ReceivedBytesSize", out var receivedBytes) || + totalBytes <= 0 || receivedBytes < 0 || receivedBytes >= totalBytes) + { + return speedText; + } + + var remainingBytes = totalBytes - receivedBytes; + var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); + var etaText = $"ETA {FormatEta(eta)}"; + + return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; + } + + private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) + { + if (watch == null || !watch.IsRunning || percent <= 1.0) + return ""; + + var elapsed = watch.Elapsed.TotalSeconds; + var total = elapsed / (percent / 100.0); + var remaining = Math.Max(0, total - elapsed); + return $"ETA {FormatEta(TimeSpan.FromSeconds(remaining))}"; + } + + private static string FormatEta(TimeSpan eta) + { + if (eta.TotalHours >= 1) + return eta.ToString(@"hh\:mm\:ss"); + return eta.ToString(@"mm\:ss"); + } + + private static bool TryGetDoubleProperty(object obj, string propertyName, out double value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToDouble(raw); + return true; + } + catch + { + return false; + } + } + + private static bool TryGetLongProperty(object obj, string propertyName, out long value) + { + value = 0; + var prop = obj.GetType().GetProperty(propertyName); + if (prop == null) return false; + var raw = prop.GetValue(obj); + if (raw == null) return false; + try + { + value = Convert.ToInt64(raw); + return true; + } + catch + { + return false; + } + } + + // Minimal parser for launch options that supports quoted values. + private static IEnumerable ParseLaunchOptions(string options) + { + if (string.IsNullOrWhiteSpace(options)) + yield break; + + var current = new StringBuilder(); + bool inQuotes = false; + + foreach (var ch in options) + { + if (ch == '"') + { + inQuotes = !inQuotes; + continue; + } + + if (char.IsWhiteSpace(ch) && !inQuotes) + { + if (current.Length > 0) + { + yield return current.ToString(); + current.Clear(); + } + continue; + } + + current.Append(ch); + } + + if (current.Length > 0) + yield return current.ToString(); + } + + } +} From 4804b29a7668e164dc0611477579786ce0b64451 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:53:55 -0400 Subject: [PATCH 23/51] Only minimize to tray while game is running --- Wauncher/App.axaml.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Wauncher/App.axaml.cs b/Wauncher/App.axaml.cs index b85422c..19fe3cd 100644 --- a/Wauncher/App.axaml.cs +++ b/Wauncher/App.axaml.cs @@ -100,9 +100,6 @@ private void SetupTrayIcon() }; _discordRpcMenuItem.Click += DiscordRpc_Click; - var openItem = new NativeMenuItem { Header = "Open" }; - openItem.Click += (_, _) => ShowMainWindow(); - var exitItem = new NativeMenuItem { Header = "Exit" }; exitItem.Click += (_, _) => { @@ -115,8 +112,6 @@ private void SetupTrayIcon() }; var menu = new NativeMenu(); - menu.Items.Add(openItem); - menu.Items.Add(new NativeMenuItemSeparator()); menu.Items.Add(_discordRpcMenuItem); menu.Items.Add(new NativeMenuItemSeparator()); menu.Items.Add(exitItem); From 0032dcc79e3f73ce77b4f0bb8666480095bd599c Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:54:14 -0400 Subject: [PATCH 24/51] Only minimize to tray while game is running --- Wauncher/Views/MainWindow.axaml.cs | 46 +++++++++++++++++------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index c905bf5..af4c4e4 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -77,16 +77,16 @@ public MainWindow() // Window minimize always goes to taskbar; tray hide only happens on game launch. - this.Closing += (s, e) => - { - if (_forceClose) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - if (_settings.MinimizeToTray) - { - e.Cancel = true; - Hide(); - } - }; + this.Closing += (s, e) => + { + if (_forceClose) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + if (_settings.MinimizeToTray && IsGameRunning()) + { + e.Cancel = true; + Hide(); + } + }; // Ensure carousel timer is stopped whenever the window closes, // regardless of which code path triggered it. @@ -464,16 +464,22 @@ private void CloseButton_Click(object? sender, Avalonia.Interactivity.RoutedEven Close(); } - public void ForceQuit() - { - _forceClose = true; - TeardownCarousel(); - Close(); - } - - private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var dir = WauncherDirectory; + public void ForceQuit() + { + _forceClose = true; + TeardownCarousel(); + Close(); + } + + private bool IsGameRunning() + { + return DataContext is MainWindowViewModel vm && + string.Equals(vm.GameStatus, "Running", StringComparison.Ordinal); + } + + private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var dir = WauncherDirectory; System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = dir, From 55c5188b737e4028c5e41b6d5970715292ae8997 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:11:47 -0400 Subject: [PATCH 25/51] Update README for current Wauncher features --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d5627e7..be853d1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@

ClassicCounter Wauncher

- Wauncher for ClassicCounter with Discord RPC, Auto-Updates and More! + Wauncher for ClassicCounter with Discord RPC, a Server List, Friends List, Auto-Updates and More!
- Written in C# using .NET 8. + Written in C# using .NET 8 and Avalonia.

@@ -15,12 +15,18 @@ > [!IMPORTANT] > .NET Runtime 8 is required to run the Wauncher. Download it from [**here**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.11-windows-x64-installer). +> [!IMPORTANT] +> Wauncher is still a work in progress. Some features are unfinished and you may run into bugs or changes between builds. + ## Settings -- Validation behavior is controlled in the GUI. -- Use `Verify Game Files` from the launch button drop-up menu when you want a full file check. +- `Minimize to System Tray` keeps the launcher running in the background when in-game. +- `Skip Updates` lets you launch the game even when Wauncher detects available updates. +- `Discord RPC` controls whether Wauncher updates your Discord presence. In some cases, Discord may still show "Counter-Strike: Global Offensive" even when RPC is disabled. +- `Launch Options` lets you pass extra game launch flags such as `-high` or `+fps_max 300`. +- `Verify Game Files` checks your installation and repairs any missing or damaged game files automatically. ## Known Issues -- Friends list currently cannot show when a friend is online and actively in-game. +- Friends list does not currently show accurate `Online` or `Offline` status. ## Build / Publish - Build: `dotnet build Wauncher/Wauncher.csproj -c Release` From 97d88c204504082d60db2aac723197f3601d4dfc Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:19:20 -0400 Subject: [PATCH 26/51] Exclude pdb file from publish script --- publish.bat | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/publish.bat b/publish.bat index a2baac1..13147c7 100644 --- a/publish.bat +++ b/publish.bat @@ -2,6 +2,8 @@ for /f %%a in ('echo prompt $E^| cmd') do set "ESC=%%a" setlocal +set "publishDir=Wauncher\bin\Release\net8.0-windows7.0\win-x64\publish" + echo ============================= echo %ESC%[42mBuilding Wauncher...%ESC%[0m dotnet publish Wauncher\Wauncher.csproj -c Release -r win-x64 --self-contained false @@ -12,7 +14,7 @@ if errorlevel 1 ( echo ============================= echo %ESC%[41mHashing wauncher.exe...%ESC%[0m -certutil -hashfile "Wauncher\bin\Release\net8.0-windows7.0\win-x64\publish\wauncher.exe" MD5 +certutil -hashfile "%publishDir%\wauncher.exe" MD5 echo ============================= echo %ESC%[1;43mCopying Wauncher publish output...%ESC%[0m @@ -24,6 +26,11 @@ if not exist "%destination%" ( mkdir "%destination%" ) -xcopy "Wauncher\bin\Release\net8.0-windows7.0\win-x64\publish\*" "%destination%\" /e /y /i >nul -echo Copied to: %destination% +robocopy "%publishDir%" "%destination%" /e /r:1 /w:1 /xf *.pdb >nul +if errorlevel 8 ( + echo Copy failed. + exit /b 1 +) + +echo Copied to: %destination% (without .pdb files) timeout /t 3 >nul From 8c92abf3eef9de847ef4872f18063aa7d0d1bd3e Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:42:58 -0400 Subject: [PATCH 27/51] Downscale oversized Steam avatars in cache --- Wauncher/Utils/AvatarCache.cs | 86 +++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/Wauncher/Utils/AvatarCache.cs b/Wauncher/Utils/AvatarCache.cs index cb1fdff..3e03d3d 100644 --- a/Wauncher/Utils/AvatarCache.cs +++ b/Wauncher/Utils/AvatarCache.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; +using SkiaSharp; namespace Wauncher.Utils { @@ -16,6 +17,7 @@ public static class AvatarCache "avatars"); private const int MaxAvatarBytes = 20 * 1024 * 1024; // 20 MB + private const int MaxSteamAvatarDimension = 128; public static string GetDisplaySource(string? avatarUrl) { @@ -66,9 +68,18 @@ private static async Task EnsureCachedAsync(string avatarUrl) using var response = await _http.GetAsync(avatarUrl, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); - await using var input = await response.Content.ReadAsStreamAsync(); var tempPath = cachePath + ".tmp"; - await using var output = File.Create(tempPath); + var bytes = await ReadAvatarBytesAsync(response); + var bytesToWrite = TryDownscaleSteamAvatar(avatarUrl, bytes) ?? bytes; + + await File.WriteAllBytesAsync(tempPath, bytesToWrite); + File.Move(tempPath, cachePath, overwrite: true); + } + + private static async Task ReadAvatarBytesAsync(HttpResponseMessage response) + { + await using var input = await response.Content.ReadAsStreamAsync(); + await using var bufferStream = new MemoryStream(); var buffer = new byte[81920]; int read; @@ -79,11 +90,76 @@ private static async Task EnsureCachedAsync(string avatarUrl) if (total > MaxAvatarBytes) throw new InvalidDataException("Avatar exceeds size limit."); - await output.WriteAsync(buffer.AsMemory(0, read)); + await bufferStream.WriteAsync(buffer.AsMemory(0, read)); } - output.Close(); - File.Move(tempPath, cachePath, overwrite: true); + return bufferStream.ToArray(); + } + + private static byte[]? TryDownscaleSteamAvatar(string avatarUrl, byte[] bytes) + { + if (!ShouldDownscaleAvatar(avatarUrl)) + return null; + + try + { + using var sourceBitmap = SKBitmap.Decode(bytes); + if (sourceBitmap == null) + return null; + + if (sourceBitmap.Width <= MaxSteamAvatarDimension && + sourceBitmap.Height <= MaxSteamAvatarDimension) + { + return null; + } + + var scale = Math.Min( + (double)MaxSteamAvatarDimension / sourceBitmap.Width, + (double)MaxSteamAvatarDimension / sourceBitmap.Height); + + int targetWidth = Math.Max(1, (int)Math.Round(sourceBitmap.Width * scale)); + int targetHeight = Math.Max(1, (int)Math.Round(sourceBitmap.Height * scale)); + + using var resizedBitmap = sourceBitmap.Resize( + new SKImageInfo(targetWidth, targetHeight), + SKFilterQuality.Medium); + + if (resizedBitmap == null) + return null; + + using var image = SKImage.FromBitmap(resizedBitmap); + var format = GetEncodedFormat(avatarUrl); + using var data = image.Encode(format, 90); + return data?.ToArray(); + } + catch + { + return null; + } + } + + private static bool ShouldDownscaleAvatar(string avatarUrl) + { + try + { + var host = new Uri(avatarUrl).Host; + return host.Contains("steamstatic.com", StringComparison.OrdinalIgnoreCase); + } + catch + { + return false; + } + } + + private static SKEncodedImageFormat GetEncodedFormat(string avatarUrl) + { + var ext = GetExtensionFromUrl(avatarUrl); + return ext switch + { + ".png" => SKEncodedImageFormat.Png, + ".webp" => SKEncodedImageFormat.Webp, + _ => SKEncodedImageFormat.Jpeg, + }; } private static string GetCachePath(string avatarUrl) From 74a8382f6b1840a44b792578e68f61f27a512d11 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:31:09 -0400 Subject: [PATCH 28/51] Removed friends list issue --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index be853d1..54cba75 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,6 @@ - `Launch Options` lets you pass extra game launch flags such as `-high` or `+fps_max 300`. - `Verify Game Files` checks your installation and repairs any missing or damaged game files automatically. -## Known Issues -- Friends list does not currently show accurate `Online` or `Offline` status. - ## Build / Publish - Build: `dotnet build Wauncher/Wauncher.csproj -c Release` - Publish: `dotnet publish Wauncher/Wauncher.csproj -c Release -r win-x64 --self-contained false` From b573108bfa41ab4b3c5af2cd847b28e0fbf8ff37 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:20:59 -0400 Subject: [PATCH 29/51] Keep rich friend statuses --- Wauncher/Utils/Api.cs | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/Wauncher/Utils/Api.cs b/Wauncher/Utils/Api.cs index 976c7dc..56daddf 100644 --- a/Wauncher/Utils/Api.cs +++ b/Wauncher/Utils/Api.cs @@ -94,12 +94,21 @@ public string? CustomAvatar } [JsonProperty("status")] - public string Status { get; set; } = "Offline"; // "Online" | "Offline" + public string Status { get; set; } = "Offline"; - public string DotColor => Status == "Online" ? "#4CAF50" : "#888888"; - public bool IsOffline => Status == "Offline"; + [JsonIgnore] + public string QuickJoinIpPort { get; set; } = ""; + + [JsonIgnore] + public string QuickJoinServerName { get; set; } = ""; + + [JsonIgnore] + public bool CanQuickJoin => !string.IsNullOrWhiteSpace(QuickJoinIpPort); + + public string DotColor => IsOffline ? "#888888" : "#4CAF50"; + public bool IsOffline => string.Equals(Status, "Offline", StringComparison.OrdinalIgnoreCase); public double AvatarOpacity => IsOffline ? 0.35 : 1.0; - public string StatusText => IsOffline ? "Offline" : "In Game"; + public string StatusText => string.IsNullOrWhiteSpace(Status) ? "Offline" : Status; public string StatusColor => IsOffline ? "#666666" : "#999999"; } @@ -200,9 +209,7 @@ private static List NormalizeFriends(IEnumerable friends foreach (var f in friends) { var username = string.IsNullOrWhiteSpace(f.Username) ? "Unknown" : f.Username; - var status = string.Equals(f.Status, "Online", StringComparison.OrdinalIgnoreCase) - ? "Online" - : "Offline"; + var status = NormalizeStatus(f.Status); normalized.Add(new FriendInfo { @@ -215,6 +222,21 @@ private static List NormalizeFriends(IEnumerable friends return normalized; } + + private static string NormalizeStatus(string? status) + { + if (string.IsNullOrWhiteSpace(status)) + return "Offline"; + + var trimmed = status.Trim(); + if (string.Equals(trimmed, "Offline", StringComparison.OrdinalIgnoreCase)) + return "Offline"; + + if (string.Equals(trimmed, "Online", StringComparison.OrdinalIgnoreCase)) + return "Online"; + + return trimmed; + } } } From 6c0be2b3c2ff1f6c4c3739d26a907c89f6e90070 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:21:27 -0400 Subject: [PATCH 30/51] Add friend quick join matching --- Wauncher/ViewModels/MainWindowViewModel.cs | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Wauncher/ViewModels/MainWindowViewModel.cs b/Wauncher/ViewModels/MainWindowViewModel.cs index 15dc8ba..4a2a88e 100644 --- a/Wauncher/ViewModels/MainWindowViewModel.cs +++ b/Wauncher/ViewModels/MainWindowViewModel.cs @@ -5,6 +5,7 @@ using System.ComponentModel; using System.Linq; using System.Net.NetworkInformation; +using System.Text.RegularExpressions; using System.Text; using System.Threading; using FriendInfo = Wauncher.Utils.FriendInfo; @@ -401,6 +402,8 @@ private async Task RefreshFriendsAsync() { var sorted = apiFriends; + ApplyQuickJoinMetadata(sorted); + foreach (var f in sorted) f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); @@ -450,6 +453,8 @@ private bool TryShowCachedFriends(string steamId, bool forceOfflineStatus) f.Status = "Offline"; } + ApplyQuickJoinMetadata(sorted); + foreach (var f in sorted) f.AvatarUrl = AvatarCache.GetDisplaySource(f.AvatarUrl); @@ -487,6 +492,45 @@ private static string BuildFriendsSignature(IEnumerable friends) return sb.ToString(); } + private void ApplyQuickJoinMetadata(IEnumerable friends) + { + foreach (var friend in friends) + { + friend.QuickJoinIpPort = string.Empty; + friend.QuickJoinServerName = string.Empty; + + var serverName = ExtractServerNameFromStatus(friend.Status); + if (string.IsNullOrWhiteSpace(serverName)) + continue; + + var matches = Servers + .Where(s => !s.IsNone) + .Where(s => string.Equals(s.Name, serverName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (matches.Count == 1) + { + friend.QuickJoinIpPort = matches[0].IpPort; + friend.QuickJoinServerName = matches[0].Name; + } + } + } + + private static string ExtractServerNameFromStatus(string? status) + { + if (string.IsNullOrWhiteSpace(status)) + return string.Empty; + + var match = Regex.Match( + status, + @"^In Game - (?.+?) \(\d+/\d+\)$", + RegexOptions.IgnoreCase); + + return match.Success + ? match.Groups["name"].Value.Trim() + : string.Empty; + } + private async Task RefreshFriendsSafeAsync() { if (Interlocked.Exchange(ref _friendsRefreshInProgress, 1) == 1) From 4db59205a4a9640d6582c24740f34aafde3fdb55 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:22:26 -0400 Subject: [PATCH 31/51] Add patch note date headers --- Wauncher/ViewModels/PatchNoteItem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Wauncher/ViewModels/PatchNoteItem.cs b/Wauncher/ViewModels/PatchNoteItem.cs index cdd8904..9641dfd 100644 --- a/Wauncher/ViewModels/PatchNoteItem.cs +++ b/Wauncher/ViewModels/PatchNoteItem.cs @@ -4,6 +4,7 @@ public class PatchNoteItem { public string Text { get; set; } = string.Empty; public bool IsMajorHeader { get; set; } + public bool IsDateHeader { get; set; } public bool IsHeader { get; set; } public bool IsBullet { get; set; } } From d64372072d1d08e756ee41995a4f845e5dc3ac03 Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:22:56 -0400 Subject: [PATCH 32/51] Save launch options live --- Wauncher/Views/SettingsWindow.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wauncher/Views/SettingsWindow.axaml b/Wauncher/Views/SettingsWindow.axaml index b04c0a1..0a67744 100644 --- a/Wauncher/Views/SettingsWindow.axaml +++ b/Wauncher/Views/SettingsWindow.axaml @@ -195,7 +195,7 @@ FontSize="13" Foreground="{DynamicResource AppPrimaryText}" Watermark="e.g. -high -novid +fps_max 300" - Text="{Binding LaunchOptions}" /> + Text="{Binding LaunchOptions, UpdateSourceTrigger=PropertyChanged}" />
From aa4175afababb7517129e6d324ccdcfa2a1cccdc Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:23:54 -0400 Subject: [PATCH 33/51] Refine patch note parsing --- Wauncher/Views/MainWindow.axaml.cs | 381 ++++++++++++++++++----------- 1 file changed, 241 insertions(+), 140 deletions(-) diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index af4c4e4..706e7d1 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Net.Http; using System.Linq; using System.Net.NetworkInformation; @@ -38,7 +38,7 @@ public partial class MainWindow : Window private const double HeightClosed = 720; private const double HeightOpen = 720; - // ── Image carousel (center content area) ────────────────────────────────── + // ── Image carousel (center content area) ────────────────────────────────── private Image[] _carouselImages = Array.Empty(); private DispatcherTimer? _carouselTimer; private int _currentCarouselIndex = 0; @@ -93,7 +93,7 @@ public MainWindow() this.Closed += (_, _) => TeardownCarousel(); } - // ── Image carousel (center content area) ────────────────────────────────── + // ── Image carousel (center content area) ────────────────────────────────── private static readonly HttpClient _http = new(); private static string PatchNotesCachePath => Path.Combine( @@ -323,7 +323,7 @@ private void StopZoom(int slot) _zoomCts[slot] = null; } - // ── Server dropdown ─────────────────────────────────────────── + // ── Server dropdown ─────────────────────────────────────────── private void ToggleServerDropdown(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { if (DataContext is MainWindowViewModel vmOffline && vmOffline.IsOfflineMode) @@ -366,7 +366,7 @@ private void CloseDropdown() ServerListPanel.MaxHeight = 0; } - // ── Game launch ─────────────────────────────────────────── + // ── Game launch ─────────────────────────────────────────── private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { var vm = DataContext as MainWindowViewModel; @@ -436,14 +436,25 @@ private async Task LaunchGameAsync() { Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); } - finally - { - if (vm != null) vm.GameStatus = "Not Running"; - Interlocked.Exchange(ref _launchInProgress, 0); - } - } + finally + { + if (vm != null) vm.GameStatus = "Not Running"; + + if (!_forceClose && _settings.MinimizeToTray && !IsVisible) + { + Dispatcher.UIThread.Post(() => + { + Show(); + WindowState = WindowState.Normal; + Activate(); + }); + } + + Interlocked.Exchange(ref _launchInProgress, 0); + } + } - // ── Window chrome ─────────────────────────────────────────── + // ── Window chrome ─────────────────────────────────────────── private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) { if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) @@ -500,7 +511,7 @@ private void VerifyGameFiles_Click(object? sender, Avalonia.Interactivity.Routed Button_Update(sender, e); } - // ── Update ───────────────────────────────────────────────────── + // ── Update ───────────────────────────────────────────────────── private CancellationTokenSource? _updateCts; private Patches? _cachedPatches; private bool _selfUpdateAvailable; @@ -725,46 +736,40 @@ private async Task LoadPatchNotesAsync() { try { - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Loading latest patch notes..."; - PatchNotesVersion.IsVisible = true; - }); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.IsVisible = false; + }); if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) { Dispatcher.UIThread.Post(() => { var cachedItems = LoadCachedPatchNotes(); - if (cachedItems.Count > 0) - { - PatchNotesVersion.Text = "Offline mode: showing cached patch notes."; - PatchNotesList.ItemsSource = cachedItems; - } - else - { - PatchNotesVersion.Text = "Patch notes are unavailable offline."; - PatchNotesList.ItemsSource = new List(); - } - - PatchNotesVersion.IsVisible = true; - PatchNotesScroll.Offset = new Vector(0, 0); - }); + if (cachedItems.Count > 0) + { + PatchNotesList.ItemsSource = cachedItems; + } + else + { + PatchNotesList.ItemsSource = new List(); + } + + PatchNotesVersion.IsVisible = false; + PatchNotesScroll.Offset = new Vector(0, 0); + }); return; } var md = await Api.GitHub.GetPatchNotesWauncher(); var items = ParsePatchNotes(md); SavePatchNotesCache(md); - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = items.Count > 0 - ? $"Updated {DateTime.Now:MMM d, h:mm tt}" - : "Patch notes are currently empty."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.IsVisible = false; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); } catch { @@ -774,13 +779,12 @@ private async Task LoadPatchNotesAsync() items = BuildFallbackPatchNotes(); } - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.Text = "Using fallback patch notes."; - PatchNotesVersion.IsVisible = true; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.IsVisible = false; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); } } @@ -820,84 +824,155 @@ private static void SavePatchNotesCache(string markdown) } } - private static List BuildFallbackPatchNotes() - { - return new List - { - new() { Text = "Anniversary Update", IsMajorHeader = true }, - new() { Text = "What's Changed", IsHeader = true }, - new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, - new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, - new() { Text = "Bug fixes and security improvements.", IsBullet = true }, - }; - } - - private static List ParsePatchNotes(string markdown) - { - var items = new List(); - foreach (var raw in markdown.Split('\n')) - { - var line = raw.TrimEnd(); - if (string.IsNullOrWhiteSpace(line)) continue; - - line = line.Trim(); - line = line.Replace("**", "").Replace("__", ""); - line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); - line = Regex.Replace(line, @"`([^`]*)`", "$1"); - - if (line.StartsWith("# ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsMajorHeader = true - }); - } - else if (line.StartsWith("## ") || line.StartsWith("### ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsHeader = true - }); - } - else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Substring(2).Trim(), - IsBullet = true - }); - } - else if (Regex.IsMatch(line, @"^\d+\.\s+")) - { - var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); - items.Add(new ViewModels.PatchNoteItem - { - Text = bulletText, - IsBullet = true - }); - } - else if (line.StartsWith("**") && line.EndsWith("**")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Trim('*', ' '), - IsHeader = true - }); - } - else - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', '*', '-', ' '), - IsBullet = true - }); - } - } - return items; - } - + private static List BuildFallbackPatchNotes() + { + return new List + { + new() { Text = "Anniversary Update", IsMajorHeader = true }, + new() { Text = "March 12, 2026", IsDateHeader = true }, + new() { Text = "What''s Changed", IsHeader = true }, + new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, + new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, + new() { Text = "Bug fixes and security improvements.", IsBullet = true }, + }; + } + + private static List ParsePatchNotes(string markdown) + { + var items = new List(); + bool lastWasMajorHeader = false; + + foreach (var raw in markdown.Split('\n')) + { + var line = raw.TrimEnd(); + if (string.IsNullOrWhiteSpace(line)) continue; + + line = line.Trim(); + line = line.Replace("**", "").Replace("__", ""); + line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); + line = Regex.Replace(line, @"`([^`]*)`", "$1"); + + if (line.StartsWith("# ")) + { + var headerText = line.TrimStart('#', ' '); + var (title, dateText) = SplitPatchTitleAndDate(headerText); + + items.Add(new ViewModels.PatchNoteItem + { + Text = title, + IsMajorHeader = true + }); + + if (!string.IsNullOrWhiteSpace(dateText)) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = dateText, + IsDateHeader = true + }); + } + + lastWasMajorHeader = true; + } + else if (lastWasMajorHeader && TryParsePatchDateLine(line, out var parsedDate)) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = parsedDate, + IsDateHeader = true + }); + lastWasMajorHeader = false; + } + else if (line.StartsWith("## ") || line.StartsWith("### ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsHeader = true + }); + lastWasMajorHeader = false; + } + else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Substring(2).Trim(), + IsBullet = true + }); + lastWasMajorHeader = false; + } + else if (Regex.IsMatch(line, @"^\d+\.\s+")) + { + var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); + items.Add(new ViewModels.PatchNoteItem + { + Text = bulletText, + IsBullet = true + }); + lastWasMajorHeader = false; + } + else if (line.StartsWith("**") && line.EndsWith("**")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Trim('*', ' '), + IsHeader = true + }); + lastWasMajorHeader = false; + } + else + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', '*', '-', ' '), + IsBullet = true + }); + lastWasMajorHeader = false; + } + } + + return items; + } + + private static (string Title, string DateText) SplitPatchTitleAndDate(string headerText) + { + var match = Regex.Match( + headerText, + @"^(?.+?)\s+(?:-|–|—)\s+(?<date>(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},\s+\d{4})$", + RegexOptions.IgnoreCase); + + if (!match.Success) + return (headerText, string.Empty); + + return (match.Groups["title"].Value.Trim(), match.Groups["date"].Value.Trim()); + } + + private static bool TryParsePatchDateLine(string line, out string dateText) + { + dateText = string.Empty; + var trimmed = line.Trim(); + + if (trimmed.StartsWith("Date:", StringComparison.OrdinalIgnoreCase)) + { + dateText = trimmed[5..].Trim(); + return !string.IsNullOrWhiteSpace(dateText); + } + + if (Regex.IsMatch(trimmed, @"^(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},\s+\d{4}$", RegexOptions.IgnoreCase)) + { + dateText = trimmed; + return true; + } + + if (Regex.IsMatch(trimmed, @"^\d{1,2}/\d{1,2}/\d{4}$", RegexOptions.IgnoreCase)) + { + dateText = trimmed; + return true; + } + + return false; + } + private sealed class GitHubRelease { [JsonProperty("tag_name")] @@ -1270,7 +1345,7 @@ private async void Button_Update(object? sender, Avalonia.Interactivity.RoutedEv } var patches = _cachedPatches ?? await Task.Run(() => PatchManager.ValidatePatches(validateAll: validateAll), token); - _cachedPatches = null; // consumed — force fresh check next time + _cachedPatches = null; // consumed — force fresh check next time if (token.IsCancellationRequested) return; bool hasPatches = patches.Missing.Count > 0 || patches.Outdated.Count > 0; @@ -1378,7 +1453,7 @@ private void Button_CancelUpdate(object? sender, Avalonia.Interactivity.RoutedEv _updateCts?.Cancel(); } - // ── Settings / Info windows ──────────────────────────────────────── + // ── Settings / Info windows ──────────────────────────────────────── private void FriendsTab_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { if (DataContext is MainWindowViewModel vm) vm.ActiveRightTab = "Friends"; @@ -1390,10 +1465,10 @@ private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEv PatchNotesScroll.Offset = new Vector(0, 0); } - private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (sender is not MenuItem { Tag: FriendInfo friend }) - return; + private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: FriendInfo friend }) + return; var profileId = ResolveProfileSteamId(friend.SteamId); if (string.IsNullOrWhiteSpace(profileId)) @@ -1410,12 +1485,37 @@ private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.Rout catch { // Best-effort open. - } - } - - private static string ResolveProfileSteamId(string? steamId) - { - if (string.IsNullOrWhiteSpace(steamId)) + } + } + + private async void JoinFriendServer_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: FriendInfo friend }) + return; + + if (DataContext is not MainWindowViewModel vm) + return; + + if (string.IsNullOrWhiteSpace(friend.QuickJoinIpPort)) + return; + + var matchedServer = vm.Servers.FirstOrDefault(s => + !s.IsNone && + string.Equals(s.IpPort, friend.QuickJoinIpPort, StringComparison.OrdinalIgnoreCase)); + + if (matchedServer == null) + { + Wauncher.Utils.ConsoleManager.ShowError("Couldn't match that friend to a server in your server list."); + return; + } + + vm.SelectedServer = matchedServer; + await LaunchGameAsync(); + } + + private static string ResolveProfileSteamId(string? steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) return string.Empty; var value = steamId.Trim(); @@ -1473,7 +1573,7 @@ private void Button_Info(object? sender, Avalonia.Interactivity.RoutedEventArgs else _infoWindow.Activate(); } - // ── Launch button glow + color ──────────────────────────────────────────── + // ── Launch button glow + color ──────────────────────────────────────────── private void SetLaunchGlow(bool updating) { var brush = new SolidColorBrush(Color.Parse(updating ? "#FFC107" : "#4CAF50")); @@ -1524,7 +1624,7 @@ private static string FormatDownloadSpeedAndEta(object progressArgs) var eta = TimeSpan.FromSeconds(remainingBytes / speedBytes); var etaText = $"ETA {FormatEta(eta)}"; - return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; + return string.IsNullOrEmpty(speedText) ? etaText : $"{speedText} • {etaText}"; } private static string FormatExtractEta(System.Diagnostics.Stopwatch watch, double percent) @@ -1619,3 +1719,4 @@ private static IEnumerable<string> ParseLaunchOptions(string options) } + From 0126a529d0f9415e13dd3c8386c9b41bb4a1b23f Mon Sep 17 00:00:00 2001 From: Ways <109334069+ways15xx@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:24:15 -0400 Subject: [PATCH 34/51] Polish friends and patch notes UI --- Wauncher/Views/MainWindow.axaml | 87 +++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 27 deletions(-) diff --git a/Wauncher/Views/MainWindow.axaml b/Wauncher/Views/MainWindow.axaml index c2d0d61..86f2a6a 100644 --- a/Wauncher/Views/MainWindow.axaml +++ b/Wauncher/Views/MainWindow.axaml @@ -260,6 +260,32 @@ <Style Selector="Button.friendMenuBtn:pointerover"> <Setter Property="Background" Value="#22FFFFFF" /> </Style> + <Style Selector="Button.friendRowBtn"> + <Setter Property="Padding" Value="0" /> + <Setter Property="BorderThickness" Value="0" /> + <Setter Property="Background" Value="Transparent" /> + <Setter Property="MinHeight" Value="50" /> + <Setter Property="HorizontalAlignment" Value="Stretch" /> + <Setter Property="HorizontalContentAlignment" Value="Stretch" /> + <Setter Property="Cursor" Value="Hand" /> + <Setter Property="Template"> + <ControlTemplate> + <Border + Background="{TemplateBinding Background}" + CornerRadius="6" + MinHeight="{TemplateBinding MinHeight}" + Padding="{TemplateBinding Padding}"> + <ContentPresenter + HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" + VerticalAlignment="Center" + Content="{TemplateBinding Content}" /> + </Border> + </ControlTemplate> + </Setter> + </Style> + <Style Selector="Button.friendRowBtn:pointerover"> + <Setter Property="Background" Value="#14FFFFFF" /> + </Style> <!-- Selected label default (no server) --> <Style Selector="TextBlock.selectedlabel"> @@ -450,14 +476,14 @@ </Border> <!-- Server dropdown list --> - <Border - x:Name="ServerListPanel" + <Border + x:Name="ServerListPanel" Grid.Row="0" MaxHeight="0" Margin="0,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Bottom" - Width="360" + Width="390" Background="{DynamicResource AppServerListBg}" BorderBrush="{DynamicResource AppDivider}" BorderThickness="1" @@ -473,7 +499,7 @@ <ItemsControl.ItemTemplate> <DataTemplate x:DataType="vm:ServerInfo"> <Button Classes="serverBtn" Click="ServerItem_Click" Tag="{Binding}"> - <Grid ColumnDefinitions="14,*,120,60"> + <Grid ColumnDefinitions="14,*,120,60"> <Ellipse Grid.Column="0" Width="7" Height="7" VerticalAlignment="Center" Fill="{Binding DotColor}" /> <TextBlock Grid.Column="1" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="13" Foreground="{Binding NameColor}" Text="{Binding Name}" /> <TextBlock Grid.Column="2" Margin="8,0,8,0" VerticalAlignment="Center" FontSize="11" FontFamily="Consolas,monospace" Foreground="{DynamicResource AppSectionLabel}" Text="{Binding MapDisplay}" /> @@ -546,8 +572,8 @@ <Button Classes="iconBtn" Width="36" Height="36" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Click="Button_Settings" CornerRadius="18"> <Image Width="18" Height="18" Source="avares://Wauncher/Assets/settings.png" /> </Button> - <!-- Server selector --> - <Button Classes="iconBtn serverSelectorBtn" Width="36" Height="36" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Click="ToggleServerDropdown" CornerRadius="18" IsEnabled="{Binding IsOnlineMode}" ToolTip.Tip="Select Server"> + <!-- Server selector --> + <Button Classes="iconBtn serverSelectorBtn" Width="36" Height="36" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Click="ToggleServerDropdown" CornerRadius="18" IsEnabled="{Binding IsOnlineMode}" ToolTip.Tip="Select Server"> <Image Width="18" Height="18" Source="avares://Wauncher/Assets/server.png" /> </Button> <!-- Launch button --> @@ -667,30 +693,35 @@ <StackPanel Spacing="2" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> - <ItemsControl.ItemTemplate> - <DataTemplate x:DataType="wau:FriendInfo"> - <Grid ColumnDefinitions="54,*,18,16" Margin="4,8"> - <Border Width="42" Height="42" CornerRadius="21" ClipToBounds="True" VerticalAlignment="Center" Opacity="{Binding AvatarOpacity}"> - <Image asyncImageLoader:ImageLoader.Source="{Binding AvatarUrl}" Stretch="UniformToFill" /> - </Border> - <StackPanel Grid.Column="1" Margin="8,0" VerticalAlignment="Center" Spacing="1"> - <TextBlock - FontSize="14" - FontWeight="SemiBold" - Foreground="{DynamicResource AppPrimaryText}" - Text="{Binding Username}" - TextTrimming="CharacterEllipsis" /> - <TextBlock FontSize="11" Foreground="{Binding StatusColor}" Text="{Binding StatusText}" TextTrimming="CharacterEllipsis" /> - </StackPanel> - <Button Grid.Column="2" Classes="friendMenuBtn" VerticalAlignment="Center"> + <ItemsControl.ItemTemplate> + <DataTemplate x:DataType="wau:FriendInfo"> + <Grid ColumnDefinitions="56,*,12,20" Margin="4,1"> + <Button Grid.Column="0" Grid.ColumnSpan="3" Classes="friendRowBtn"> <Button.Flyout> <MenuFlyout Placement="Top"> + <MenuItem Header="Join Server" Tag="{Binding}" Click="JoinFriendServer_Click" IsVisible="{Binding CanQuickJoin}" /> <MenuItem Header="View eddies.cc Profile" Tag="{Binding}" Click="ViewFriendProfile_Click" /> </MenuFlyout> </Button.Flyout> - <Path Width="8" Height="5" Data="M0,0 L4,5 L8,0" Stroke="{DynamicResource AppMutedText}" StrokeThickness="1.2" /> + <Grid ColumnDefinitions="56,*,12" VerticalAlignment="Center" Margin="6,0"> + <Grid Width="40" Height="40" Margin="2,0,0,0" VerticalAlignment="Center"> + <Border Width="40" Height="40" CornerRadius="20" ClipToBounds="True" VerticalAlignment="Center" Opacity="{Binding AvatarOpacity}"> + <Image asyncImageLoader:ImageLoader.Source="{Binding AvatarUrl}" Stretch="UniformToFill" /> + </Border> + </Grid> + <StackPanel Grid.Column="1" Margin="6,0,0,0" VerticalAlignment="Center" Spacing="1"> + <TextBlock + FontSize="14" + FontWeight="SemiBold" + Foreground="{DynamicResource AppPrimaryText}" + Text="{Binding Username}" + TextTrimming="CharacterEllipsis" /> + <TextBlock FontSize="11" Foreground="#A8A8A8" Text="{Binding StatusText}" TextTrimming="CharacterEllipsis" /> + </StackPanel> + <Path Grid.Column="2" Width="8" Height="5" HorizontalAlignment="Center" VerticalAlignment="Center" Data="M0,5 L4,0 L8,5" Stroke="{DynamicResource AppMutedText}" StrokeThickness="1.2" /> + </Grid> </Button> - <Ellipse Grid.Column="3" Width="10" Height="10" VerticalAlignment="Center" Fill="{Binding DotColor}" /> + <Ellipse Grid.Column="3" Width="10" Height="10" Margin="4,0,0,0" VerticalAlignment="Center" Fill="{Binding DotColor}" /> </Grid> </DataTemplate> </ItemsControl.ItemTemplate> @@ -715,6 +746,7 @@ Margin="0,0,0,8" FontSize="11" Foreground="{DynamicResource AppMutedText}" + IsVisible="False" Text="Latest wauncher changes" TextWrapping="Wrap" /> <TextBlock @@ -723,7 +755,7 @@ FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource AppMutedText}" - IsVisible="True" + IsVisible="False" Text="Loading..." /> <ItemsControl x:Name="PatchNotesList" HorizontalAlignment="Stretch" MaxWidth="268"> @@ -733,9 +765,10 @@ </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> - <DataTemplate x:DataType="vm:PatchNoteItem"> - <Grid> + <DataTemplate x:DataType="vm:PatchNoteItem"> + <Grid> <TextBlock IsVisible="{Binding IsMajorHeader}" Margin="0,14,0,8" FontSize="22" FontWeight="ExtraBold" Foreground="{DynamicResource AppPrimaryText}" TextWrapping="Wrap" LineHeight="28" MaxWidth="256" Text="{Binding Text}" /> + <TextBlock IsVisible="{Binding IsDateHeader}" Margin="0,-2,0,8" FontSize="11" FontWeight="SemiBold" Foreground="{DynamicResource AppMutedText}" TextWrapping="Wrap" LineHeight="16" MaxWidth="256" Text="{Binding Text}" /> <TextBlock IsVisible="{Binding IsHeader}" Margin="0,10,0,7" FontSize="15" FontWeight="Bold" Foreground="{DynamicResource AppPrimaryText}" TextWrapping="Wrap" LineHeight="20" MaxWidth="256" Text="{Binding Text}" /> <Grid IsVisible="{Binding IsBullet}" Margin="0,3,0,3" ColumnDefinitions="10,*"> <TextBlock Grid.Column="0" FontSize="11" Foreground="#4CAF50" VerticalAlignment="Top" Margin="0,2,0,0" Text="•" /> From 94e62ca65e41f240d2f06446dd777071effd91ca Mon Sep 17 00:00:00 2001 From: eddies <zombie@z.org> Date: Fri, 13 Mar 2026 03:45:55 -0400 Subject: [PATCH 35/51] Update proprietary Wauncher license --- Wauncher/LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wauncher/LICENSE b/Wauncher/LICENSE index d1fd9fe..64e6564 100644 --- a/Wauncher/LICENSE +++ b/Wauncher/LICENSE @@ -1,3 +1,3 @@ © 2026 ClassicCounter. All Rights Reserved. Do Not Redistribute. This software and its code are protected by copyright law and international treaties. Unauthorized reproduction or distribution may result in civil and criminal penalties. -You may not modify, use, or distribute Wauncher outside of ClassicCounter or ClassicCounter servers without explicit written or recorded permission from a ClassicCounter staff member. +You may not modify, use, reproduce, or distribute Wauncher outside of ClassicCounter or ClassicCounter servers without explicit written or recorded permission from a ClassicCounter staff member. From 6d741e3744b237495034cdce6ce866ce743475ac Mon Sep 17 00:00:00 2001 From: eddies <zombie@z.org> Date: Fri, 13 Mar 2026 04:19:35 -0400 Subject: [PATCH 36/51] PUBLISH WAUNCHER LICENSE --- Wauncher/LICENSE | 78 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/Wauncher/LICENSE b/Wauncher/LICENSE index 64e6564..c9920fd 100644 --- a/Wauncher/LICENSE +++ b/Wauncher/LICENSE @@ -1,3 +1,75 @@ -© 2026 ClassicCounter. All Rights Reserved. Do Not Redistribute. -This software and its code are protected by copyright law and international treaties. Unauthorized reproduction or distribution may result in civil and criminal penalties. -You may not modify, use, reproduce, or distribute Wauncher outside of ClassicCounter or ClassicCounter servers without explicit written or recorded permission from a ClassicCounter staff member. +ClassicCounter Community Source License (CCCSL) + +Copyright (c) 2026 ClassicCounter Community Developers. +Maintained by eddies. All Rights Reserved. + +1. Definitions + +"Software" refers to the Wauncher program, including its source code, +compiled binaries, documentation, and any associated files distributed +as part of the ClassicCounter project. + +"ClassicCounter Community" refers to the official ClassicCounter +community, its approved members, and its official servers. + +"Authorized Members" refers to individuals who are explicitly approved +or whitelisted by ClassicCounter staff. + +2. Permission Grant + +Subject to the restrictions below, Authorized Members of the +ClassicCounter Community are granted permission to: + +- View the source code of the Software +- Use the Software within the ClassicCounter Community +- Modify the Software for use within the ClassicCounter Community +- Create forks or derivative works solely for use within + ClassicCounter community servers + +3. Restrictions + +The following actions are strictly prohibited unless explicit written +permission is granted by the copyright holder or authorized +ClassicCounter staff: + +1. Using the Software outside of the ClassicCounter Community. +2. Running or deploying the Software on non-ClassicCounter servers. +3. Redistributing, publishing, or mirroring the Software outside of + the ClassicCounter Community. +4. Selling, sublicensing, or commercially exploiting the Software. +5. Distributing modified versions outside of the ClassicCounter + Community. + +Any forks, modifications, or derivative works must remain exclusively +for use within ClassicCounter community servers. + +4. Contributions + +By submitting code, patches, or modifications to the ClassicCounter +project, you grant ClassicCounter and its maintainers a perpetual, +worldwide, non-exclusive, royalty-free license to use, modify, +and distribute your contributions as part of the ClassicCounter +project under the terms of this license. + +Contributors retain ownership of their individual contributions. + +5. Termination + +Any violation of this license automatically terminates the permissions +granted under this license. Upon termination, all copies of the +Software must be destroyed and use of the Software must cease +immediately. + +6. No Warranty + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM +THE USE OF THE SOFTWARE. + +7. Jurisdiction + +This license shall be governed by applicable copyright laws and +international copyright treaties. From 168564aa3a05653fa2484666893e522f9ae6a0d5 Mon Sep 17 00:00:00 2001 From: eddies <zombie@z.org> Date: Fri, 13 Mar 2026 04:21:05 -0400 Subject: [PATCH 37/51] Create Wauncher Proprietary README.md --- Wauncher/README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Wauncher/README.md diff --git a/Wauncher/README.md b/Wauncher/README.md new file mode 100644 index 0000000..ab066e7 --- /dev/null +++ b/Wauncher/README.md @@ -0,0 +1,2 @@ +This project is source-available for the ClassicCounter community only. +It is NOT open source and may not be used outside of official ClassicCounter servers. From c4426169bbccc7243aa114d06e0d0c87d8b08766 Mon Sep 17 00:00:00 2001 From: eddies <zombie@z.org> Date: Fri, 13 Mar 2026 04:30:10 -0400 Subject: [PATCH 38/51] Update LICENSE hosting conditions --- Wauncher/LICENSE | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Wauncher/LICENSE b/Wauncher/LICENSE index c9920fd..66c3b2d 100644 --- a/Wauncher/LICENSE +++ b/Wauncher/LICENSE @@ -39,6 +39,14 @@ ClassicCounter staff: 4. Selling, sublicensing, or commercially exploiting the Software. 5. Distributing modified versions outside of the ClassicCounter Community. +6. Public hosting of the Software or its source code on external + repositories, mirrors, or file hosting platforms (including but + not limited to GitHub, GitLab, Bitbucket, or similar services) + is prohibited unless the repository is owned or explicitly + authorized by ClassicCounter staff. +7. Replication of the Software's functionality, architecture, or + design for the purpose of creating a competing implementation + for use outside the ClassicCounter community is prohibited. Any forks, modifications, or derivative works must remain exclusively for use within ClassicCounter community servers. From ac45661d7868b8e674262d472be34c8b424a4403 Mon Sep 17 00:00:00 2001 From: ways15xx <ways15xx@gmail.com> Date: Fri, 13 Mar 2026 11:48:18 -0400 Subject: [PATCH 39/51] sync local Wauncher updates --- README.md | 3 + Wauncher/Utils/Download.cs | 1433 ++++++++++++---------------- Wauncher/Utils/Game.cs | 107 ++- Wauncher/Utils/ServerQuery.cs | 29 +- Wauncher/Views/InfoWindow.axaml | 43 +- Wauncher/Views/MainWindow.axaml | 19 +- Wauncher/Views/MainWindow.axaml.cs | 415 +++++--- Wauncher/Wauncher.csproj | 1 + 8 files changed, 1014 insertions(+), 1036 deletions(-) diff --git a/README.md b/README.md index 54cba75..be853d1 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ - `Launch Options` lets you pass extra game launch flags such as `-high` or `+fps_max 300`. - `Verify Game Files` checks your installation and repairs any missing or damaged game files automatically. +## Known Issues +- Friends list does not currently show accurate `Online` or `Offline` status. + ## Build / Publish - Build: `dotnet build Wauncher/Wauncher.csproj -c Release` - Publish: `dotnet publish Wauncher/Wauncher.csproj -c Release -r win-x64 --self-contained false` diff --git a/Wauncher/Utils/Download.cs b/Wauncher/Utils/Download.cs index 50d06ae..8cec0dc 100644 --- a/Wauncher/Utils/Download.cs +++ b/Wauncher/Utils/Download.cs @@ -1,837 +1,598 @@ -using Downloader; -using Refit; -using Spectre.Console; -using System.Diagnostics; -using System.Text.RegularExpressions; - -namespace Wauncher.Utils -{ - public static class DownloadManager - { - private static readonly DownloadConfiguration _settings = new() - { - ChunkCount = 8, - ParallelDownload = true - }; - // Shared only for DownloadUpdater / DownloadDependencies (console-launcher, always sequential) - private static DownloadService _downloader = new DownloadService(_settings); - - public static async Task DownloadUpdater(string path) - { - await _downloader.DownloadFileTaskAsync( - $"https://github.com/ClassicCounter/updater/releases/download/updater/updater.exe", - path - ); - } - - public static async Task<Dependencies> DownloadDependencies(StatusContext ctx, List<Dependency> dependencies) - { - List<Dependency> local = new List<Dependency>(); - List<Dependency> remote = new List<Dependency>(); - Dependencies? _dependencies; - foreach (var dependency in dependencies) - { - if (!DependencyManager.IsInstalled(ctx, dependency)) - { - if (dependency.URL != null) - { - string path = Directory.GetCurrentDirectory() + dependency.Path; - if (File.Exists(path)) - File.Delete(path); - if (Debug.Enabled()) - Terminal.Debug($"Downloading {dependency.Name}"); - await _downloader.DownloadFileTaskAsync( - $"{dependency.URL}", - $"{Directory.GetCurrentDirectory()}{dependency.Path}"); - remote.Add(dependency); - } - else - { - local.Add(dependency); - } - } - } - _dependencies = new Dependencies(false, local, remote); - return _dependencies; - } - - public static async Task DownloadPatch( - Patch patch, - bool validateAll = false, - Action<Downloader.DownloadProgressChangedEventArgs>? onProgress = null, - Action? onExtract = null, - Action<double>? onExtractProgress = null) - { - string originalFileName = patch.File.EndsWith(".7z") ? patch.File[..^3] : patch.File; - string downloadPath = $"{Directory.GetCurrentDirectory()}/{patch.File}"; - - if (Debug.Enabled()) - Terminal.Debug($"Starting download of: {patch.File}"); - - if (patch.File.EndsWith(".7z") && File.Exists(downloadPath)) - { - try - { - if (Debug.Enabled()) - Terminal.Debug($"Found existing .7z file, trying to delete: {downloadPath}"); - File.Delete(downloadPath); - } - catch (Exception ex) - { - if (Debug.Enabled()) - Terminal.Debug($"Failed to delete existing .7z file: {ex.Message}"); - } - } - - string baseUrl = "https://patch.classiccounter.cc"; - - // Use a fresh DownloadService per call so concurrent or back-to-back downloads - // never share state on the same instance. - using var downloader = new DownloadService(_settings); - if (onProgress != null) - downloader.DownloadProgressChanged += (sender, e) => onProgress(e); - - await downloader.DownloadFileTaskAsync( - $"{baseUrl}/{patch.File}", - $"{Directory.GetCurrentDirectory()}/{patch.File}" - ); - - if (patch.File.EndsWith(".7z")) - { - if (Debug.Enabled()) - Terminal.Debug($"Download complete, starting extraction of: {patch.File}"); - onExtract?.Invoke(); - string extractPath = $"{Directory.GetCurrentDirectory()}/{originalFileName}"; - await Extract7z(downloadPath, extractPath, onExtractProgress); - } - } - - public static async Task HandlePatches(Patches patches, StatusContext ctx, bool isGameFiles, int startingProgress = 0) - { - string fileType = isGameFiles ? "game file" : "patch"; - string fileTypePlural = isGameFiles ? "game files" : "patches"; - - var allFiles = patches.Missing.Concat(patches.Outdated).ToList(); - int totalFiles = allFiles.Count; - int completedFiles = startingProgress; - int failedFiles = 0; - - // status update - Action<Downloader.DownloadProgressChangedEventArgs, string> updateStatus = (progress, filename) => - { - var speed = progress.BytesPerSecondSpeed / (1024.0 * 1024.0); - var progressText = $"{((float)completedFiles / totalFiles * 100):F1}% ({completedFiles}/{totalFiles})"; - var status = filename.EndsWith(".7z") && progress.ProgressPercentage >= 100 ? "Extracting" : "Downloading new"; - ctx.Status = $"{status} {fileTypePlural}{GetDots().PadRight(3)} [gray]|[/] {progressText} [gray]|[/] {GetProgressBar(progress.ProgressPercentage)} {progress.ProgressPercentage:F1}% [gray]|[/] {speed:F1} MB/s"; - }; - - foreach (var patch in allFiles) - { - try - { - await DownloadPatch(patch, isGameFiles, progress => updateStatus(progress, patch.File)); - completedFiles++; - } - catch - { - failedFiles++; - Terminal.Warning($"Couldn't process {fileType}: {patch.File}, possibly due to missing permissions."); - } - } - - if (failedFiles > 0) - Terminal.Warning($"Couldn't download {failedFiles} {(failedFiles == 1 ? fileType : fileTypePlural)}!"); - } - - public static async Task DownloadFullGame(StatusContext ctx) - { - try - { - await Steam.GetRecentLoggedInSteamID(); - if (string.IsNullOrEmpty(Steam.recentSteamID2)) - { - Terminal.Error("Steam does not seem to be installed. Please make sure that you have Steam installed."); - Terminal.Error("Closing launcher in 5 seconds..."); - await Task.Delay(5000); - Environment.Exit(1); - return; - } - - // pass steam id to api - var gameFiles = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); - - if (gameFiles?.Files == null || gameFiles.Files.Count == 0) - { - Terminal.Error("No game files returned from the API. You may not be whitelisted."); - Terminal.Error("Closing launcher in 5 seconds..."); - await Task.Delay(5000); - Environment.Exit(1); - return; - } - - int totalFiles = gameFiles.Files.Count; - int completedFiles = 0; - List<string> failedFiles = new List<string>(); - - foreach (var file in gameFiles.Files) - { - string filePath = Path.Combine(Directory.GetCurrentDirectory(), file.File); - bool needsDownload = true; - - if (File.Exists(filePath)) - { - string fileHash = CalculateMD5(filePath); - if (fileHash.Equals(file.Hash, StringComparison.OrdinalIgnoreCase)) - { - needsDownload = false; - completedFiles++; - continue; - } - } - - if (needsDownload) - { - try - { - EventHandler<Downloader.DownloadProgressChangedEventArgs> progressHandler = (sender, e) => - { - var speed = e.BytesPerSecondSpeed / (1024.0 * 1024.0); - var progressText = $"{((float)completedFiles / totalFiles * 100):F1}% ({completedFiles}/{totalFiles})"; - ctx.Status = $"Downloading {file.File}{GetDots().PadRight(3)} [gray]|[/] {progressText} [gray]|[/] {GetProgressBar(e.ProgressPercentage)} {e.ProgressPercentage:F1}% [gray]|[/] {speed:F1} MB/s"; - }; - _downloader.DownloadProgressChanged += progressHandler; - - try - { - await _downloader.DownloadFileTaskAsync( - file.Link, - filePath - ); - - string downloadedHash = CalculateMD5(filePath); - if (!downloadedHash.Equals(file.Hash, StringComparison.OrdinalIgnoreCase)) - { - failedFiles.Add(file.File); - Terminal.Error($"Hash mismatch for {file.File}"); - continue; - } - - completedFiles++; - } - finally - { - _downloader.DownloadProgressChanged -= progressHandler; - } - } - catch (Exception ex) - { - failedFiles.Add(file.File); - Terminal.Error($"Failed to download {file.File}: {ex.Message}"); - } - } - } - - if (failedFiles.Count == 0) - { - string extractPath = Directory.GetCurrentDirectory(); - string tempExtractPath = Path.Combine(extractPath, "ClassicCounter_temp"); - - // check for running 7za.exe processes - var processes = Process.GetProcessesByName("7za"); - if (processes.Length > 0) - { - if (Debug.Enabled()) - Terminal.Debug("Found running 7za.exe process, waiting..."); - - // wait for existing 7za.exe to finish - while (Process.GetProcessesByName("7za").Length > 0) - { - ctx.Status = "Found already running extraction. Waiting for it to complete..."; - await Task.Delay(1000); - } - - // this is just code from ExtractSplitArchive (the moving folder part) - string classicCounterPath = Path.Combine(tempExtractPath, "ClassicCounter"); - if (Directory.Exists(tempExtractPath) && Directory.Exists(classicCounterPath)) - { - // check if the directory has any contents - if (Directory.GetFiles(classicCounterPath, "*.*", SearchOption.AllDirectories).Any()) - { - try - { - if (Debug.Enabled()) - Terminal.Debug("Moving contents from ClassicCounter folder to root directory..."); - - foreach (string dirPath in Directory.GetDirectories(classicCounterPath, "*", SearchOption.AllDirectories)) - { - string newDirPath = dirPath.Replace(classicCounterPath, extractPath); - Directory.CreateDirectory(newDirPath); - } - - foreach (string filePath in Directory.GetFiles(classicCounterPath, "*.*", SearchOption.AllDirectories)) - { - string newFilePath = filePath.Replace(classicCounterPath, extractPath); - - // skip launcher.exe - if (Path.GetFileName(filePath).Equals("launcher.exe", StringComparison.OrdinalIgnoreCase)) - { - if (Debug.Enabled()) - Terminal.Debug("Skipping launcher.exe"); - continue; - } - - try - { - if (File.Exists(newFilePath)) - { - File.Delete(newFilePath); - } - File.Move(filePath, newFilePath); - } - catch (Exception ex) - { - Terminal.Warning($"Failed to move file {filePath}: {ex.Message}"); - } - } - - // cleanup temp directory - try - { - Directory.Delete(tempExtractPath, true); - if (Debug.Enabled()) - Terminal.Debug("Deleted temporary extraction directory"); - } - catch (Exception ex) - { - Terminal.Warning($"Failed to cleanup temporary directory: {ex.Message}"); - } - - // cleanup .7z.xxx files - try - { - var splitArchiveFiles = Directory.GetFiles(extractPath, "*.7z.*") - .Where(f => Path.GetFileName(f).StartsWith("ClassicCounter.7z.")); - - foreach (var file in splitArchiveFiles) - { - try - { - File.Delete(file); - if (Debug.Enabled()) - Terminal.Debug($"Deleted split archive file: {file}"); - } - catch (Exception ex) - { - Terminal.Warning($"Failed to delete split archive file {file}: {ex.Message}"); - } - } - } - catch (Exception ex) - { - Terminal.Warning($"Failed to cleanup some split archive files: {ex.Message}"); - } - } - catch (Exception ex) - { - Terminal.Warning($"Some files may not have been moved correctly: {ex.Message}"); - } - } - else if (Debug.Enabled()) - { - Terminal.Debug("ClassicCounter folder exists but is empty, skipping file movement"); - } - } - else if (Debug.Enabled()) - { - Terminal.Debug("Temp directory or ClassicCounter folder not found, skipping file movement"); - } - - Terminal.Success("Extraction finished! Closing launcher..."); - Terminal.Warning("Make sure to run the launcher again if the game doesn't start afterwards."); - ctx.Status = "Done!"; - await Task.Delay(10000); - Environment.Exit(0); - } - - ctx.Status = "Extracting game files... Please do not close the launcher."; - await ExtractSplitArchive(gameFiles.Files.Select(f => f.File).ToList()); - Terminal.Success("Game files downloaded and extracted successfully!"); - } - else - { - Terminal.Error($"Failed to download {failedFiles.Count} files. Closing launcher in 5 seconds..."); - await Task.Delay(5000); - Environment.Exit(1); - } - } - catch (ApiException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden) - { - Terminal.Error("You are not whitelisted on ClassicCounter! (https://classiccounter.cc/whitelist)"); - Terminal.Error("If you are whitelisted, check if you have Steam installed & you're logged into the whitelisted account."); - Terminal.Error("If you're still facing issues, use one of our other download links to download the game."); - Terminal.Warning("Closing launcher in 10 seconds..."); - await Task.Delay(10000); - Environment.Exit(1); - } - catch (ApiException ex) - { - Terminal.Error($"Failed to get game files from API: {ex.Message}"); - Terminal.Error("Closing launcher in 5 seconds..."); - await Task.Delay(5000); - Environment.Exit(1); - } - catch (Exception ex) - { - Terminal.Error($"An error occurred: {ex.Message}"); - Terminal.Error("Closing launcher in 5 seconds..."); - await Task.Delay(5000); - Environment.Exit(1); - } - } - - /// <summary> - /// Downloads and installs the full game from ClassicCounter's CDN. - /// Designed for use from a GUI — takes progress/status callbacks instead of a StatusContext. - /// Throws on error so the caller can handle it. - /// </summary> - public static async Task InstallFullGame( - Action<string, string, double>? onProgress, // (filename, speed, totalPercent) - Action<string>? onStatus, - Action<double>? onExtractProgress = null) - { - await Steam.GetRecentLoggedInSteamID(); - if (string.IsNullOrEmpty(Steam.recentSteamID2)) - throw new Exception("Steam does not appear to be installed or you are not logged in."); - - onStatus?.Invoke("Fetching game files..."); - var gameFiles = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); - - if (gameFiles?.Files == null || gameFiles.Files.Count == 0) - throw new Exception("No game files returned. You may not be whitelisted.\nVisit classiccounter.cc/whitelist to request access."); - - int total = gameFiles.Files.Count; - int completed = 0; - - foreach (var file in gameFiles.Files) - { - string filePath = Path.Combine(Directory.GetCurrentDirectory(), file.File); - - if (File.Exists(filePath) && - CalculateMD5(filePath).Equals(file.Hash, StringComparison.OrdinalIgnoreCase)) - { - completed++; - onProgress?.Invoke(file.File, "", (double)completed / total * 100.0); - continue; - } - - using var downloader = new DownloadService(_settings); - downloader.DownloadProgressChanged += (s, e) => - onProgress?.Invoke( - file.File, - $"{e.BytesPerSecondSpeed / 1024.0 / 1024.0:F1} MB/s", - (completed + e.ProgressPercentage / 100.0) / total * 100.0); - - await downloader.DownloadFileTaskAsync(file.Link, filePath); - completed++; - } - - onStatus?.Invoke("Extracting game files..."); - await ExtractSplitArchive(gameFiles.Files.Select(f => f.File).ToList(), onExtractProgress); - } - - private static string CalculateMD5(string filename) - { - using (var md5 = System.Security.Cryptography.MD5.Create()) - using (var stream = File.OpenRead(filename)) - { - byte[] hash = md5.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - - // meant only for downloading whole game for now - // todo maybe make it more modular/allow other functions to use this - public static async Task ExtractSplitArchive(List<string> files, Action<double>? onProgress = null) - { - if (files == null || files.Count == 0) - { - throw new ArgumentException("No files provided for extraction"); - } - - files.Sort(); - - if (Debug.Enabled()) - { - Terminal.Debug($"Starting extraction of split archive:"); - foreach (var file in files) - { - Terminal.Debug($"Found part: {file}"); - } - } - - string firstFile = files[0]; - string extractPath = Directory.GetCurrentDirectory(); - string tempExtractPath = Path.Combine(extractPath, "ClassicCounter_temp"); - - try - { - Directory.CreateDirectory(tempExtractPath); - - await Download7za(); - - string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); - if (launcherDir == null) - { - throw new InvalidOperationException("Could not determine launcher directory"); - } - - string exePath = Path.Combine(launcherDir, "7za.exe"); - - using (var process = new Process()) - { - process.StartInfo = new ProcessStartInfo - { - FileName = exePath, - Arguments = $"x \"{firstFile}\" -o\"{tempExtractPath}\" -y", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - - if (Debug.Enabled()) - Terminal.Debug($"Starting extraction to temp directory..."); - - process.OutputDataReceived += (_, e) => - { - if (string.IsNullOrWhiteSpace(e.Data)) return; - var pct = TryParseSevenZipProgress(e.Data); - if (pct.HasValue) onProgress?.Invoke(pct.Value); - }; - - process.Start(); - process.BeginOutputReadLine(); - await process.WaitForExitAsync(); - - if (process.ExitCode != 0) - { - throw new Exception($"7za extraction failed with exit code: {process.ExitCode}"); - } - } - - string classicCounterPath = Path.Combine(tempExtractPath, "ClassicCounter"); - if (Directory.Exists(classicCounterPath)) - { - if (Debug.Enabled()) - Terminal.Debug("Moving contents from ClassicCounter folder to root directory..."); - - // first, get all files and directories from the ClassicCounter folder - foreach (string dirPath in Directory.GetDirectories(classicCounterPath, "*", SearchOption.AllDirectories)) - { - // create directory in root, removing the "ClassicCounter" part from the path - string newDirPath = dirPath.Replace(classicCounterPath, extractPath); - Directory.CreateDirectory(newDirPath); - } - - foreach (string filePath in Directory.GetFiles(classicCounterPath, "*.*", SearchOption.AllDirectories)) - { - string newFilePath = filePath.Replace(classicCounterPath, extractPath); - - // skip launcher.exe - if (Path.GetFileName(filePath).Equals("launcher.exe", StringComparison.OrdinalIgnoreCase)) - { - if (Debug.Enabled()) - Terminal.Debug("Skipping launcher.exe"); - continue; - } - - try - { - if (File.Exists(newFilePath)) - { - File.Delete(newFilePath); - } - File.Move(filePath, newFilePath); - } - catch (Exception ex) - { - Terminal.Warning($"Failed to move file {filePath}: {ex.Message}"); - } - } - } - else - { - throw new DirectoryNotFoundException("ClassicCounter folder not found in extracted contents"); - } - - try - { - Directory.Delete(tempExtractPath, true); - if (Debug.Enabled()) - Terminal.Debug("Deleted temporary extraction directory"); - - foreach (string file in files) - { - File.Delete(file); - if (Debug.Enabled()) - Terminal.Debug($"Deleted archive part: {file}"); - } - } - catch (Exception ex) - { - Terminal.Warning($"Failed to cleanup some temporary files: {ex.Message}"); - } - - if (Debug.Enabled()) - Terminal.Debug("Extraction and file movement completed successfully!"); - } - catch (Exception ex) - { - Terminal.Error($"Extraction failed: {ex.Message}"); - if (Debug.Enabled()) - Terminal.Debug($"Stack trace: {ex.StackTrace}"); - - try - { - if (Directory.Exists(tempExtractPath)) - Directory.Delete(tempExtractPath, true); - } - catch { } - - throw; - } - } - - // FOR DOWNLOAD STATUS - public static int dotCount = 0; - public static DateTime lastDotUpdate = DateTime.Now; - public static string GetDots() - { - if ((DateTime.Now - lastDotUpdate).TotalMilliseconds > 500) - { - dotCount = (dotCount + 1) % 4; - lastDotUpdate = DateTime.Now; - } - return "...".Substring(0, dotCount); - } - public static string GetProgressBar(double percentage) - { - int blocks = 16; - int level = (int)(percentage / (100.0 / (blocks * 3))); - string bar = ""; - - for (int i = 0; i < blocks; i++) - { - int blockLevel = Math.Min(3, Math.Max(0, level - (i * 3))); - bar += blockLevel switch - { - 0 => "░", - 1 => "▒", - 2 => "▓", - 3 => "█", - _ => "█" - }; - } - return bar; - } - // DOWNLOAD STATUS OVER - - - - private static async Task Download7za() - { - string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); - if (launcherDir == null) - throw new InvalidOperationException("Could not determine launcher directory"); - - string exePath = Path.Combine(launcherDir, "7za.exe"); - if (File.Exists(exePath)) return; - - string[] fallbackUrls = - { - "https://fastdl.classiccounter.cc/7za.exe", - "https://ollumcc.github.io/7za.exe" - }; - - bool downloaded = false; - int retryCount = 0; - - while (!downloaded && retryCount < 10) - { - if (Debug.Enabled()) - Terminal.Debug($"7za.exe not found, downloading... (Attempt {retryCount + 1}/10)"); - - try - { - using var downloader = new DownloadService(_settings); - await downloader.DownloadFileTaskAsync(fallbackUrls[retryCount % fallbackUrls.Length], exePath); - - if (File.Exists(exePath)) - { - downloaded = true; - if (Debug.Enabled()) - Terminal.Debug($"Downloaded 7za.exe to: {exePath}"); - } - else - { - Terminal.Error($"Failed to download 7za.exe! Trying again... (Attempt {retryCount + 1})"); - retryCount++; - } - } - catch (Exception ex) - { - if (Debug.Enabled()) - Terminal.Debug($"Failed to download 7za.exe: {ex.Message}"); - retryCount++; - } - - if (retryCount > 0) - await Task.Delay(1000); - } - - if (!downloaded) - { - Terminal.Error("Couldn't download 7za.exe! Launcher will close in 5 seconds..."); - await Task.Delay(5000); - Environment.Exit(1); - } - } - - private static async Task Extract7z(string archivePath, string outputPath, Action<double>? onProgress = null) - { - try - { - if (!File.Exists(archivePath)) - { - if (Debug.Enabled()) - Terminal.Debug($"Archive file not found: {archivePath}"); - return; - } - - await Download7za(); - - string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); - if (launcherDir == null) - { - throw new InvalidOperationException("Could not determine launcher directory"); - } - - string exePath = Path.Combine(launcherDir, "7za.exe"); - - using (var process = new Process()) - { - process.StartInfo = new ProcessStartInfo - { - FileName = exePath, - Arguments = $"x \"{archivePath}\" -o\"{Path.GetDirectoryName(outputPath)}\" -y", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - - if (Debug.Enabled()) - Terminal.Debug($"Starting extraction..."); - - process.OutputDataReceived += (_, e) => - { - if (string.IsNullOrWhiteSpace(e.Data)) return; - var pct = TryParseSevenZipProgress(e.Data); - if (pct.HasValue) onProgress?.Invoke(pct.Value); - }; - - process.Start(); - process.BeginOutputReadLine(); - await process.WaitForExitAsync(); - - if (process.ExitCode != 0) - { - throw new Exception($"7za extraction failed with exit code: {process.ExitCode}"); - } - - if (Debug.Enabled()) - Terminal.Debug("Extraction completed successfully!"); - } - - // delete 7z after extract - try - { - File.Delete(archivePath); - if (Debug.Enabled()) - Terminal.Debug($"Deleted archive file: {archivePath}"); - } - catch (Exception ex) - { - if (Debug.Enabled()) - Terminal.Debug($"Failed to delete archive file: {ex.Message}"); - } - } - catch (Exception ex) - { - Terminal.Error($"Extraction failed: {ex.Message}\nStack trace: {ex.StackTrace}"); - throw; - } - } - - private static readonly Regex SevenZipPercentRegex = new(@"\b(\d{1,3})%\b", RegexOptions.Compiled); - - private static double? TryParseSevenZipProgress(string line) - { - var match = SevenZipPercentRegex.Match(line); - if (!match.Success) return null; - if (!double.TryParse(match.Groups[1].Value, out var pct)) return null; - return Math.Clamp(pct, 0, 100); - } - - public static void Cleanup7zFiles() - { - try - { - string directory = Directory.GetCurrentDirectory(); - var files = Directory.GetFiles(directory, "*.7z", SearchOption.AllDirectories); - - foreach (string file in files) - { - try - { - File.Delete(file); - if (Debug.Enabled()) - Terminal.Debug($"Deleted .7z file: {file}"); - } - catch (Exception ex) - { - if (Debug.Enabled()) - Terminal.Debug($"Failed to delete .7z file {file}: {ex.Message}"); - } - } - - // Delete 7za.exe if it exists - string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); - if (launcherDir != null) - { - string sevenZaPath = Path.Combine(launcherDir, "7za.exe"); - if (File.Exists(sevenZaPath)) - { - try - { - File.Delete(sevenZaPath); - if (Debug.Enabled()) - Terminal.Debug("Deleted 7za.exe"); - } - catch (Exception ex) - { - if (Debug.Enabled()) - Terminal.Debug($"Failed to delete 7za.exe: {ex.Message}"); - } - } - } - } - catch (Exception ex) - { - if (Debug.Enabled()) - Terminal.Debug($"Failed to perform cleanup: {ex.Message}"); - } - } - } -} +using Downloader; +using Refit; +using SharpCompress.Archives; +using SharpCompress.Archives.SevenZip; +using SharpCompress.Common; +using SharpCompress.Readers; +using Spectre.Console; + +namespace Wauncher.Utils +{ + public static class DownloadManager + { + private static readonly DownloadConfiguration _settings = new() + { + ChunkCount = 8, + ParallelDownload = true + }; + // Shared only for DownloadUpdater / DownloadDependencies (console-launcher, always sequential) + private static DownloadService _downloader = new DownloadService(_settings); + + public static async Task DownloadUpdater(string path) + { + await _downloader.DownloadFileTaskAsync( + $"https://github.com/ClassicCounter/updater/releases/download/updater/updater.exe", + path + ); + } + + public static async Task<Dependencies> DownloadDependencies(StatusContext ctx, List<Dependency> dependencies) + { + List<Dependency> local = new List<Dependency>(); + List<Dependency> remote = new List<Dependency>(); + Dependencies? _dependencies; + foreach (var dependency in dependencies) + { + if (!DependencyManager.IsInstalled(ctx, dependency)) + { + if (dependency.URL != null) + { + string path = Directory.GetCurrentDirectory() + dependency.Path; + if (File.Exists(path)) + File.Delete(path); + if (Debug.Enabled()) + Terminal.Debug($"Downloading {dependency.Name}"); + await _downloader.DownloadFileTaskAsync( + $"{dependency.URL}", + $"{Directory.GetCurrentDirectory()}{dependency.Path}"); + remote.Add(dependency); + } + else + { + local.Add(dependency); + } + } + } + _dependencies = new Dependencies(false, local, remote); + return _dependencies; + } + + public static async Task DownloadPatch( + Patch patch, + bool validateAll = false, + Action<Downloader.DownloadProgressChangedEventArgs>? onProgress = null, + Action? onExtract = null, + Action<double>? onExtractProgress = null) + { + string originalFileName = patch.File.EndsWith(".7z") ? patch.File[..^3] : patch.File; + string downloadPath = $"{Directory.GetCurrentDirectory()}/{patch.File}"; + + if (Debug.Enabled()) + Terminal.Debug($"Starting download of: {patch.File}"); + + if (patch.File.EndsWith(".7z") && File.Exists(downloadPath)) + { + try + { + if (Debug.Enabled()) + Terminal.Debug($"Found existing .7z file, trying to delete: {downloadPath}"); + File.Delete(downloadPath); + } + catch (Exception ex) + { + if (Debug.Enabled()) + Terminal.Debug($"Failed to delete existing .7z file: {ex.Message}"); + } + } + + string baseUrl = "https://patch.classiccounter.cc"; + + // Use a fresh DownloadService per call so concurrent or back-to-back downloads + // never share state on the same instance. + using var downloader = new DownloadService(_settings); + if (onProgress != null) + downloader.DownloadProgressChanged += (sender, e) => onProgress(e); + + await downloader.DownloadFileTaskAsync( + $"{baseUrl}/{patch.File}", + $"{Directory.GetCurrentDirectory()}/{patch.File}" + ); + + if (patch.File.EndsWith(".7z")) + { + if (Debug.Enabled()) + Terminal.Debug($"Download complete, starting extraction of: {patch.File}"); + onExtract?.Invoke(); + string extractPath = $"{Directory.GetCurrentDirectory()}/{originalFileName}"; + await Extract7z(downloadPath, extractPath, onExtractProgress); + } + } + + public static async Task HandlePatches(Patches patches, StatusContext ctx, bool isGameFiles, int startingProgress = 0) + { + string fileType = isGameFiles ? "game file" : "patch"; + string fileTypePlural = isGameFiles ? "game files" : "patches"; + + var allFiles = patches.Missing.Concat(patches.Outdated).ToList(); + int totalFiles = allFiles.Count; + int completedFiles = startingProgress; + int failedFiles = 0; + + // status update + Action<Downloader.DownloadProgressChangedEventArgs, string> updateStatus = (progress, filename) => + { + var speed = progress.BytesPerSecondSpeed / (1024.0 * 1024.0); + var progressText = $"{((float)completedFiles / totalFiles * 100):F1}% ({completedFiles}/{totalFiles})"; + var status = filename.EndsWith(".7z") && progress.ProgressPercentage >= 100 ? "Extracting" : "Downloading new"; + ctx.Status = $"{status} {fileTypePlural}{GetDots().PadRight(3)} [gray]|[/] {progressText} [gray]|[/] {GetProgressBar(progress.ProgressPercentage)} {progress.ProgressPercentage:F1}% [gray]|[/] {speed:F1} MB/s"; + }; + + foreach (var patch in allFiles) + { + try + { + await DownloadPatch(patch, isGameFiles, progress => updateStatus(progress, patch.File)); + completedFiles++; + } + catch + { + failedFiles++; + Terminal.Warning($"Couldn't process {fileType}: {patch.File}, possibly due to missing permissions."); + } + } + + if (failedFiles > 0) + Terminal.Warning($"Couldn't download {failedFiles} {(failedFiles == 1 ? fileType : fileTypePlural)}!"); + } + + public static async Task DownloadFullGame(StatusContext ctx) + { + try + { + await Steam.GetRecentLoggedInSteamID(); + if (string.IsNullOrEmpty(Steam.recentSteamID2)) + { + Terminal.Error("Steam does not seem to be installed. Please make sure that you have Steam installed."); + Terminal.Error("Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + return; + } + + var gameFiles = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); + + if (gameFiles?.Files == null || gameFiles.Files.Count == 0) + { + Terminal.Error("No game files returned from the API. You may not be whitelisted."); + Terminal.Error("Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + return; + } + + int totalFiles = gameFiles.Files.Count; + int completedFiles = 0; + List<string> failedFiles = new List<string>(); + + foreach (var file in gameFiles.Files) + { + string filePath = Path.Combine(Directory.GetCurrentDirectory(), file.File); + bool needsDownload = true; + + if (File.Exists(filePath)) + { + string fileHash = CalculateMD5(filePath); + if (fileHash.Equals(file.Hash, StringComparison.OrdinalIgnoreCase)) + { + needsDownload = false; + completedFiles++; + continue; + } + } + + if (needsDownload) + { + try + { + EventHandler<Downloader.DownloadProgressChangedEventArgs> progressHandler = (sender, e) => + { + var speed = e.BytesPerSecondSpeed / (1024.0 * 1024.0); + var progressText = $"{((float)completedFiles / totalFiles * 100):F1}% ({completedFiles}/{totalFiles})"; + ctx.Status = $"Downloading {file.File}{GetDots().PadRight(3)} [gray]|[/] {progressText} [gray]|[/] {GetProgressBar(e.ProgressPercentage)} {e.ProgressPercentage:F1}% [gray]|[/] {speed:F1} MB/s"; + }; + _downloader.DownloadProgressChanged += progressHandler; + + try + { + await _downloader.DownloadFileTaskAsync(file.Link, filePath); + + string downloadedHash = CalculateMD5(filePath); + if (!downloadedHash.Equals(file.Hash, StringComparison.OrdinalIgnoreCase)) + { + failedFiles.Add(file.File); + Terminal.Error($"Hash mismatch for {file.File}"); + continue; + } + + completedFiles++; + } + finally + { + _downloader.DownloadProgressChanged -= progressHandler; + } + } + catch (Exception ex) + { + failedFiles.Add(file.File); + Terminal.Error($"Failed to download {file.File}: {ex.Message}"); + } + } + } + + if (failedFiles.Count == 0) + { + ctx.Status = "Extracting game files... Please do not close the launcher."; + await ExtractSplitArchive(gameFiles.Files.Select(f => f.File).ToList()); + Terminal.Success("Game files downloaded and extracted successfully!"); + } + else + { + Terminal.Error($"Failed to download {failedFiles.Count} files. Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + } + } + catch (ApiException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + Terminal.Error("You are not whitelisted on ClassicCounter! (https://classiccounter.cc/whitelist)"); + Terminal.Error("If you are whitelisted, check if you have Steam installed & you're logged into the whitelisted account."); + Terminal.Error("If you're still facing issues, use one of our other download links to download the game."); + Terminal.Warning("Closing launcher in 10 seconds..."); + await Task.Delay(10000); + Environment.Exit(1); + } + catch (ApiException ex) + { + Terminal.Error($"Failed to get game files from API: {ex.Message}"); + Terminal.Error("Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + } + catch (Exception ex) + { + Terminal.Error($"An error occurred: {ex.Message}"); + Terminal.Error("Closing launcher in 5 seconds..."); + await Task.Delay(5000); + Environment.Exit(1); + } + } + /// <summary> + /// Downloads and installs the full game from ClassicCounter's CDN. + /// Designed for use from a GUI — takes progress/status callbacks instead of a StatusContext. + /// Throws on error so the caller can handle it. + /// </summary> + public static async Task InstallFullGame( + Action<string, string, double>? onProgress, // (filename, speed, totalPercent) + Action<string>? onStatus, + Action<double>? onExtractProgress = null) + { + await Steam.GetRecentLoggedInSteamID(); + if (string.IsNullOrEmpty(Steam.recentSteamID2)) + throw new Exception("Steam does not appear to be installed or you are not logged in."); + + onStatus?.Invoke("Fetching game files..."); + var gameFiles = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); + + if (gameFiles?.Files == null || gameFiles.Files.Count == 0) + throw new Exception("No game files returned. You may not be whitelisted.\nVisit classiccounter.cc/whitelist to request access."); + + int total = gameFiles.Files.Count; + int completed = 0; + + foreach (var file in gameFiles.Files) + { + string filePath = Path.Combine(Directory.GetCurrentDirectory(), file.File); + + if (File.Exists(filePath) && + CalculateMD5(filePath).Equals(file.Hash, StringComparison.OrdinalIgnoreCase)) + { + completed++; + onProgress?.Invoke(file.File, "", (double)completed / total * 100.0); + continue; + } + + using var downloader = new DownloadService(_settings); + downloader.DownloadProgressChanged += (s, e) => + onProgress?.Invoke( + file.File, + $"{e.BytesPerSecondSpeed / 1024.0 / 1024.0:F1} MB/s", + (completed + e.ProgressPercentage / 100.0) / total * 100.0); + + await downloader.DownloadFileTaskAsync(file.Link, filePath); + completed++; + } + + onStatus?.Invoke("Extracting game files..."); + await ExtractSplitArchive(gameFiles.Files.Select(f => f.File).ToList(), onExtractProgress); + } + + private static string CalculateMD5(string filename) + { + using (var md5 = System.Security.Cryptography.MD5.Create()) + using (var stream = File.OpenRead(filename)) + { + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + + // meant only for downloading whole game for now + // todo maybe make it more modular/allow other functions to use this + // FOR DOWNLOAD STATUS + public static int dotCount = 0; + public static DateTime lastDotUpdate = DateTime.Now; + public static string GetDots() + { + if ((DateTime.Now - lastDotUpdate).TotalMilliseconds > 500) + { + dotCount = (dotCount + 1) % 4; + lastDotUpdate = DateTime.Now; + } + return "...".Substring(0, dotCount); + } + public static string GetProgressBar(double percentage) + { + int blocks = 16; + int level = (int)(percentage / (100.0 / (blocks * 3))); + string bar = ""; + + for (int i = 0; i < blocks; i++) + { + int blockLevel = Math.Min(3, Math.Max(0, level - (i * 3))); + bar += blockLevel switch + { + 0 => "¦", + 1 => "¦", + 2 => "¦", + 3 => "¦", + _ => "¦" + }; + } + return bar; + } + // DOWNLOAD STATUS OVER + public static async Task ExtractSplitArchive(List<string> files, Action<double>? onProgress = null) + { + if (files == null || files.Count == 0) + { + throw new ArgumentException("No files provided for extraction"); + } + + files.Sort(); + + if (Debug.Enabled()) + { + Terminal.Debug("Starting extraction of split archive:"); + foreach (var file in files) + { + Terminal.Debug($"Found part: {file}"); + } + } + + string firstFile = files[0]; + string extractPath = Directory.GetCurrentDirectory(); + string tempExtractPath = Path.Combine(extractPath, "ClassicCounter_temp"); + + try + { + Directory.CreateDirectory(tempExtractPath); + + if (Debug.Enabled()) + Terminal.Debug("Starting in-process extraction to temp directory..."); + + await ExtractSplitArchiveToDirectory(files, tempExtractPath, onProgress); + + string classicCounterPath = Path.Combine(tempExtractPath, "ClassicCounter"); + if (Directory.Exists(classicCounterPath)) + { + if (Debug.Enabled()) + Terminal.Debug("Moving contents from ClassicCounter folder to root directory..."); + await Task.Run(() => MoveExtractedClassicCounterFiles(classicCounterPath, extractPath)); + } + else + { + throw new DirectoryNotFoundException("ClassicCounter folder not found in extracted contents"); + } + + try + { + Directory.Delete(tempExtractPath, true); + if (Debug.Enabled()) + Terminal.Debug("Deleted temporary extraction directory"); + + foreach (string file in files) + { + File.Delete(file); + if (Debug.Enabled()) + Terminal.Debug($"Deleted archive part: {file}"); + } + } + catch (Exception ex) + { + Terminal.Warning($"Failed to cleanup some temporary files: {ex.Message}"); + } + + if (Debug.Enabled()) + Terminal.Debug("Extraction and file movement completed successfully!"); + } + catch (Exception ex) + { + Terminal.Error($"Extraction failed: {ex.Message}"); + if (Debug.Enabled()) + Terminal.Debug($"Stack trace: {ex.StackTrace}"); + + try + { + if (Directory.Exists(tempExtractPath)) + Directory.Delete(tempExtractPath, true); + } + catch { } + + throw; + } + } + + private static async Task Extract7z(string archivePath, string outputPath, Action<double>? onProgress = null) + { + try + { + if (!File.Exists(archivePath)) + { + if (Debug.Enabled()) + Terminal.Debug($"Archive file not found: {archivePath}"); + return; + } + + await ExtractArchiveToDirectory(archivePath, Path.GetDirectoryName(outputPath)!, onProgress); + + try + { + File.Delete(archivePath); + if (Debug.Enabled()) + Terminal.Debug($"Deleted archive file: {archivePath}"); + } + catch (Exception ex) + { + if (Debug.Enabled()) + Terminal.Debug($"Failed to delete archive file: {ex.Message}"); + } + } + catch (Exception ex) + { + Terminal.Error($"Extraction failed: {ex.Message}\nStack trace: {ex.StackTrace}"); + throw; + } + } + + private static void MoveExtractedClassicCounterFiles(string classicCounterPath, string extractPath) + { + foreach (string dirPath in Directory.GetDirectories(classicCounterPath, "*", SearchOption.AllDirectories)) + { + string newDirPath = dirPath.Replace(classicCounterPath, extractPath); + Directory.CreateDirectory(newDirPath); + } + + foreach (string filePath in Directory.GetFiles(classicCounterPath, "*.*", SearchOption.AllDirectories)) + { + string newFilePath = filePath.Replace(classicCounterPath, extractPath); + + if (Path.GetFileName(filePath).Equals("launcher.exe", StringComparison.OrdinalIgnoreCase)) + { + if (Debug.Enabled()) + Terminal.Debug("Skipping launcher.exe"); + continue; + } + + try + { + if (File.Exists(newFilePath)) + { + File.Delete(newFilePath); + } + File.Move(filePath, newFilePath); + } + catch (Exception ex) + { + Terminal.Warning($"Failed to move file {filePath}: {ex.Message}"); + } + } + } + + private static async Task ExtractArchiveToDirectory(string archivePath, string outputDirectory, Action<double>? onProgress = null) + { + await Task.Run(() => + { + using var archive = ArchiveFactory.OpenArchive(new FileInfo(archivePath), new ReaderOptions()); + var entries = archive.Entries.Where(entry => !entry.IsDirectory).ToArray(); + int totalEntries = entries.Length > 0 ? entries.Length : 1; + int completedEntries = 0; + + onProgress?.Invoke(0); + + foreach (var entry in entries) + { + entry.WriteToDirectory(outputDirectory, new ExtractionOptions + { + ExtractFullPath = true, + Overwrite = true + }); + + completedEntries++; + onProgress?.Invoke((double)completedEntries / totalEntries * 100.0); + } + }); + } + + + private static async Task ExtractSplitArchiveToDirectory(IEnumerable<string> archiveParts, string outputDirectory, Action<double>? onProgress = null) + { + await Task.Run(() => + { + var parts = archiveParts + .Select(part => new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), part))) + .ToArray(); + + using var archive = SevenZipArchive.OpenArchive(parts, new ReaderOptions()); + var entries = archive.Entries.Where(entry => !entry.IsDirectory).ToArray(); + int totalEntries = entries.Length > 0 ? entries.Length : 1; + int completedEntries = 0; + + onProgress?.Invoke(0); + + foreach (var entry in entries) + { + entry.WriteToDirectory(outputDirectory, new ExtractionOptions + { + ExtractFullPath = true, + Overwrite = true + }); + + completedEntries++; + onProgress?.Invoke((double)completedEntries / totalEntries * 100.0); + } + }); + } + + public static void Cleanup7zFiles() + { + try + { + string directory = Directory.GetCurrentDirectory(); + var files = Directory.GetFiles(directory, "*.7z", SearchOption.AllDirectories); + + foreach (string file in files) + { + try + { + File.Delete(file); + if (Debug.Enabled()) + Terminal.Debug($"Deleted .7z file: {file}"); + } + catch (Exception ex) + { + if (Debug.Enabled()) + Terminal.Debug($"Failed to delete .7z file {file}: {ex.Message}"); + } + } + } + catch (Exception ex) + { + if (Debug.Enabled()) + Terminal.Debug($"Failed to perform cleanup: {ex.Message}"); + } + } + } +} + + diff --git a/Wauncher/Utils/Game.cs b/Wauncher/Utils/Game.cs index 6541ca2..4002735 100644 --- a/Wauncher/Utils/Game.cs +++ b/Wauncher/Utils/Game.cs @@ -1,4 +1,4 @@ -using CSGSI; +using CSGSI; using CSGSI.Nodes; using System.Diagnostics; using System.Net.NetworkInformation; @@ -20,13 +20,13 @@ public static async Task<bool> Launch() { List<string> arguments = Argument.GenerateGameArguments(); if (arguments.Count > 0) Terminal.Print($"Arguments: {string.Join(" ", arguments)}"); - var settings = ViewModels.SettingsWindowViewModel.LoadGlobal(); + var settings = ViewModels.SettingsWindowViewModel.LoadGlobal(); string directory = Directory.GetCurrentDirectory(); Terminal.Print($"Directory: {directory}"); string gameStatePath = $"{directory}/csgo/cfg/gamestate_integration_cc.cfg"; - + if (settings.DiscordRpc) { _port = GeneratePort(); @@ -35,38 +35,43 @@ public static async Task<bool> Launch() _listener.NewGameState += OnNewGameState; _listener.Start(); - try { - await File.WriteAllTextAsync(gameStatePath, -@"""ClassicCounter"" + try + { + string gameStateContents = $$""" +"ClassicCounter" { - ""uri"" ""http://localhost:" + _port + @""" - ""timeout"" ""5.0"" - ""auth"" + "uri" "http://localhost:{{_port}}" + "timeout" "5.0" + "auth" { - ""token"" """ + $"ClassicCounter {Version.Current}" + @""" + "token" "ClassicCounter {{Version.Current}}" } - ""data"" + "data" { - ""provider"" ""1"" - ""map"" ""1"" - ""round"" ""1"" - ""player_id"" ""1"" - ""player_weapons"" ""1"" - ""player_match_stats"" ""1"" - ""player_state"" ""1"" - ""allplayers_id"" ""1"" - ""allplayers_state"" ""1"" - ""allplayers_match_stats"" ""1"" + "provider" "1" + "map" "1" + "round" "1" + "player_id" "1" + "player_weapons" "1" + "player_match_stats" "1" + "player_state" "1" + "allplayers_id" "1" + "allplayers_state" "1" + "allplayers_match_stats" "1" } -}" - ); +} +"""; + await File.WriteAllTextAsync(gameStatePath, gameStateContents); } catch { - Terminal.Error($"(!) \"/csgo/cfg/gamestate_integration_cc.cfg\" not found in the current directory!"); + Terminal.Error("(!) \"/csgo/cfg/gamestate_integration_cc.cfg\" not found in the current directory!"); } } - else if (File.Exists(gameStatePath)) File.Delete(gameStatePath); + else if (File.Exists(gameStatePath)) + { + File.Delete(gameStatePath); + } _process = new Process(); @@ -82,7 +87,6 @@ await File.WriteAllTextAsync(gameStatePath, } return _process.Start(); - } public static async Task Monitor() @@ -93,27 +97,37 @@ public static async Task Monitor() { if (_node != null && _node.Name.Trim().Length != 0) { - if (_map != _node.Name) + bool isMainMenu = string.Equals(_node.Name, "main_menu", StringComparison.OrdinalIgnoreCase); + if (!isMainMenu) { - _map = _node.Name; - _scoreCT = _node.TeamCT.Score; - _scoreT = _node.TeamT.Score; - - Discord.SetDetails(_map); - Discord.SetState($"Score → {_scoreCT}:{_scoreT}"); - Discord.SetTimestamp(DateTime.UtcNow); - Discord.SetLargeArtwork($"https://assets.classiccounter.cc/maps/default/{_map}.jpg"); - Discord.SetSmallArtwork("icon"); - Discord.Update(); + if (_map != _node.Name) + { + _map = _node.Name; + _scoreCT = _node.TeamCT.Score; + _scoreT = _node.TeamT.Score; + + Discord.SetDetails(_map); + Discord.SetState($"Score → {_scoreCT}:{_scoreT}"); + Discord.SetTimestamp(DateTime.UtcNow); + Discord.SetLargeArtwork($"https://assets.classiccounter.cc/maps/default/{_map}.jpg"); + Discord.SetSmallArtwork("icon"); + Discord.Update(); + } + + if (_scoreCT != _node.TeamCT.Score || _scoreT != _node.TeamT.Score) + { + _scoreCT = _node.TeamCT.Score; + _scoreT = _node.TeamT.Score; + + Discord.SetState($"Score → {_scoreCT}:{_scoreT}"); + Discord.Update(); + } } - - if (_scoreCT != _node.TeamCT.Score || _scoreT != _node.TeamT.Score) + else { - _scoreCT = _node.TeamCT.Score; - _scoreT = _node.TeamT.Score; - - Discord.SetState($"Score → {_scoreCT}:{_scoreT}"); - Discord.Update(); + _map = "main_menu"; + _scoreCT = 0; + _scoreT = 0; } } else if (_map != "main_menu") @@ -135,6 +149,10 @@ public static async Task Monitor() _listener?.Stop(); _listener = null; + _node = null; + _map = "main_menu"; + _scoreCT = 0; + _scoreT = 0; } private static int GeneratePort() @@ -153,4 +171,3 @@ private static int GeneratePort() public static void OnNewGameState(GameState gs) => _node = gs.Map; } } - diff --git a/Wauncher/Utils/ServerQuery.cs b/Wauncher/Utils/ServerQuery.cs index 5b06876..112f1e1 100644 --- a/Wauncher/Utils/ServerQuery.cs +++ b/Wauncher/Utils/ServerQuery.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Sockets; using System.Text; +using System.Collections.Concurrent; namespace Wauncher.Utils { @@ -14,12 +15,20 @@ public class ServerQueryResult public static class ServerQuery { + private sealed class CachedHostEntry + { + public IPAddress[] Addresses { get; init; } = Array.Empty<IPAddress>(); + public DateTime ExpiresAtUtc { get; init; } + } + private static readonly byte[] A2S_INFO_REQUEST = { 0xFF, 0xFF, 0xFF, 0xFF, 0x54, 0x53, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x20, 0x45, 0x6E, 0x67, 0x69, 0x6E, 0x65, 0x20, 0x51, 0x75, 0x65, 0x72, 0x79, 0x00 }; + private static readonly ConcurrentDictionary<string, CachedHostEntry> _dnsCache = new(); + private static readonly TimeSpan DnsCacheDuration = TimeSpan.FromMinutes(5); public static async Task<ServerQueryResult> QueryAsync(string ipPort, int timeoutMs = 2000) { @@ -30,7 +39,7 @@ public static async Task<ServerQueryResult> QueryAsync(string ipPort, int timeou string host = parts[0]; int port = int.Parse(parts[1]); - var addresses = await Dns.GetHostAddressesAsync(host); + var addresses = await GetHostAddressesCachedAsync(host); if (addresses.Length == 0) return result; var endpoint = new IPEndPoint(addresses[0], port); @@ -108,5 +117,23 @@ public static async Task RefreshServers(IEnumerable<Wauncher.ViewModels.ServerIn await Task.WhenAll(tasks); } + + private static async Task<IPAddress[]> GetHostAddressesCachedAsync(string host) + { + if (_dnsCache.TryGetValue(host, out var cached) && + cached.ExpiresAtUtc > DateTime.UtcNow && + cached.Addresses.Length > 0) + { + return cached.Addresses; + } + + var addresses = await Dns.GetHostAddressesAsync(host); + _dnsCache[host] = new CachedHostEntry + { + Addresses = addresses, + ExpiresAtUtc = DateTime.UtcNow.Add(DnsCacheDuration) + }; + return addresses; + } } } diff --git a/Wauncher/Views/InfoWindow.axaml b/Wauncher/Views/InfoWindow.axaml index 5c275ac..4532358 100644 --- a/Wauncher/Views/InfoWindow.axaml +++ b/Wauncher/Views/InfoWindow.axaml @@ -18,6 +18,13 @@ Background="Transparent" mc:Ignorable="d"> + <Window.Resources> + <SolidColorBrush x:Key="InfoOwnerBrush" Color="#E74C3C" /> + <SolidColorBrush x:Key="InfoDevBrush" Color="#6CB6FF" /> + <SolidColorBrush x:Key="InfoContributorBrush" Color="#F1C40F" /> + <SolidColorBrush x:Key="InfoModeratorBrush" Color="#9B59B6" /> + </Window.Resources> + <Window.Styles> <Style Selector="Button.closeBtn"> <Setter Property="Padding" Value="0" /> @@ -153,20 +160,37 @@ FontSize="13" LineHeight="20" Foreground="{DynamicResource AppBodyText}" - TextWrapping="Wrap" - Text="Special thanks to h4rmy, heapy, and eddies for maintaining this project." /> + TextWrapping="Wrap"> + <Run Text="Special thanks to " /> + <Run Foreground="{StaticResource InfoOwnerBrush}" Text="h4rmy" /> + <Run Text=", " /> + <Run Foreground="{StaticResource InfoDevBrush}" Text="heapy" /> + <Run Text=", " /> + <Run Foreground="{StaticResource InfoDevBrush}" Text="Grizzle" /> + <Run Text=", and " /> + <Run Foreground="{StaticResource InfoOwnerBrush}" Text="eddies" /> + <Run Text=" for maintaining this project." /> + </TextBlock> <TextBlock FontSize="13" LineHeight="20" Foreground="{DynamicResource AppBodyText}" - TextWrapping="Wrap" - Text="Original launcher coded by heapy." /> + TextWrapping="Wrap"> + <Run Text="Original launcher coded by " /> + <Run Foreground="{StaticResource InfoDevBrush}" Text="heapy" /> + <Run Text="." /> + </TextBlock> <TextBlock FontSize="13" LineHeight="20" Foreground="{DynamicResource AppBodyText}" - TextWrapping="Wrap" - Text="Wauncher coded by koolych and Ways." /> + TextWrapping="Wrap"> + <Run Text="Wauncher coded by " /> + <Run Foreground="{StaticResource InfoContributorBrush}" Text="koolych" /> + <Run Text=" and " /> + <Run Foreground="{StaticResource InfoModeratorBrush}" Text="Ways" /> + <Run Text="." /> + </TextBlock> <TextBlock FontSize="13" @@ -180,8 +204,11 @@ Margin="0,2,0,8" FontStyle="Italic" Foreground="{DynamicResource AppBodyText}" - TextWrapping="Wrap" - Text="In loving memory of h4rmy." /> + TextWrapping="Wrap"> + <Run Text="In loving memory of " /> + <Run Foreground="{StaticResource InfoOwnerBrush}" Text="h4rmy" /> + <Run Text="." /> + </TextBlock> </StackPanel> </ScrollViewer> diff --git a/Wauncher/Views/MainWindow.axaml b/Wauncher/Views/MainWindow.axaml index 86f2a6a..1f3a51a 100644 --- a/Wauncher/Views/MainWindow.axaml +++ b/Wauncher/Views/MainWindow.axaml @@ -286,6 +286,11 @@ <Style Selector="Button.friendRowBtn:pointerover"> <Setter Property="Background" Value="#14FFFFFF" /> </Style> + <Style Selector="MenuItem.compactFlyoutItem"> + <Setter Property="FontSize" Value="12.5" /> + <Setter Property="Padding" Value="9,5" /> + <Setter Property="MinHeight" Value="0" /> + </Style> <!-- Selected label default (no server) --> <Style Selector="TextBlock.selectedlabel"> @@ -547,9 +552,9 @@ <!-- Profile left + buttons right --> <StackPanel Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Center" Orientation="Horizontal" Spacing="10"> - <Border Width="36" Height="36" Background="{DynamicResource AppInputBg}" ClipToBounds="True" CornerRadius="18"> - <Image asyncImageLoader:ImageLoader.Source="{Binding ProfilePicture}" Stretch="UniformToFill" /> - </Border> + <Border Width="40" Height="40" Background="{DynamicResource AppInputBg}" ClipToBounds="True" CornerRadius="20"> + <Image asyncImageLoader:ImageLoader.Source="{Binding ProfilePicture}" Stretch="UniformToFill" /> + </Border> <StackPanel VerticalAlignment="Center" Spacing="2"> <TextBlock FontSize="13" FontWeight="Bold" Foreground="{DynamicResource AppPrimaryText}" Text="{Binding UsernameGreeting}" /> <StackPanel Orientation="Horizontal" Spacing="5"> @@ -591,8 +596,8 @@ <Button x:Name="ArrowButton" Grid.Column="2" Classes="launchBtn" Classes.updating="{Binding IsUpdatingOrInstalling}" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"> <Button.Flyout> <MenuFlyout Placement="Top"> - <MenuItem Header="Verify Game Files" Click="VerifyGameFiles_Click" /> - <MenuItem Header="Open Game Folder" Click="OpenGameFolder_Click" /> + <MenuItem Classes="compactFlyoutItem" Header="Verify Game Files" Click="VerifyGameFiles_Click" /> + <MenuItem Classes="compactFlyoutItem" Header="Open Game Folder" Click="OpenGameFolder_Click" /> </MenuFlyout> </Button.Flyout> <Path Width="8" Height="5" Data="M0,5 L4,0 L8,5" Stroke="White" StrokeThickness="1.5" /> @@ -699,8 +704,8 @@ <Button Grid.Column="0" Grid.ColumnSpan="3" Classes="friendRowBtn"> <Button.Flyout> <MenuFlyout Placement="Top"> - <MenuItem Header="Join Server" Tag="{Binding}" Click="JoinFriendServer_Click" IsVisible="{Binding CanQuickJoin}" /> - <MenuItem Header="View eddies.cc Profile" Tag="{Binding}" Click="ViewFriendProfile_Click" /> + <MenuItem Classes="compactFlyoutItem" Header="Join Server" Tag="{Binding}" Click="JoinFriendServer_Click" IsVisible="{Binding CanQuickJoin}" /> + <MenuItem Classes="compactFlyoutItem" Header="View eddies.cc Profile" Tag="{Binding}" Click="ViewFriendProfile_Click" /> </MenuFlyout> </Button.Flyout> <Grid ColumnDefinitions="56,*,12" VerticalAlignment="Center" Margin="6,0"> diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index 706e7d1..ecd62da 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -3,22 +3,24 @@ using System.Linq; using System.Net.NetworkInformation; using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Text.Json; -using System.Text; -using System.Text.RegularExpressions; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text.Json; +using System.Text; +using System.Text.RegularExpressions; using Avalonia.Animation; using Avalonia.Animation.Easings; using Avalonia; using Avalonia.Controls; using Avalonia.Input; -using Avalonia.Media; -using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Threading; -using Wauncher.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Threading; +using SkiaSharp; +using Wauncher.Utils; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Wauncher.ViewModels; using Wauncher.Views; @@ -39,13 +41,18 @@ public partial class MainWindow : Window private const double HeightOpen = 720; // ── Image carousel (center content area) ────────────────────────────────── - private Image[] _carouselImages = Array.Empty<Image>(); - private DispatcherTimer? _carouselTimer; - private int _currentCarouselIndex = 0; - private const int CarouselRotationIntervalSeconds = 5; - private readonly List<System.Threading.CancellationTokenSource?> _zoomCts = new(); - private static string WauncherDirectory => - Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); + private Image[] _carouselImages = Array.Empty<Image>(); + private List<string> _carouselImageUrls = new(); + private DispatcherTimer? _carouselTimer; + private int _currentCarouselIndex = 0; + private int _currentCarouselSlot = 0; + private int _carouselRotateInProgress = 0; + private const int CarouselRotationIntervalSeconds = 5; + private const int CarouselMaxWidth = 1280; + private const int CarouselMaxHeight = 720; + private readonly List<System.Threading.CancellationTokenSource?> _zoomCts = new(); + private static string WauncherDirectory => + Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); public MainWindow() { @@ -95,35 +102,42 @@ public MainWindow() // ── Image carousel (center content area) ────────────────────────────────── private static readonly HttpClient _http = new(); - private static string PatchNotesCachePath => - Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "cache", - "patchnotes.md"); - - private async Task SetupCarouselAsync() - { - try - { - TeardownCarousel(); + private static string PatchNotesCachePath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache", + "patchnotes.md"); + private static string CarouselCacheDir => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache", + "carousel"); + + private async Task SetupCarouselAsync() + { + try + { + TeardownCarousel(); var carouselContainer = this.FindControl<Grid>("CarouselContainer"); var offlinePanel = this.FindControl<Border>("CarouselOfflinePanel"); var offlineSubText = this.FindControl<TextBlock>("CarouselOfflineSubText"); if (carouselContainer == null) return; - - bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); - var bitmaps = hasInternet - ? await LoadCarouselFromGitHubAsync() - : null; - - if (bitmaps == null || bitmaps.Count == 0) - { - if (offlinePanel != null) - offlinePanel.IsVisible = true; + + bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); + var urls = hasInternet + ? await LoadCarouselUrlsFromGitHubAsync() + : null; + + if (urls == null || urls.Count == 0) + { + if (offlinePanel != null) + offlinePanel.IsVisible = true; if (offlineSubText != null) { offlineSubText.Text = hasInternet @@ -133,14 +147,15 @@ private async Task SetupCarouselAsync() return; } - if (offlinePanel != null) - offlinePanel.IsVisible = false; - - _carouselImages = CreateCarouselImages(bitmaps); - EnsureZoomSlots(_carouselImages.Length); - - foreach (var existingImage in carouselContainer.Children.OfType<Image>().ToList()) - carouselContainer.Children.Remove(existingImage); + if (offlinePanel != null) + offlinePanel.IsVisible = false; + + _carouselImageUrls = urls; + _carouselImages = CreateCarouselImages(2); + EnsureZoomSlots(_carouselImages.Length); + + foreach (var existingImage in carouselContainer.Children.OfType<Image>().ToList()) + carouselContainer.Children.Remove(existingImage); int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; for (int i = 0; i < _carouselImages.Length; i++) @@ -152,64 +167,55 @@ private async Task SetupCarouselAsync() } else { - carouselContainer.Children.Add(_carouselImages[i]); - } - } - - _currentCarouselIndex = 0; - _carouselImages[0].Opacity = 1.0; - StartZoomOut(_carouselImages[0], 0); - - _carouselTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CarouselRotationIntervalSeconds) }; - _carouselTimer.Tick += (_, _) => RotateCarousel(); - _carouselTimer.Start(); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine("Carousel: " + ex.Message); - } - } - - private async Task<List<Bitmap>?> LoadCarouselFromGitHubAsync() - { - try - { - var json = await Api.GitHub.GetCarouselAssetsWauncher(); - var assets = JsonConvert.DeserializeObject<List<GitHubAssetEntry>>(json); + carouselContainer.Children.Add(_carouselImages[i]); + } + } + + _currentCarouselIndex = 0; + _currentCarouselSlot = 0; + await SetCarouselImageAsync(_carouselImages[_currentCarouselSlot], _carouselImageUrls[_currentCarouselIndex]); + _carouselImages[_currentCarouselSlot].Opacity = 1.0; + StartZoomOut(_carouselImages[_currentCarouselSlot], _currentCarouselSlot); + + _carouselTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CarouselRotationIntervalSeconds) }; + _carouselTimer.Tick += async (_, _) => await RotateCarouselAsync(); + _carouselTimer.Start(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine("Carousel: " + ex.Message); + } + } + + private async Task<List<string>?> LoadCarouselUrlsFromGitHubAsync() + { + try + { + var json = await Api.GitHub.GetCarouselAssetsWauncher(); + var assets = JsonConvert.DeserializeObject<List<GitHubAssetEntry>>(json); if (assets == null || assets.Count == 0) return null; - var urls = assets - .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) - .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || + var urls = assets + .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) + .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) - .OrderBy(a => GetCarouselSortIndex(a.Name)) - .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) - .Select(a => a.DownloadUrl!) - .ToList(); - - if (urls.Count == 0) - return null; - - var bitmaps = new List<Bitmap>(); - foreach (var url in urls) - { - try - { - var bytes = await _http.GetByteArrayAsync(url); - using var ms = new MemoryStream(bytes); - bitmaps.Add(new Bitmap(ms)); - } - catch { } - } - return bitmaps.Count > 0 ? bitmaps : null; - } - catch { return null; } - } + .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) + .OrderBy(a => GetCarouselSortIndex(a.Name)) + .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) + .Select(a => a.DownloadUrl!) + .ToList(); + + if (urls.Count == 0) + return null; + + return urls; + } + catch { return null; } + } private static int GetCarouselSortIndex(string name) { @@ -231,17 +237,16 @@ private sealed class GitHubAssetEntry public string? DownloadUrl { get; set; } } - private static Image[] CreateCarouselImages(IReadOnlyList<Bitmap> bitmaps) - { - var images = new Image[bitmaps.Count]; - for (int i = 0; i < bitmaps.Count; i++) - { - images[i] = new Image - { - Source = bitmaps[i], - Stretch = Stretch.UniformToFill, - Opacity = 0.0, - Transitions = new Transitions + private static Image[] CreateCarouselImages(int count) + { + var images = new Image[count]; + for (int i = 0; i < count; i++) + { + images[i] = new Image + { + Stretch = Stretch.UniformToFill, + Opacity = 0.0, + Transitions = new Transitions { new DoubleTransition { @@ -259,32 +264,164 @@ private static Image[] CreateCarouselImages(IReadOnlyList<Bitmap> bitmaps) private void EnsureZoomSlots(int count) { while (_zoomCts.Count < count) - _zoomCts.Add(null); - } - - private void RotateCarousel() - { - if (_carouselImages.Length == 0) - return; - - // Fade out current image (zoom continues through the crossfade) - _carouselImages[_currentCarouselIndex].Opacity = 0.0; - - // Move to next image - _currentCarouselIndex = (_currentCarouselIndex + 1) % _carouselImages.Length; - - // Fade in next image and start fresh zoom-out - StartZoomOut(_carouselImages[_currentCarouselIndex], _currentCarouselIndex); - _carouselImages[_currentCarouselIndex].Opacity = 1.0; - } - - private void TeardownCarousel() - { - _carouselTimer?.Stop(); - _carouselTimer = null; - for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); - _carouselImages = Array.Empty<Image>(); - } + _zoomCts.Add(null); + } + + private async Task RotateCarouselAsync() + { + if (_carouselImages.Length < 2 || _carouselImageUrls.Count < 2) + return; + + if (Interlocked.Exchange(ref _carouselRotateInProgress, 1) == 1) + return; + + try + { + int nextIndex = (_currentCarouselIndex + 1) % _carouselImageUrls.Count; + int nextSlot = (_currentCarouselSlot + 1) % _carouselImages.Length; + int currentSlot = _currentCarouselSlot; + + await SetCarouselImageAsync(_carouselImages[nextSlot], _carouselImageUrls[nextIndex]); + + _carouselImages[currentSlot].Opacity = 0.0; + StartZoomOut(_carouselImages[nextSlot], nextSlot); + _carouselImages[nextSlot].Opacity = 1.0; + + _currentCarouselIndex = nextIndex; + _currentCarouselSlot = nextSlot; + } + finally + { + Interlocked.Exchange(ref _carouselRotateInProgress, 0); + } + } + + private void TeardownCarousel() + { + _carouselTimer?.Stop(); + _carouselTimer = null; + for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); + foreach (var image in _carouselImages) + { + if (image.Source is IDisposable disposable) + disposable.Dispose(); + + image.Source = null; + } + _carouselImageUrls.Clear(); + _carouselImages = Array.Empty<Image>(); + _currentCarouselIndex = 0; + _currentCarouselSlot = 0; + Interlocked.Exchange(ref _carouselRotateInProgress, 0); + } + + private async Task SetCarouselImageAsync(Image image, string url) + { + var nextBitmap = await LoadCarouselBitmapAsync(url); + if (nextBitmap == null) + return; + + if (image.Source is IDisposable disposable) + disposable.Dispose(); + + image.Source = nextBitmap; + } + + private static async Task<Bitmap?> LoadCarouselBitmapAsync(string url) + { + try + { + var cachedBytes = await TryGetCachedCarouselBytesAsync(url); + var bytes = cachedBytes ?? await _http.GetByteArrayAsync(url); + var resized = cachedBytes ?? TryResizeCarouselBytes(bytes) ?? bytes; + + if (cachedBytes == null) + await TryWriteCarouselCacheAsync(url, resized); + + using var ms = new MemoryStream(resized); + return new Bitmap(ms); + } + catch + { + return null; + } + } + + private static async Task<byte[]?> TryGetCachedCarouselBytesAsync(string url) + { + try + { + var path = GetCarouselCachePath(url); + if (!File.Exists(path)) + return null; + + return await File.ReadAllBytesAsync(path); + } + catch + { + return null; + } + } + + private static async Task TryWriteCarouselCacheAsync(string url, byte[] bytes) + { + try + { + Directory.CreateDirectory(CarouselCacheDir); + var path = GetCarouselCachePath(url); + var tempPath = path + ".tmp"; + await File.WriteAllBytesAsync(tempPath, bytes); + File.Move(tempPath, path, overwrite: true); + } + catch + { + // Best-effort cache only. + } + } + + private static string GetCarouselCachePath(string url) + { + var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(url))).ToLowerInvariant(); + return Path.Combine(CarouselCacheDir, $"{hash}.jpg"); + } + + private static byte[]? TryResizeCarouselBytes(byte[] bytes) + { + try + { + using var sourceBitmap = SKBitmap.Decode(bytes); + if (sourceBitmap == null) + return null; + + if (sourceBitmap.Width <= CarouselMaxWidth && + sourceBitmap.Height <= CarouselMaxHeight) + { + return null; + } + + var scale = Math.Min( + (double)CarouselMaxWidth / sourceBitmap.Width, + (double)CarouselMaxHeight / sourceBitmap.Height); + + int targetWidth = Math.Max(1, (int)Math.Round(sourceBitmap.Width * scale)); + int targetHeight = Math.Max(1, (int)Math.Round(sourceBitmap.Height * scale)); + + using var resizedBitmap = sourceBitmap.Resize( + new SKImageInfo(targetWidth, targetHeight), + SKFilterQuality.Medium); + + if (resizedBitmap == null) + return null; + + using var image = SKImage.FromBitmap(resizedBitmap); + using var data = image.Encode(SKEncodedImageFormat.Jpeg, 88); + return data?.ToArray(); + } + catch + { + return null; + } + } private void StartZoomOut(Image img, int slot) { diff --git a/Wauncher/Wauncher.csproj b/Wauncher/Wauncher.csproj index 263e830..68cfd9f 100644 --- a/Wauncher/Wauncher.csproj +++ b/Wauncher/Wauncher.csproj @@ -53,6 +53,7 @@ <PackageReference Include="Gameloop.Vdf" Version="0.6.2" /> <PackageReference Include="Refit" Version="8.0.0" /> <PackageReference Include="Refit.Newtonsoft.Json" Version="8.0.0" /> + <PackageReference Include="SharpCompress" Version="0.47.0" /> <PackageReference Include="Spectre.Console" Version="0.49.1" /> <PackageReference Include="Svg.Controls.Skia.Avalonia" Version="11.3.0.4" /> </ItemGroup> From 1a1fdbfb1ca2f1a4cb9ca1c003bb6a2f60bdf035 Mon Sep 17 00:00:00 2001 From: ways15xx <ways15xx@gmail.com> Date: Fri, 13 Mar 2026 11:55:27 -0400 Subject: [PATCH 40/51] remove known issues from readme --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index be853d1..54cba75 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,6 @@ - `Launch Options` lets you pass extra game launch flags such as `-high` or `+fps_max 300`. - `Verify Game Files` checks your installation and repairs any missing or damaged game files automatically. -## Known Issues -- Friends list does not currently show accurate `Online` or `Offline` status. - ## Build / Publish - Build: `dotnet build Wauncher/Wauncher.csproj -c Release` - Publish: `dotnet publish Wauncher/Wauncher.csproj -c Release -r win-x64 --self-contained false` From f0e626394d0adafe6bb9014b8258e01d28cfffde Mon Sep 17 00:00:00 2001 From: ways15xx <ways15xx@gmail.com> Date: Fri, 13 Mar 2026 12:37:39 -0400 Subject: [PATCH 41/51] simplify in-game friend status text --- Wauncher/Utils/Api.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Wauncher/Utils/Api.cs b/Wauncher/Utils/Api.cs index 56daddf..7943655 100644 --- a/Wauncher/Utils/Api.cs +++ b/Wauncher/Utils/Api.cs @@ -108,7 +108,19 @@ public string? CustomAvatar public string DotColor => IsOffline ? "#888888" : "#4CAF50"; public bool IsOffline => string.Equals(Status, "Offline", StringComparison.OrdinalIgnoreCase); public double AvatarOpacity => IsOffline ? 0.35 : 1.0; - public string StatusText => string.IsNullOrWhiteSpace(Status) ? "Offline" : Status; + public string StatusText + { + get + { + if (string.IsNullOrWhiteSpace(Status)) + return "Offline"; + + const string inGamePrefix = "In Game - "; + return Status.StartsWith(inGamePrefix, StringComparison.OrdinalIgnoreCase) + ? Status[inGamePrefix.Length..].Trim() + : Status; + } + } public string StatusColor => IsOffline ? "#666666" : "#999999"; } From 8d2a966794a77906ab34c135ca569b3bf8700af3 Mon Sep 17 00:00:00 2001 From: Ioannis <ioanniskiourtsidis09@gmail.com> Date: Fri, 13 Mar 2026 16:40:01 +0000 Subject: [PATCH 42/51] Fixed an issue where the Install button appeared in green and not in blue --- Wauncher/Views/MainWindow.axaml.cs | 1067 ++++++++++++++-------------- 1 file changed, 532 insertions(+), 535 deletions(-) diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index ecd62da..5e5327a 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -1,26 +1,26 @@ -using System.IO; +using System.IO; using System.Net.Http; using System.Linq; using System.Net.NetworkInformation; using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Security.Cryptography; -using System.Text.Json; -using System.Text; -using System.Text.RegularExpressions; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text.Json; +using System.Text; +using System.Text.RegularExpressions; using Avalonia.Animation; using Avalonia.Animation.Easings; using Avalonia; using Avalonia.Controls; using Avalonia.Input; -using Avalonia.Media; -using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Threading; -using SkiaSharp; -using Wauncher.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Threading; +using SkiaSharp; +using Wauncher.Utils; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Wauncher.ViewModels; using Wauncher.Views; @@ -41,18 +41,18 @@ public partial class MainWindow : Window private const double HeightOpen = 720; // ── Image carousel (center content area) ────────────────────────────────── - private Image[] _carouselImages = Array.Empty<Image>(); - private List<string> _carouselImageUrls = new(); - private DispatcherTimer? _carouselTimer; - private int _currentCarouselIndex = 0; - private int _currentCarouselSlot = 0; - private int _carouselRotateInProgress = 0; - private const int CarouselRotationIntervalSeconds = 5; - private const int CarouselMaxWidth = 1280; - private const int CarouselMaxHeight = 720; - private readonly List<System.Threading.CancellationTokenSource?> _zoomCts = new(); - private static string WauncherDirectory => - Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); + private Image[] _carouselImages = Array.Empty<Image>(); + private List<string> _carouselImageUrls = new(); + private DispatcherTimer? _carouselTimer; + private int _currentCarouselIndex = 0; + private int _currentCarouselSlot = 0; + private int _carouselRotateInProgress = 0; + private const int CarouselRotationIntervalSeconds = 5; + private const int CarouselMaxWidth = 1280; + private const int CarouselMaxHeight = 720; + private readonly List<System.Threading.CancellationTokenSource?> _zoomCts = new(); + private static string WauncherDirectory => + Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? Directory.GetCurrentDirectory(); public MainWindow() { @@ -61,9 +61,6 @@ public MainWindow() this.Loaded += (_, _) => { - var buttonColor = new SolidColorBrush(Color.Parse("#4CAF50")); - LaunchUpdateButton.Background = buttonColor; - ArrowButton.Background = buttonColor; LaunchUpdateButton.IsEnabled = true; }; @@ -84,16 +81,16 @@ public MainWindow() // Window minimize always goes to taskbar; tray hide only happens on game launch. - this.Closing += (s, e) => - { - if (_forceClose) return; - _settings = SettingsWindowViewModel.LoadGlobal(); - if (_settings.MinimizeToTray && IsGameRunning()) - { - e.Cancel = true; - Hide(); - } - }; + this.Closing += (s, e) => + { + if (_forceClose) return; + _settings = SettingsWindowViewModel.LoadGlobal(); + if (_settings.MinimizeToTray && IsGameRunning()) + { + e.Cancel = true; + Hide(); + } + }; // Ensure carousel timer is stopped whenever the window closes, // regardless of which code path triggered it. @@ -102,42 +99,42 @@ public MainWindow() // ── Image carousel (center content area) ────────────────────────────────── private static readonly HttpClient _http = new(); - private static string PatchNotesCachePath => - Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "cache", - "patchnotes.md"); - private static string CarouselCacheDir => - Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "ClassicCounter", - "Wauncher", - "cache", - "carousel"); - - private async Task SetupCarouselAsync() - { - try - { - TeardownCarousel(); + private static string PatchNotesCachePath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache", + "patchnotes.md"); + private static string CarouselCacheDir => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClassicCounter", + "Wauncher", + "cache", + "carousel"); + + private async Task SetupCarouselAsync() + { + try + { + TeardownCarousel(); var carouselContainer = this.FindControl<Grid>("CarouselContainer"); var offlinePanel = this.FindControl<Border>("CarouselOfflinePanel"); var offlineSubText = this.FindControl<TextBlock>("CarouselOfflineSubText"); if (carouselContainer == null) return; - - bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); - var urls = hasInternet - ? await LoadCarouselUrlsFromGitHubAsync() - : null; - - if (urls == null || urls.Count == 0) - { - if (offlinePanel != null) - offlinePanel.IsVisible = true; + + bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); + var urls = hasInternet + ? await LoadCarouselUrlsFromGitHubAsync() + : null; + + if (urls == null || urls.Count == 0) + { + if (offlinePanel != null) + offlinePanel.IsVisible = true; if (offlineSubText != null) { offlineSubText.Text = hasInternet @@ -147,15 +144,15 @@ private async Task SetupCarouselAsync() return; } - if (offlinePanel != null) - offlinePanel.IsVisible = false; - - _carouselImageUrls = urls; - _carouselImages = CreateCarouselImages(2); - EnsureZoomSlots(_carouselImages.Length); - - foreach (var existingImage in carouselContainer.Children.OfType<Image>().ToList()) - carouselContainer.Children.Remove(existingImage); + if (offlinePanel != null) + offlinePanel.IsVisible = false; + + _carouselImageUrls = urls; + _carouselImages = CreateCarouselImages(2); + EnsureZoomSlots(_carouselImages.Length); + + foreach (var existingImage in carouselContainer.Children.OfType<Image>().ToList()) + carouselContainer.Children.Remove(existingImage); int overlayIndex = offlinePanel != null ? carouselContainer.Children.IndexOf(offlinePanel) : -1; for (int i = 0; i < _carouselImages.Length; i++) @@ -167,55 +164,55 @@ private async Task SetupCarouselAsync() } else { - carouselContainer.Children.Add(_carouselImages[i]); - } - } - - _currentCarouselIndex = 0; - _currentCarouselSlot = 0; - await SetCarouselImageAsync(_carouselImages[_currentCarouselSlot], _carouselImageUrls[_currentCarouselIndex]); - _carouselImages[_currentCarouselSlot].Opacity = 1.0; - StartZoomOut(_carouselImages[_currentCarouselSlot], _currentCarouselSlot); - - _carouselTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CarouselRotationIntervalSeconds) }; - _carouselTimer.Tick += async (_, _) => await RotateCarouselAsync(); - _carouselTimer.Start(); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine("Carousel: " + ex.Message); - } - } - - private async Task<List<string>?> LoadCarouselUrlsFromGitHubAsync() - { - try - { - var json = await Api.GitHub.GetCarouselAssetsWauncher(); - var assets = JsonConvert.DeserializeObject<List<GitHubAssetEntry>>(json); + carouselContainer.Children.Add(_carouselImages[i]); + } + } + + _currentCarouselIndex = 0; + _currentCarouselSlot = 0; + await SetCarouselImageAsync(_carouselImages[_currentCarouselSlot], _carouselImageUrls[_currentCarouselIndex]); + _carouselImages[_currentCarouselSlot].Opacity = 1.0; + StartZoomOut(_carouselImages[_currentCarouselSlot], _currentCarouselSlot); + + _carouselTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CarouselRotationIntervalSeconds) }; + _carouselTimer.Tick += async (_, _) => await RotateCarouselAsync(); + _carouselTimer.Start(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine("Carousel: " + ex.Message); + } + } + + private async Task<List<string>?> LoadCarouselUrlsFromGitHubAsync() + { + try + { + var json = await Api.GitHub.GetCarouselAssetsWauncher(); + var assets = JsonConvert.DeserializeObject<List<GitHubAssetEntry>>(json); if (assets == null || assets.Count == 0) return null; - var urls = assets - .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) - .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || - a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || + var urls = assets + .Where(a => string.Equals(a.Type, "file", StringComparison.OrdinalIgnoreCase)) + .Where(a => !string.IsNullOrWhiteSpace(a.Name) && a.Name.StartsWith("carousel_", StringComparison.OrdinalIgnoreCase)) + .Where(a => a.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || + a.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || a.Name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || a.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)) - .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) - .OrderBy(a => GetCarouselSortIndex(a.Name)) - .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) - .Select(a => a.DownloadUrl!) - .ToList(); - - if (urls.Count == 0) - return null; - - return urls; - } - catch { return null; } - } + .Where(a => !string.IsNullOrWhiteSpace(a.DownloadUrl)) + .OrderBy(a => GetCarouselSortIndex(a.Name)) + .ThenBy(a => a.Name, StringComparer.OrdinalIgnoreCase) + .Select(a => a.DownloadUrl!) + .ToList(); + + if (urls.Count == 0) + return null; + + return urls; + } + catch { return null; } + } private static int GetCarouselSortIndex(string name) { @@ -237,16 +234,16 @@ private sealed class GitHubAssetEntry public string? DownloadUrl { get; set; } } - private static Image[] CreateCarouselImages(int count) - { - var images = new Image[count]; - for (int i = 0; i < count; i++) - { - images[i] = new Image - { - Stretch = Stretch.UniformToFill, - Opacity = 0.0, - Transitions = new Transitions + private static Image[] CreateCarouselImages(int count) + { + var images = new Image[count]; + for (int i = 0; i < count; i++) + { + images[i] = new Image + { + Stretch = Stretch.UniformToFill, + Opacity = 0.0, + Transitions = new Transitions { new DoubleTransition { @@ -264,164 +261,164 @@ private static Image[] CreateCarouselImages(int count) private void EnsureZoomSlots(int count) { while (_zoomCts.Count < count) - _zoomCts.Add(null); - } - - private async Task RotateCarouselAsync() - { - if (_carouselImages.Length < 2 || _carouselImageUrls.Count < 2) - return; - - if (Interlocked.Exchange(ref _carouselRotateInProgress, 1) == 1) - return; - - try - { - int nextIndex = (_currentCarouselIndex + 1) % _carouselImageUrls.Count; - int nextSlot = (_currentCarouselSlot + 1) % _carouselImages.Length; - int currentSlot = _currentCarouselSlot; - - await SetCarouselImageAsync(_carouselImages[nextSlot], _carouselImageUrls[nextIndex]); - - _carouselImages[currentSlot].Opacity = 0.0; - StartZoomOut(_carouselImages[nextSlot], nextSlot); - _carouselImages[nextSlot].Opacity = 1.0; - - _currentCarouselIndex = nextIndex; - _currentCarouselSlot = nextSlot; - } - finally - { - Interlocked.Exchange(ref _carouselRotateInProgress, 0); - } - } - - private void TeardownCarousel() - { - _carouselTimer?.Stop(); - _carouselTimer = null; - for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); - foreach (var image in _carouselImages) - { - if (image.Source is IDisposable disposable) - disposable.Dispose(); - - image.Source = null; - } - _carouselImageUrls.Clear(); - _carouselImages = Array.Empty<Image>(); - _currentCarouselIndex = 0; - _currentCarouselSlot = 0; - Interlocked.Exchange(ref _carouselRotateInProgress, 0); - } - - private async Task SetCarouselImageAsync(Image image, string url) - { - var nextBitmap = await LoadCarouselBitmapAsync(url); - if (nextBitmap == null) - return; - - if (image.Source is IDisposable disposable) - disposable.Dispose(); - - image.Source = nextBitmap; - } - - private static async Task<Bitmap?> LoadCarouselBitmapAsync(string url) - { - try - { - var cachedBytes = await TryGetCachedCarouselBytesAsync(url); - var bytes = cachedBytes ?? await _http.GetByteArrayAsync(url); - var resized = cachedBytes ?? TryResizeCarouselBytes(bytes) ?? bytes; - - if (cachedBytes == null) - await TryWriteCarouselCacheAsync(url, resized); - - using var ms = new MemoryStream(resized); - return new Bitmap(ms); - } - catch - { - return null; - } - } - - private static async Task<byte[]?> TryGetCachedCarouselBytesAsync(string url) - { - try - { - var path = GetCarouselCachePath(url); - if (!File.Exists(path)) - return null; - - return await File.ReadAllBytesAsync(path); - } - catch - { - return null; - } - } - - private static async Task TryWriteCarouselCacheAsync(string url, byte[] bytes) - { - try - { - Directory.CreateDirectory(CarouselCacheDir); - var path = GetCarouselCachePath(url); - var tempPath = path + ".tmp"; - await File.WriteAllBytesAsync(tempPath, bytes); - File.Move(tempPath, path, overwrite: true); - } - catch - { - // Best-effort cache only. - } - } - - private static string GetCarouselCachePath(string url) - { - var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(url))).ToLowerInvariant(); - return Path.Combine(CarouselCacheDir, $"{hash}.jpg"); - } - - private static byte[]? TryResizeCarouselBytes(byte[] bytes) - { - try - { - using var sourceBitmap = SKBitmap.Decode(bytes); - if (sourceBitmap == null) - return null; - - if (sourceBitmap.Width <= CarouselMaxWidth && - sourceBitmap.Height <= CarouselMaxHeight) - { - return null; - } - - var scale = Math.Min( - (double)CarouselMaxWidth / sourceBitmap.Width, - (double)CarouselMaxHeight / sourceBitmap.Height); - - int targetWidth = Math.Max(1, (int)Math.Round(sourceBitmap.Width * scale)); - int targetHeight = Math.Max(1, (int)Math.Round(sourceBitmap.Height * scale)); - - using var resizedBitmap = sourceBitmap.Resize( - new SKImageInfo(targetWidth, targetHeight), - SKFilterQuality.Medium); - - if (resizedBitmap == null) - return null; - - using var image = SKImage.FromBitmap(resizedBitmap); - using var data = image.Encode(SKEncodedImageFormat.Jpeg, 88); - return data?.ToArray(); - } - catch - { - return null; - } - } + _zoomCts.Add(null); + } + + private async Task RotateCarouselAsync() + { + if (_carouselImages.Length < 2 || _carouselImageUrls.Count < 2) + return; + + if (Interlocked.Exchange(ref _carouselRotateInProgress, 1) == 1) + return; + + try + { + int nextIndex = (_currentCarouselIndex + 1) % _carouselImageUrls.Count; + int nextSlot = (_currentCarouselSlot + 1) % _carouselImages.Length; + int currentSlot = _currentCarouselSlot; + + await SetCarouselImageAsync(_carouselImages[nextSlot], _carouselImageUrls[nextIndex]); + + _carouselImages[currentSlot].Opacity = 0.0; + StartZoomOut(_carouselImages[nextSlot], nextSlot); + _carouselImages[nextSlot].Opacity = 1.0; + + _currentCarouselIndex = nextIndex; + _currentCarouselSlot = nextSlot; + } + finally + { + Interlocked.Exchange(ref _carouselRotateInProgress, 0); + } + } + + private void TeardownCarousel() + { + _carouselTimer?.Stop(); + _carouselTimer = null; + for (int i = 0; i < _zoomCts.Count; i++) StopZoom(i); + foreach (var image in _carouselImages) + { + if (image.Source is IDisposable disposable) + disposable.Dispose(); + + image.Source = null; + } + _carouselImageUrls.Clear(); + _carouselImages = Array.Empty<Image>(); + _currentCarouselIndex = 0; + _currentCarouselSlot = 0; + Interlocked.Exchange(ref _carouselRotateInProgress, 0); + } + + private async Task SetCarouselImageAsync(Image image, string url) + { + var nextBitmap = await LoadCarouselBitmapAsync(url); + if (nextBitmap == null) + return; + + if (image.Source is IDisposable disposable) + disposable.Dispose(); + + image.Source = nextBitmap; + } + + private static async Task<Bitmap?> LoadCarouselBitmapAsync(string url) + { + try + { + var cachedBytes = await TryGetCachedCarouselBytesAsync(url); + var bytes = cachedBytes ?? await _http.GetByteArrayAsync(url); + var resized = cachedBytes ?? TryResizeCarouselBytes(bytes) ?? bytes; + + if (cachedBytes == null) + await TryWriteCarouselCacheAsync(url, resized); + + using var ms = new MemoryStream(resized); + return new Bitmap(ms); + } + catch + { + return null; + } + } + + private static async Task<byte[]?> TryGetCachedCarouselBytesAsync(string url) + { + try + { + var path = GetCarouselCachePath(url); + if (!File.Exists(path)) + return null; + + return await File.ReadAllBytesAsync(path); + } + catch + { + return null; + } + } + + private static async Task TryWriteCarouselCacheAsync(string url, byte[] bytes) + { + try + { + Directory.CreateDirectory(CarouselCacheDir); + var path = GetCarouselCachePath(url); + var tempPath = path + ".tmp"; + await File.WriteAllBytesAsync(tempPath, bytes); + File.Move(tempPath, path, overwrite: true); + } + catch + { + // Best-effort cache only. + } + } + + private static string GetCarouselCachePath(string url) + { + var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(url))).ToLowerInvariant(); + return Path.Combine(CarouselCacheDir, $"{hash}.jpg"); + } + + private static byte[]? TryResizeCarouselBytes(byte[] bytes) + { + try + { + using var sourceBitmap = SKBitmap.Decode(bytes); + if (sourceBitmap == null) + return null; + + if (sourceBitmap.Width <= CarouselMaxWidth && + sourceBitmap.Height <= CarouselMaxHeight) + { + return null; + } + + var scale = Math.Min( + (double)CarouselMaxWidth / sourceBitmap.Width, + (double)CarouselMaxHeight / sourceBitmap.Height); + + int targetWidth = Math.Max(1, (int)Math.Round(sourceBitmap.Width * scale)); + int targetHeight = Math.Max(1, (int)Math.Round(sourceBitmap.Height * scale)); + + using var resizedBitmap = sourceBitmap.Resize( + new SKImageInfo(targetWidth, targetHeight), + SKFilterQuality.Medium); + + if (resizedBitmap == null) + return null; + + using var image = SKImage.FromBitmap(resizedBitmap); + using var data = image.Encode(SKEncodedImageFormat.Jpeg, 88); + return data?.ToArray(); + } + catch + { + return null; + } + } private void StartZoomOut(Image img, int slot) { @@ -573,23 +570,23 @@ private async Task LaunchGameAsync() { Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); } - finally - { - if (vm != null) vm.GameStatus = "Not Running"; - - if (!_forceClose && _settings.MinimizeToTray && !IsVisible) - { - Dispatcher.UIThread.Post(() => - { - Show(); - WindowState = WindowState.Normal; - Activate(); - }); - } - - Interlocked.Exchange(ref _launchInProgress, 0); - } - } + finally + { + if (vm != null) vm.GameStatus = "Not Running"; + + if (!_forceClose && _settings.MinimizeToTray && !IsVisible) + { + Dispatcher.UIThread.Post(() => + { + Show(); + WindowState = WindowState.Normal; + Activate(); + }); + } + + Interlocked.Exchange(ref _launchInProgress, 0); + } + } // ── Window chrome ─────────────────────────────────────────── private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) @@ -612,22 +609,22 @@ private void CloseButton_Click(object? sender, Avalonia.Interactivity.RoutedEven Close(); } - public void ForceQuit() - { - _forceClose = true; - TeardownCarousel(); - Close(); - } - - private bool IsGameRunning() - { - return DataContext is MainWindowViewModel vm && - string.Equals(vm.GameStatus, "Running", StringComparison.Ordinal); - } - - private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var dir = WauncherDirectory; + public void ForceQuit() + { + _forceClose = true; + TeardownCarousel(); + Close(); + } + + private bool IsGameRunning() + { + return DataContext is MainWindowViewModel vm && + string.Equals(vm.GameStatus, "Running", StringComparison.Ordinal); + } + + private void OpenGameFolder_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var dir = WauncherDirectory; System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = dir, @@ -873,40 +870,40 @@ private async Task LoadPatchNotesAsync() { try { - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.IsVisible = false; - }); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.IsVisible = false; + }); if (DataContext is MainWindowViewModel vm && vm.IsOfflineMode) { Dispatcher.UIThread.Post(() => { var cachedItems = LoadCachedPatchNotes(); - if (cachedItems.Count > 0) - { - PatchNotesList.ItemsSource = cachedItems; - } - else - { - PatchNotesList.ItemsSource = new List<ViewModels.PatchNoteItem>(); - } - - PatchNotesVersion.IsVisible = false; - PatchNotesScroll.Offset = new Vector(0, 0); - }); + if (cachedItems.Count > 0) + { + PatchNotesList.ItemsSource = cachedItems; + } + else + { + PatchNotesList.ItemsSource = new List<ViewModels.PatchNoteItem>(); + } + + PatchNotesVersion.IsVisible = false; + PatchNotesScroll.Offset = new Vector(0, 0); + }); return; } var md = await Api.GitHub.GetPatchNotesWauncher(); var items = ParsePatchNotes(md); SavePatchNotesCache(md); - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.IsVisible = false; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.IsVisible = false; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); } catch { @@ -916,12 +913,12 @@ private async Task LoadPatchNotesAsync() items = BuildFallbackPatchNotes(); } - Dispatcher.UIThread.Post(() => - { - PatchNotesVersion.IsVisible = false; - PatchNotesList.ItemsSource = items; - PatchNotesScroll.Offset = new Vector(0, 0); - }); + Dispatcher.UIThread.Post(() => + { + PatchNotesVersion.IsVisible = false; + PatchNotesList.ItemsSource = items; + PatchNotesScroll.Offset = new Vector(0, 0); + }); } } @@ -961,155 +958,155 @@ private static void SavePatchNotesCache(string markdown) } } - private static List<ViewModels.PatchNoteItem> BuildFallbackPatchNotes() - { - return new List<ViewModels.PatchNoteItem> - { - new() { Text = "Anniversary Update", IsMajorHeader = true }, - new() { Text = "March 12, 2026", IsDateHeader = true }, - new() { Text = "What''s Changed", IsHeader = true }, - new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, - new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, - new() { Text = "Bug fixes and security improvements.", IsBullet = true }, - }; - } - - private static List<ViewModels.PatchNoteItem> ParsePatchNotes(string markdown) - { - var items = new List<ViewModels.PatchNoteItem>(); - bool lastWasMajorHeader = false; - - foreach (var raw in markdown.Split('\n')) - { - var line = raw.TrimEnd(); - if (string.IsNullOrWhiteSpace(line)) continue; - - line = line.Trim(); - line = line.Replace("**", "").Replace("__", ""); - line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); - line = Regex.Replace(line, @"`([^`]*)`", "$1"); - - if (line.StartsWith("# ")) - { - var headerText = line.TrimStart('#', ' '); - var (title, dateText) = SplitPatchTitleAndDate(headerText); - - items.Add(new ViewModels.PatchNoteItem - { - Text = title, - IsMajorHeader = true - }); - - if (!string.IsNullOrWhiteSpace(dateText)) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = dateText, - IsDateHeader = true - }); - } - - lastWasMajorHeader = true; - } - else if (lastWasMajorHeader && TryParsePatchDateLine(line, out var parsedDate)) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = parsedDate, - IsDateHeader = true - }); - lastWasMajorHeader = false; - } - else if (line.StartsWith("## ") || line.StartsWith("### ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', ' '), - IsHeader = true - }); - lastWasMajorHeader = false; - } - else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Substring(2).Trim(), - IsBullet = true - }); - lastWasMajorHeader = false; - } - else if (Regex.IsMatch(line, @"^\d+\.\s+")) - { - var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); - items.Add(new ViewModels.PatchNoteItem - { - Text = bulletText, - IsBullet = true - }); - lastWasMajorHeader = false; - } - else if (line.StartsWith("**") && line.EndsWith("**")) - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.Trim('*', ' '), - IsHeader = true - }); - lastWasMajorHeader = false; - } - else - { - items.Add(new ViewModels.PatchNoteItem - { - Text = line.TrimStart('#', '*', '-', ' '), - IsBullet = true - }); - lastWasMajorHeader = false; - } - } - - return items; - } - - private static (string Title, string DateText) SplitPatchTitleAndDate(string headerText) - { - var match = Regex.Match( - headerText, - @"^(?<title>.+?)\s+(?:-|–|—)\s+(?<date>(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},\s+\d{4})$", - RegexOptions.IgnoreCase); - - if (!match.Success) - return (headerText, string.Empty); - - return (match.Groups["title"].Value.Trim(), match.Groups["date"].Value.Trim()); - } - - private static bool TryParsePatchDateLine(string line, out string dateText) - { - dateText = string.Empty; - var trimmed = line.Trim(); - - if (trimmed.StartsWith("Date:", StringComparison.OrdinalIgnoreCase)) - { - dateText = trimmed[5..].Trim(); - return !string.IsNullOrWhiteSpace(dateText); - } - - if (Regex.IsMatch(trimmed, @"^(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},\s+\d{4}$", RegexOptions.IgnoreCase)) - { - dateText = trimmed; - return true; - } - - if (Regex.IsMatch(trimmed, @"^\d{1,2}/\d{1,2}/\d{4}$", RegexOptions.IgnoreCase)) - { - dateText = trimmed; - return true; - } - - return false; - } - + private static List<ViewModels.PatchNoteItem> BuildFallbackPatchNotes() + { + return new List<ViewModels.PatchNoteItem> + { + new() { Text = "Anniversary Update", IsMajorHeader = true }, + new() { Text = "March 12, 2026", IsDateHeader = true }, + new() { Text = "What''s Changed", IsHeader = true }, + new() { Text = "Donors now permanently get an extra drop at the end of each match.", IsBullet = true }, + new() { Text = "NOVAGANG Collection drops have been reverted back to normal rates.", IsBullet = true }, + new() { Text = "Bug fixes and security improvements.", IsBullet = true }, + }; + } + + private static List<ViewModels.PatchNoteItem> ParsePatchNotes(string markdown) + { + var items = new List<ViewModels.PatchNoteItem>(); + bool lastWasMajorHeader = false; + + foreach (var raw in markdown.Split('\n')) + { + var line = raw.TrimEnd(); + if (string.IsNullOrWhiteSpace(line)) continue; + + line = line.Trim(); + line = line.Replace("**", "").Replace("__", ""); + line = Regex.Replace(line, @"\[(.*?)\]\((.*?)\)", "$1"); + line = Regex.Replace(line, @"`([^`]*)`", "$1"); + + if (line.StartsWith("# ")) + { + var headerText = line.TrimStart('#', ' '); + var (title, dateText) = SplitPatchTitleAndDate(headerText); + + items.Add(new ViewModels.PatchNoteItem + { + Text = title, + IsMajorHeader = true + }); + + if (!string.IsNullOrWhiteSpace(dateText)) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = dateText, + IsDateHeader = true + }); + } + + lastWasMajorHeader = true; + } + else if (lastWasMajorHeader && TryParsePatchDateLine(line, out var parsedDate)) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = parsedDate, + IsDateHeader = true + }); + lastWasMajorHeader = false; + } + else if (line.StartsWith("## ") || line.StartsWith("### ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', ' '), + IsHeader = true + }); + lastWasMajorHeader = false; + } + else if (line.StartsWith("* ") || line.StartsWith("- ") || line.StartsWith("• ")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Substring(2).Trim(), + IsBullet = true + }); + lastWasMajorHeader = false; + } + else if (Regex.IsMatch(line, @"^\d+\.\s+")) + { + var bulletText = Regex.Replace(line, @"^\d+\.\s+", string.Empty).Trim(); + items.Add(new ViewModels.PatchNoteItem + { + Text = bulletText, + IsBullet = true + }); + lastWasMajorHeader = false; + } + else if (line.StartsWith("**") && line.EndsWith("**")) + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.Trim('*', ' '), + IsHeader = true + }); + lastWasMajorHeader = false; + } + else + { + items.Add(new ViewModels.PatchNoteItem + { + Text = line.TrimStart('#', '*', '-', ' '), + IsBullet = true + }); + lastWasMajorHeader = false; + } + } + + return items; + } + + private static (string Title, string DateText) SplitPatchTitleAndDate(string headerText) + { + var match = Regex.Match( + headerText, + @"^(?<title>.+?)\s+(?:-|–|—)\s+(?<date>(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},\s+\d{4})$", + RegexOptions.IgnoreCase); + + if (!match.Success) + return (headerText, string.Empty); + + return (match.Groups["title"].Value.Trim(), match.Groups["date"].Value.Trim()); + } + + private static bool TryParsePatchDateLine(string line, out string dateText) + { + dateText = string.Empty; + var trimmed = line.Trim(); + + if (trimmed.StartsWith("Date:", StringComparison.OrdinalIgnoreCase)) + { + dateText = trimmed[5..].Trim(); + return !string.IsNullOrWhiteSpace(dateText); + } + + if (Regex.IsMatch(trimmed, @"^(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},\s+\d{4}$", RegexOptions.IgnoreCase)) + { + dateText = trimmed; + return true; + } + + if (Regex.IsMatch(trimmed, @"^\d{1,2}/\d{1,2}/\d{4}$", RegexOptions.IgnoreCase)) + { + dateText = trimmed; + return true; + } + + return false; + } + private sealed class GitHubRelease { [JsonProperty("tag_name")] @@ -1602,10 +1599,10 @@ private void PatchNotesTab_Click(object? sender, Avalonia.Interactivity.RoutedEv PatchNotesScroll.Offset = new Vector(0, 0); } - private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (sender is not MenuItem { Tag: FriendInfo friend }) - return; + private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: FriendInfo friend }) + return; var profileId = ResolveProfileSteamId(friend.SteamId); if (string.IsNullOrWhiteSpace(profileId)) @@ -1622,37 +1619,37 @@ private void ViewFriendProfile_Click(object? sender, Avalonia.Interactivity.Rout catch { // Best-effort open. - } - } - - private async void JoinFriendServer_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (sender is not MenuItem { Tag: FriendInfo friend }) - return; - - if (DataContext is not MainWindowViewModel vm) - return; - - if (string.IsNullOrWhiteSpace(friend.QuickJoinIpPort)) - return; - - var matchedServer = vm.Servers.FirstOrDefault(s => - !s.IsNone && - string.Equals(s.IpPort, friend.QuickJoinIpPort, StringComparison.OrdinalIgnoreCase)); - - if (matchedServer == null) - { - Wauncher.Utils.ConsoleManager.ShowError("Couldn't match that friend to a server in your server list."); - return; - } - - vm.SelectedServer = matchedServer; - await LaunchGameAsync(); - } - - private static string ResolveProfileSteamId(string? steamId) - { - if (string.IsNullOrWhiteSpace(steamId)) + } + } + + private async void JoinFriendServer_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: FriendInfo friend }) + return; + + if (DataContext is not MainWindowViewModel vm) + return; + + if (string.IsNullOrWhiteSpace(friend.QuickJoinIpPort)) + return; + + var matchedServer = vm.Servers.FirstOrDefault(s => + !s.IsNone && + string.Equals(s.IpPort, friend.QuickJoinIpPort, StringComparison.OrdinalIgnoreCase)); + + if (matchedServer == null) + { + Wauncher.Utils.ConsoleManager.ShowError("Couldn't match that friend to a server in your server list."); + return; + } + + vm.SelectedServer = matchedServer; + await LaunchGameAsync(); + } + + private static string ResolveProfileSteamId(string? steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) return string.Empty; var value = steamId.Trim(); From 0a8ede0247d24872c789983f00fb58b9903012cb Mon Sep 17 00:00:00 2001 From: ways15xx <ways15xx@gmail.com> Date: Fri, 13 Mar 2026 13:51:43 -0400 Subject: [PATCH 43/51] set wauncher branding and release version --- Wauncher/Utils/Discord.cs | 2 +- Wauncher/Wauncher.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Wauncher/Utils/Discord.cs b/Wauncher/Utils/Discord.cs index c1c612a..a6f162b 100644 --- a/Wauncher/Utils/Discord.cs +++ b/Wauncher/Utils/Discord.cs @@ -26,7 +26,7 @@ public static void Init() if (!_client.Initialize()) return; - SetDetails("In Launcher"); + SetDetails("In Wauncher"); SetTimestamp(DateTime.UtcNow); SetLargeArtwork("icon"); diff --git a/Wauncher/Wauncher.csproj b/Wauncher/Wauncher.csproj index 68cfd9f..1b7b752 100644 --- a/Wauncher/Wauncher.csproj +++ b/Wauncher/Wauncher.csproj @@ -15,7 +15,7 @@ <PublishSingleFile>true</PublishSingleFile> <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> <SelfContained>false</SelfContained> - <Version>3.0.5</Version> + <Version>3.0.0</Version> <AssemblyVersion>$(Version)</AssemblyVersion> <FileVersion>$(Version)</FileVersion> <AssemblyName>wauncher</AssemblyName> From fbb853bc2781f93a87bb31734b131ccb2334459d Mon Sep 17 00:00:00 2001 From: ways15xx <ways15xx@gmail.com> Date: Fri, 13 Mar 2026 13:54:36 -0400 Subject: [PATCH 44/51] sync info window version display --- Wauncher/ViewModels/InfoWindowViewModel.cs | 5 +++-- Wauncher/Views/InfoWindow.axaml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Wauncher/ViewModels/InfoWindowViewModel.cs b/Wauncher/ViewModels/InfoWindowViewModel.cs index be05980..1c77c7e 100644 --- a/Wauncher/ViewModels/InfoWindowViewModel.cs +++ b/Wauncher/ViewModels/InfoWindowViewModel.cs @@ -2,11 +2,12 @@ using CommunityToolkit.Mvvm.Input; using System.Diagnostics; using System.Runtime.InteropServices; - namespace Wauncher.ViewModels { public partial class InfoWindowViewModel : ViewModelBase { + public string DisplayVersion => $"Version {Wauncher.Utils.Version.Current}"; + [RelayCommand] private void OpenUrl(string? url) { @@ -32,4 +33,4 @@ private void OpenUrl(string? url) } } } -} \ No newline at end of file +} diff --git a/Wauncher/Views/InfoWindow.axaml b/Wauncher/Views/InfoWindow.axaml index 4532358..7112602 100644 --- a/Wauncher/Views/InfoWindow.axaml +++ b/Wauncher/Views/InfoWindow.axaml @@ -132,7 +132,7 @@ VerticalAlignment="Center" FontSize="12" Foreground="{DynamicResource AppSectionLabel}" - Text="Version 3.0.5" /> + Text="{Binding DisplayVersion}" /> </Grid> <Rectangle Grid.Row="1" Fill="{DynamicResource AppDivider}" Margin="24,0" /> From b7e5995af0b76dd6fbd2d870610a90e7bf09e09a Mon Sep 17 00:00:00 2001 From: ways15xx <ways15xx@gmail.com> Date: Fri, 13 Mar 2026 14:05:59 -0400 Subject: [PATCH 45/51] queue protocol server launches through wauncher --- Wauncher/Utils/Argument.cs | 33 +++++++++++- Wauncher/Views/MainWindow.axaml.cs | 83 +++++++++++++++++++++--------- 2 files changed, 91 insertions(+), 25 deletions(-) diff --git a/Wauncher/Utils/Argument.cs b/Wauncher/Utils/Argument.cs index e64e917..38c0f8f 100644 --- a/Wauncher/Utils/Argument.cs +++ b/Wauncher/Utils/Argument.cs @@ -3,6 +3,7 @@ namespace Wauncher.Utils public static class Argument { private static readonly List<string> _additionalArguments = new(); + private static bool _protocolConnectConsumed; public static void AddArgument(string argument) { @@ -19,6 +20,33 @@ public static bool HasProtocolCommand() => Environment.GetCommandLineArgs().Any(arg => arg.StartsWith("cc://", StringComparison.OrdinalIgnoreCase)); + public static string? GetProtocolConnectTarget() + { + foreach (string arg in Environment.GetCommandLineArgs()) + { + if (!arg.StartsWith("cc://", StringComparison.OrdinalIgnoreCase)) + continue; + + string protocolArgument = arg.Replace("cc://", "", StringComparison.OrdinalIgnoreCase); + string[] protocolArguments = protocolArgument.Split('/'); + if (protocolArguments.Length < 2) + continue; + + if (!string.Equals(protocolArguments[0], "connect", StringComparison.OrdinalIgnoreCase)) + continue; + + var target = Uri.UnescapeDataString(protocolArguments[1]).Trim(); + return string.IsNullOrWhiteSpace(target) ? null : target; + } + + return null; + } + + public static void ConsumeProtocolConnectTarget() + { + _protocolConnectConsumed = true; + } + public static List<string> GenerateGameArguments() { IEnumerable<string> launcherArguments = Environment.GetCommandLineArgs(); @@ -37,8 +65,11 @@ public static List<string> GenerateGameArguments() switch (protocolArguments[0]) { case "connect": + if (_protocolConnectConsumed) + break; + gameArguments.Add("+connect"); - gameArguments.Add(protocolArguments[1]); + gameArguments.Add(Uri.UnescapeDataString(protocolArguments[1])); break; } } diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index 5e5327a..9cacb34 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -648,21 +648,22 @@ private void VerifyGameFiles_Click(object? sender, Avalonia.Interactivity.Routed // ── Update ───────────────────────────────────────────────────── private CancellationTokenSource? _updateCts; private Patches? _cachedPatches; - private bool _selfUpdateAvailable; - private string _selfUpdateDownloadUrl = string.Empty; - private string _selfUpdateVersion = string.Empty; - private int _autoSelfUpdateTriggered; + private bool _selfUpdateAvailable; + private string _selfUpdateDownloadUrl = string.Empty; + private string _selfUpdateVersion = string.Empty; + private int _autoSelfUpdateTriggered; + private int _protocolLaunchTriggered; private bool _forceValidateAllOnce; /// <summary> /// Called on window open. If csgo.exe is missing, triggers a full CDN install. /// Otherwise runs the normal patch update check. /// </summary> - private async Task StartupAsync() - { - // Yield to let Avalonia finish its initial layout/styling pass - // (Loaded sets the button disabled/gray; we need that to settle before overriding) - await Task.Delay(50); + private async Task StartupAsync() + { + // Yield to let Avalonia finish its initial layout/styling pass + // (Loaded sets the button disabled/gray; we need that to settle before overriding) + await Task.Delay(50); LaunchUpdateButton.IsEnabled = true; @@ -670,21 +671,24 @@ private async Task StartupAsync() if (DataContext is not MainWindowViewModel vm) return; - if (!File.Exists(csgoExe)) - { - vm.IsNeedingInstall = true; - var blue = new SolidColorBrush(Color.Parse("#2196F3")); - LaunchUpdateButton.Background = blue; + if (!File.Exists(csgoExe)) + { + vm.IsNeedingInstall = true; + var blue = new SolidColorBrush(Color.Parse("#2196F3")); + LaunchUpdateButton.Background = blue; ArrowButton.Background = blue; LaunchButtonGlow.BoxShadow = BoxShadows.Parse("0 0 18 2 #552196F3"); LaunchUpdateButton.IsEnabled = true; - return; - } - - if (vm?.IsOfflineMode == true) - { - vm.IsNeedingInstall = false; - vm.UpdateAvailable = false; + return; + } + + if (await TryHandleProtocolLaunchAsync(vm)) + return; + + if (vm?.IsOfflineMode == true) + { + vm.IsNeedingInstall = false; + vm.UpdateAvailable = false; var green = new SolidColorBrush(Color.Parse("#4CAF50")); LaunchUpdateButton.Background = green; ArrowButton.Background = green; @@ -706,9 +710,40 @@ private async Task StartupAsync() LaunchUpdateButton.IsEnabled = true; return; } - - await CheckForUpdatesAsync(); - } + + await CheckForUpdatesAsync(); + } + + private async Task<bool> TryHandleProtocolLaunchAsync(MainWindowViewModel vm) + { + var protocolTarget = Argument.GetProtocolConnectTarget(); + if (string.IsNullOrWhiteSpace(protocolTarget)) + return false; + + if (Interlocked.Exchange(ref _protocolLaunchTriggered, 1) == 1) + return true; + + try + { + var matchedServer = vm.Servers.FirstOrDefault(s => + !s.IsNone && + string.Equals(s.IpPort, protocolTarget, StringComparison.OrdinalIgnoreCase)); + + if (matchedServer != null) + { + vm.SelectedServer = matchedServer; + Argument.ConsumeProtocolConnectTarget(); + } + + await LaunchGameAsync(); + return true; + } + catch + { + Interlocked.Exchange(ref _protocolLaunchTriggered, 0); + throw; + } + } private async Task InstallGameFromCdnAsync() { From 7c057d098cb5211a4a5dfe16c08ef6f38dabbc8c Mon Sep 17 00:00:00 2001 From: ways15xx <ways15xx@gmail.com> Date: Fri, 13 Mar 2026 14:11:15 -0400 Subject: [PATCH 46/51] fix protocol launches using wrong game directory --- Wauncher/Program.cs | 4 ++++ Wauncher/Utils/Game.cs | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Wauncher/Program.cs b/Wauncher/Program.cs index 01b0298..1e757c4 100644 --- a/Wauncher/Program.cs +++ b/Wauncher/Program.cs @@ -15,6 +15,10 @@ public static void Main(string[] args) { try { + var exeDirectory = Path.GetDirectoryName(Services.GetExePath()); + if (!string.IsNullOrWhiteSpace(exeDirectory) && Directory.Exists(exeDirectory)) + Directory.SetCurrentDirectory(exeDirectory); + if (OnStartup(args) == false) { Environment.Exit(0); diff --git a/Wauncher/Utils/Game.cs b/Wauncher/Utils/Game.cs index 4002735..898e43d 100644 --- a/Wauncher/Utils/Game.cs +++ b/Wauncher/Utils/Game.cs @@ -22,10 +22,10 @@ public static async Task<bool> Launch() if (arguments.Count > 0) Terminal.Print($"Arguments: {string.Join(" ", arguments)}"); var settings = ViewModels.SettingsWindowViewModel.LoadGlobal(); - string directory = Directory.GetCurrentDirectory(); + string directory = Path.GetDirectoryName(Services.GetExePath()) ?? Directory.GetCurrentDirectory(); Terminal.Print($"Directory: {directory}"); - string gameStatePath = $"{directory}/csgo/cfg/gamestate_integration_cc.cfg"; + string gameStatePath = Path.Combine(directory, "csgo", "cfg", "gamestate_integration_cc.cfg"); if (settings.DiscordRpc) { @@ -76,8 +76,9 @@ public static async Task<bool> Launch() _process = new Process(); string gameExe = "csgo.exe"; - _process.StartInfo.FileName = $"{directory}\\{gameExe}"; + _process.StartInfo.FileName = Path.Combine(directory, gameExe); _process.StartInfo.Arguments = string.Join(" ", arguments); + _process.StartInfo.WorkingDirectory = directory; if (!File.Exists(_process.StartInfo.FileName)) { From 9e3933cb06585882abe4bcfa1ac1344f28d88a73 Mon Sep 17 00:00:00 2001 From: ways15xx <ways15xx@gmail.com> Date: Fri, 13 Mar 2026 14:17:01 -0400 Subject: [PATCH 47/51] remove alpha banner from main window --- Wauncher/Views/MainWindow.axaml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/Wauncher/Views/MainWindow.axaml b/Wauncher/Views/MainWindow.axaml index 1f3a51a..66f9428 100644 --- a/Wauncher/Views/MainWindow.axaml +++ b/Wauncher/Views/MainWindow.axaml @@ -371,22 +371,10 @@ VerticalAlignment="Top" Background="{DynamicResource AppTitleBarBrush}" PointerPressed="TitleBar_PointerPressed"> - <TextBlock - HorizontalAlignment="Left" - VerticalAlignment="Center" - Margin="12,0,0,0" - FontSize="11" - FontStyle="Italic" - Foreground="{DynamicResource AppPrimaryText}" - Text="ALPHA TESTING NOT FINAL"> - <TextBlock.Effect> - <DropShadowEffect BlurRadius="6" Color="Black" OffsetX="0" OffsetY="1" Opacity="0.8" /> - </TextBlock.Effect> - </TextBlock> - <Image - HorizontalAlignment="Center" - VerticalAlignment="Center" - Height="22" + <Image + HorizontalAlignment="Center" + VerticalAlignment="Center" + Height="22" Source="avares://Wauncher/Assets/logo.png"> <Image.Effect> <DropShadowEffect From 927644ad05c2d62bca175b9217d7aeb6f0e611cb Mon Sep 17 00:00:00 2001 From: eddies <zombie@z.org> Date: Tue, 17 Mar 2026 16:25:22 -0400 Subject: [PATCH 48/51] Create CONTRIBUTORS.md --- Wauncher/CONTRIBUTORS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Wauncher/CONTRIBUTORS.md diff --git a/Wauncher/CONTRIBUTORS.md b/Wauncher/CONTRIBUTORS.md new file mode 100644 index 0000000..272983b --- /dev/null +++ b/Wauncher/CONTRIBUTORS.md @@ -0,0 +1,12 @@ +ClassicCounter Wauncher Contributors + +Developers & Contributors +- koolych +- Ways +- Grizzle +- Simpy + +Maintainer +- eddies + +Thank you for your contributions! From de27353f841fdf5406d59eff8dc23bb8d63303d5 Mon Sep 17 00:00:00 2001 From: ways15xx <ways15xx@gmail.com> Date: Wed, 18 Mar 2026 00:25:42 -0400 Subject: [PATCH 49/51] release wauncher 3.0.1 hotfixes --- Wauncher/App.axaml.cs | 8 + Wauncher/Utils/Download.cs | 277 ++++++++++++++++++++--------- Wauncher/Utils/Game.cs | 13 ++ Wauncher/Views/MainWindow.axaml.cs | 123 +++++++------ Wauncher/Wauncher.csproj | 2 +- 5 files changed, 286 insertions(+), 137 deletions(-) diff --git a/Wauncher/App.axaml.cs b/Wauncher/App.axaml.cs index 19fe3cd..a4e44fe 100644 --- a/Wauncher/App.axaml.cs +++ b/Wauncher/App.axaml.cs @@ -46,6 +46,14 @@ public override void OnFrameworkInitializationCompleted() return; } + if (Game.IsRunning()) + { + Wauncher.Utils.ConsoleManager.ShowError( + "ClassicCounter is already running.\n\nPlease close the game before opening Wauncher again."); + desktop.Shutdown(); + return; + } + bool hasRecentSteamUser = Steam.GetRecentLoggedInSteamID(false).GetAwaiter().GetResult(); if (!hasRecentSteamUser) { diff --git a/Wauncher/Utils/Download.cs b/Wauncher/Utils/Download.cs index 8cec0dc..e6e2f59 100644 --- a/Wauncher/Utils/Download.cs +++ b/Wauncher/Utils/Download.cs @@ -1,10 +1,11 @@ -using Downloader; -using Refit; -using SharpCompress.Archives; -using SharpCompress.Archives.SevenZip; -using SharpCompress.Common; -using SharpCompress.Readers; -using Spectre.Console; +using Downloader; +using Refit; +using SharpCompress.Archives; +using SharpCompress.Archives.SevenZip; +using SharpCompress.Common; +using SharpCompress.Readers; +using Spectre.Console; +using System.Diagnostics; namespace Wauncher.Utils { @@ -271,20 +272,44 @@ public static async Task DownloadFullGame(StatusContext ctx) /// Designed for use from a GUI — takes progress/status callbacks instead of a StatusContext. /// Throws on error so the caller can handle it. /// </summary> - public static async Task InstallFullGame( - Action<string, string, double>? onProgress, // (filename, speed, totalPercent) - Action<string>? onStatus, - Action<double>? onExtractProgress = null) - { - await Steam.GetRecentLoggedInSteamID(); - if (string.IsNullOrEmpty(Steam.recentSteamID2)) - throw new Exception("Steam does not appear to be installed or you are not logged in."); - - onStatus?.Invoke("Fetching game files..."); - var gameFiles = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); - - if (gameFiles?.Files == null || gameFiles.Files.Count == 0) - throw new Exception("No game files returned. You may not be whitelisted.\nVisit classiccounter.cc/whitelist to request access."); + public static async Task InstallFullGame( + Action<string, string, double>? onProgress, // (filename, speed, totalPercent) + Action<string>? onStatus, + Action<double>? onExtractProgress = null) + { + await Steam.GetRecentLoggedInSteamID(); + if (string.IsNullOrEmpty(Steam.recentSteamID2)) + throw new Exception("Steam does not appear to be installed or you are not logged in."); + + onStatus?.Invoke("Fetching game files..."); + FullGameDownloadResponse gameFiles; + try + { + gameFiles = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); + } + catch (ApiException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("Not whitelisted. Visit classiccounter.cc/whitelist"); + } + catch (ApiException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + throw new Exception("Wrong Steam account or not logged in"); + } + catch (ApiException ex) when ((int)ex.StatusCode >= 500) + { + throw new Exception("Download server is down. Try again soon"); + } + catch (ApiException) + { + throw new Exception("Couldn't fetch game files. Try again soon"); + } + catch (HttpRequestException) + { + throw new Exception("No internet or server unreachable"); + } + + if (gameFiles?.Files == null || gameFiles.Files.Count == 0) + throw new Exception("No game files returned. You may not be whitelisted.\nVisit classiccounter.cc/whitelist to request access."); int total = gameFiles.Files.Count; int completed = 0; @@ -312,8 +337,8 @@ public static async Task InstallFullGame( completed++; } - onStatus?.Invoke("Extracting game files..."); - await ExtractSplitArchive(gameFiles.Files.Select(f => f.File).ToList(), onExtractProgress); + onStatus?.Invoke("Extracting game files... This may take a few minutes."); + await ExtractSplitArchive(gameFiles.Files.Select(f => f.File).ToList(), onExtractProgress); } private static string CalculateMD5(string filename) @@ -361,11 +386,11 @@ public static string GetProgressBar(double percentage) return bar; } // DOWNLOAD STATUS OVER - public static async Task ExtractSplitArchive(List<string> files, Action<double>? onProgress = null) - { - if (files == null || files.Count == 0) - { - throw new ArgumentException("No files provided for extraction"); + public static async Task ExtractSplitArchive(List<string> files, Action<double>? onProgress = null) + { + if (files == null || files.Count == 0) + { + throw new ArgumentException("No files provided for extraction"); } files.Sort(); @@ -383,19 +408,45 @@ public static async Task ExtractSplitArchive(List<string> files, Action<double>? string extractPath = Directory.GetCurrentDirectory(); string tempExtractPath = Path.Combine(extractPath, "ClassicCounter_temp"); - try - { - Directory.CreateDirectory(tempExtractPath); - - if (Debug.Enabled()) - Terminal.Debug("Starting in-process extraction to temp directory..."); - - await ExtractSplitArchiveToDirectory(files, tempExtractPath, onProgress); - - string classicCounterPath = Path.Combine(tempExtractPath, "ClassicCounter"); - if (Directory.Exists(classicCounterPath)) - { - if (Debug.Enabled()) + try + { + Directory.CreateDirectory(tempExtractPath); + + await Download7za(); + + string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); + if (launcherDir == null) + throw new InvalidOperationException("Could not determine launcher directory"); + + string exePath = Path.Combine(launcherDir, "7za.exe"); + + if (Debug.Enabled()) + Terminal.Debug("Starting 7za extraction to temp directory..."); + + using (var process = new Process()) + { + process.StartInfo = new ProcessStartInfo + { + FileName = exePath, + Arguments = $"x \"{firstFile}\" -o\"{tempExtractPath}\" -y", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + + process.Start(); + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + throw new Exception($"7za extraction failed with exit code: {process.ExitCode}"); + } + + onProgress?.Invoke(100.0); + + string classicCounterPath = Path.Combine(tempExtractPath, "ClassicCounter"); + if (Directory.Exists(classicCounterPath)) + { + if (Debug.Enabled()) Terminal.Debug("Moving contents from ClassicCounter folder to root directory..."); await Task.Run(() => MoveExtractedClassicCounterFiles(classicCounterPath, extractPath)); } @@ -404,43 +455,47 @@ public static async Task ExtractSplitArchive(List<string> files, Action<double>? throw new DirectoryNotFoundException("ClassicCounter folder not found in extracted contents"); } - try - { - Directory.Delete(tempExtractPath, true); - if (Debug.Enabled()) - Terminal.Debug("Deleted temporary extraction directory"); - - foreach (string file in files) - { - File.Delete(file); - if (Debug.Enabled()) - Terminal.Debug($"Deleted archive part: {file}"); - } - } - catch (Exception ex) - { - Terminal.Warning($"Failed to cleanup some temporary files: {ex.Message}"); - } + try + { + Directory.Delete(tempExtractPath, true); + if (Debug.Enabled()) + Terminal.Debug("Deleted temporary extraction directory"); + + foreach (string file in files) + { + File.Delete(file); + if (Debug.Enabled()) + Terminal.Debug($"Deleted archive part: {file}"); + } + + Delete7zaExecutable(); + } + catch (Exception ex) + { + Terminal.Warning($"Failed to cleanup some temporary files: {ex.Message}"); + } if (Debug.Enabled()) Terminal.Debug("Extraction and file movement completed successfully!"); } - catch (Exception ex) - { - Terminal.Error($"Extraction failed: {ex.Message}"); - if (Debug.Enabled()) - Terminal.Debug($"Stack trace: {ex.StackTrace}"); - - try - { - if (Directory.Exists(tempExtractPath)) - Directory.Delete(tempExtractPath, true); - } - catch { } - - throw; - } - } + catch (Exception ex) + { + Terminal.Error($"Extraction failed: {ex.Message}"); + if (Debug.Enabled()) + Terminal.Debug($"Stack trace: {ex.StackTrace}"); + + try + { + if (Directory.Exists(tempExtractPath)) + Directory.Delete(tempExtractPath, true); + } + catch { } + + Delete7zaExecutable(); + + throw; + } + } private static async Task Extract7z(string archivePath, string outputPath, Action<double>? onProgress = null) { @@ -486,12 +541,14 @@ private static void MoveExtractedClassicCounterFiles(string classicCounterPath, { string newFilePath = filePath.Replace(classicCounterPath, extractPath); - if (Path.GetFileName(filePath).Equals("launcher.exe", StringComparison.OrdinalIgnoreCase)) - { - if (Debug.Enabled()) - Terminal.Debug("Skipping launcher.exe"); - continue; - } + string fileName = Path.GetFileName(filePath); + if (fileName.Equals("launcher.exe", StringComparison.OrdinalIgnoreCase) || + fileName.Equals("wauncher.exe", StringComparison.OrdinalIgnoreCase)) + { + if (Debug.Enabled()) + Terminal.Debug($"Skipping {fileName}"); + continue; + } try { @@ -506,7 +563,65 @@ private static void MoveExtractedClassicCounterFiles(string classicCounterPath, Terminal.Warning($"Failed to move file {filePath}: {ex.Message}"); } } - } + } + + private static async Task Download7za() + { + string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); + if (launcherDir == null) + throw new InvalidOperationException("Could not determine launcher directory"); + + string exePath = Path.Combine(launcherDir, "7za.exe"); + if (File.Exists(exePath)) + return; + + string[] fallbackUrls = + { + "https://fastdl.classiccounter.cc/7za.exe", + "https://ollumcc.github.io/7za.exe" + }; + + Exception? lastError = null; + foreach (var url in fallbackUrls) + { + try + { + await _downloader.DownloadFileTaskAsync(url, exePath); + if (File.Exists(exePath)) + return; + } + catch (Exception ex) + { + lastError = ex; + } + } + + throw new Exception($"Couldn't download 7za.exe{(lastError != null ? $": {lastError.Message}" : string.Empty)}"); + } + + private static void Delete7zaExecutable() + { + try + { + string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); + if (string.IsNullOrWhiteSpace(launcherDir)) + return; + + string exePath = Path.Combine(launcherDir, "7za.exe"); + if (!File.Exists(exePath)) + return; + + File.Delete(exePath); + + if (Debug.Enabled()) + Terminal.Debug("Deleted 7za.exe"); + } + catch (Exception ex) + { + if (Debug.Enabled()) + Terminal.Debug($"Failed to delete 7za.exe: {ex.Message}"); + } + } private static async Task ExtractArchiveToDirectory(string archivePath, string outputDirectory, Action<double>? onProgress = null) { diff --git a/Wauncher/Utils/Game.cs b/Wauncher/Utils/Game.cs index 898e43d..810ffc0 100644 --- a/Wauncher/Utils/Game.cs +++ b/Wauncher/Utils/Game.cs @@ -16,6 +16,19 @@ public static class Game private static int _scoreCT = 0; private static int _scoreT = 0; + public static bool IsRunning() + { + try + { + return Process.GetProcessesByName("csgo").Length > 0 || + Process.GetProcessesByName("cc").Length > 0; + } + catch + { + return false; + } + } + public static async Task<bool> Launch() { List<string> arguments = Argument.GenerateGameArguments(); diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index 9cacb34..a720aa0 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -524,20 +524,28 @@ private void LaunchUpdate_Click(object? sender, Avalonia.Interactivity.RoutedEve _ = LaunchGameAsync(); } - private async Task LaunchGameAsync() - { - if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) - return; - - var vm = DataContext as MainWindowViewModel; - try - { - _settings = SettingsWindowViewModel.LoadGlobal(); - - if (vm != null) vm.GameStatus = "Running"; - - // Clear any arguments left over from a previous launch before adding new ones. - Argument.ClearAdditionalArguments(); + private async Task LaunchGameAsync() + { + if (Interlocked.Exchange(ref _launchInProgress, 1) == 1) + return; + + var vm = DataContext as MainWindowViewModel; + try + { + _settings = SettingsWindowViewModel.LoadGlobal(); + var selected = vm?.SelectedServer; + + if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort) && Game.IsRunning()) + { + Wauncher.Utils.ConsoleManager.ShowError( + "ClassicCounter is already running.\n\nPlease close the game before joining a server from Wauncher."); + return; + } + + if (vm != null) vm.GameStatus = "Running"; + + // Clear any arguments left over from a previous launch before adding new ones. + Argument.ClearAdditionalArguments(); Argument.AddArgument("-novid"); if (!string.IsNullOrWhiteSpace(_settings.LaunchOptions)) @@ -546,16 +554,17 @@ private async Task LaunchGameAsync() Argument.AddArgument(arg); } - var selected = vm?.SelectedServer; - if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) - { - Argument.AddArgument("+connect"); - Argument.AddArgument(selected.IpPort); - } - - await Game.Launch(); - - if (_settings.MinimizeToTray) Hide(); + if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) + { + Argument.AddArgument("+connect"); + Argument.AddArgument(selected.IpPort); + } + + var launched = await Game.Launch(); + if (!launched) + return; + + if (_settings.MinimizeToTray) Hide(); if (_settings.DiscordRpc) { @@ -589,16 +598,16 @@ private async Task LaunchGameAsync() } // ── Window chrome ─────────────────────────────────────────── - private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) - { - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - BeginMoveDrag(e); - } - - private void MinimizeButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - WindowState = WindowState.Minimized; - } + private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + BeginMoveDrag(e); + } + + private void MinimizeButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + WindowState = WindowState.Minimized; + } private bool _forceClose = false; @@ -772,27 +781,31 @@ await DownloadManager.InstallFullGame( vm.UpdateIndeterminate = false; }); }, - onStatus: status => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateStatusFile = status; - vm.UpdateStatusSpeed = ""; - vm.UpdateIndeterminate = !status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); - if (!vm.UpdateIndeterminate) - vm.UpdateProgress = 0; - }); - }, - onExtractProgress: extractPercent => - { - Dispatcher.UIThread.Post(() => - { - vm.UpdateIndeterminate = false; - vm.UpdateStatusFile = $"Extracting game files... {extractPercent:F0}%"; - vm.UpdateStatusSpeed = ""; - vm.UpdateProgress = extractPercent; - }); - }); + onStatus: status => + { + Dispatcher.UIThread.Post(() => + { + bool isExtracting = status.Contains("Extracting", StringComparison.OrdinalIgnoreCase); + vm.UpdateStatusFile = status; + vm.UpdateStatusSpeed = isExtracting ? "Large installs can take a few minutes." : ""; + vm.UpdateIndeterminate = isExtracting; + if (isExtracting) + vm.UpdateProgress = 0; + }); + }, + onExtractProgress: extractPercent => + { + Dispatcher.UIThread.Post(() => + { + if (extractPercent >= 100) + { + vm.UpdateIndeterminate = false; + vm.UpdateStatusFile = "Finalizing extracted files..."; + vm.UpdateStatusSpeed = ""; + vm.UpdateProgress = 100; + } + }); + }); // Immediately apply any post-install patches so first-time installs // end in a launch-ready state without requiring a second manual update. diff --git a/Wauncher/Wauncher.csproj b/Wauncher/Wauncher.csproj index 1b7b752..d6c8dc0 100644 --- a/Wauncher/Wauncher.csproj +++ b/Wauncher/Wauncher.csproj @@ -15,7 +15,7 @@ <PublishSingleFile>true</PublishSingleFile> <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> <SelfContained>false</SelfContained> - <Version>3.0.0</Version> + <Version>3.0.1</Version> <AssemblyVersion>$(Version)</AssemblyVersion> <FileVersion>$(Version)</FileVersion> <AssemblyName>wauncher</AssemblyName> From 9b69da70b2d0b7343ae82f256c9c21b156748fe1 Mon Sep 17 00:00:00 2001 From: ways15xx <ways15xx@gmail.com> Date: Wed, 18 Mar 2026 12:53:52 -0400 Subject: [PATCH 50/51] fix wauncher gamestate listener crash --- Wauncher/Utils/Game.cs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Wauncher/Utils/Game.cs b/Wauncher/Utils/Game.cs index 810ffc0..6ae3cf6 100644 --- a/Wauncher/Utils/Game.cs +++ b/Wauncher/Utils/Game.cs @@ -42,11 +42,7 @@ public static async Task<bool> Launch() if (settings.DiscordRpc) { - _port = GeneratePort(); - - _listener = new($"http://localhost:{_port}/"); - _listener.NewGameState += OnNewGameState; - _listener.Start(); + EnsureGameStateListenerStarted(); try { @@ -161,14 +157,32 @@ public static async Task Monitor() await Task.Delay(2000); } - _listener?.Stop(); - _listener = null; + _process = null; _node = null; _map = "main_menu"; _scoreCT = 0; _scoreT = 0; } + private static void EnsureGameStateListenerStarted() + { + if (_listener != null) + return; + + _port = GeneratePort(); + + var listener = new GameStateListener($"http://localhost:{_port}/"); + listener.NewGameState += OnNewGameState; + + if (!listener.Start()) + { + listener.NewGameState -= OnNewGameState; + throw new InvalidOperationException("Couldn't start Wauncher's local game state listener."); + } + + _listener = listener; + } + private static int GeneratePort() { int port = new Random().Next(1024, 65536); From 54288fc4db36d7ee252f36c518e6c04a0aa5df14 Mon Sep 17 00:00:00 2001 From: Grizzle <grizzle@crystallium.net> Date: Wed, 18 Mar 2026 18:01:29 +0100 Subject: [PATCH 51/51] Fix launcher errors throwing exceptions, add another method of finding Steam path (adds around 1MB to size) if registry key fails --- Wauncher/App.axaml.cs | 76 ++++++++++++++--------------- Wauncher/Utils/Steam.cs | 21 ++++++-- Wauncher/Utils/SteamNative.cs | 72 +++++++++++++++++++++++++++ Wauncher/Views/MainWindow.axaml.cs | 4 +- Wauncher/Wauncher.csproj | 6 +++ Wauncher/steam_api.dll | Bin 0 -> 274072 bytes Wauncher/steam_api64.dll | Bin 0 -> 317080 bytes 7 files changed, 136 insertions(+), 43 deletions(-) create mode 100644 Wauncher/Utils/SteamNative.cs create mode 100644 Wauncher/steam_api.dll create mode 100644 Wauncher/steam_api64.dll diff --git a/Wauncher/App.axaml.cs b/Wauncher/App.axaml.cs index a4e44fe..d5c261c 100644 --- a/Wauncher/App.axaml.cs +++ b/Wauncher/App.axaml.cs @@ -28,51 +28,51 @@ public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - DisableAvaloniaDataAnnotationValidation(); - - if (!Steam.IsInstalled()) - { - Wauncher.Utils.ConsoleManager.ShowError( - "Steam is required to use Wauncher.\n\nPlease install Steam and relaunch."); - desktop.Shutdown(); - return; - } - - if (!IsSteamRunning()) - { - Wauncher.Utils.ConsoleManager.ShowError( - "Steam must be open before using Wauncher.\n\nPlease open Steam, then relaunch Wauncher."); - desktop.Shutdown(); - return; - } - - if (Game.IsRunning()) - { - Wauncher.Utils.ConsoleManager.ShowError( - "ClassicCounter is already running.\n\nPlease close the game before opening Wauncher again."); - desktop.Shutdown(); - return; - } - - bool hasRecentSteamUser = Steam.GetRecentLoggedInSteamID(false).GetAwaiter().GetResult(); - if (!hasRecentSteamUser) - { - Wauncher.Utils.ConsoleManager.ShowError( - "Steam is open, but no logged-in Steam account was detected.\n\nPlease sign in to Steam and relaunch Wauncher."); - desktop.Shutdown(); - return; + DisableAvaloniaDataAnnotationValidation(); + + if (!Steam.IsInstalled()) + { + ConsoleManager.ShowError( + "Steam is required to use Wauncher.\n\nPlease install Steam and relaunch."); + Environment.Exit(1); + return; + } + + if (!IsSteamRunning()) + { + ConsoleManager.ShowError( + "Steam must be open before using Wauncher.\n\nPlease open Steam, then relaunch Wauncher."); + Environment.Exit(2); + return; + } + + if (Game.IsRunning()) + { + ConsoleManager.ShowError( + "ClassicCounter is already running.\n\nPlease close the game before opening Wauncher again."); + Environment.Exit(3); + return; + } + + bool hasRecentSteamUser = Steam.GetRecentLoggedInSteamID(false).GetAwaiter().GetResult(); + if (!hasRecentSteamUser) + { + ConsoleManager.ShowError( + "Steam is open, but no logged-in Steam account was detected.\n\nPlease sign in to Steam and relaunch Wauncher."); + Environment.Exit(4); + return; } - - // Always init so Discord username/avatar callbacks fire for the greeting. - // Presence is only pushed via Update() when RPC is enabled. + + // Always init so Discord username/avatar callbacks fire for the greeting. + // Presence is only pushed via Update() when RPC is enabled. try { if (DependencyChecks.IsDiscordInstalled()) Discord.Init(); } catch - { - // Discord integration is optional. + { + // Discord integration is optional. } desktop.MainWindow = new MainWindow diff --git a/Wauncher/Utils/Steam.cs b/Wauncher/Utils/Steam.cs index 4d96aa0..0f301e4 100644 --- a/Wauncher/Utils/Steam.cs +++ b/Wauncher/Utils/Steam.cs @@ -1,5 +1,8 @@ using Microsoft.Win32; using Gameloop.Vdf; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.Unicode; namespace Wauncher.Utils { @@ -9,18 +12,30 @@ public class Steam public static string? recentSteamID2 { get; private set; } private static string? steamPath { get; set; } + private static string? GetSteamInstallPath() { + // If was already found return it right away. + if (steamPath != null) + return steamPath; + + // Try finding it registry. using (RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)) { using (RegistryKey? key = hklm.OpenSubKey(@"SOFTWARE\Wow6432Node\Valve\Steam") ?? hklm.OpenSubKey(@"SOFTWARE\Valve\Steam")) { steamPath = key?.GetValue("InstallPath") as string; - if (Debug.Enabled()) - Terminal.Debug($"Steam folder found at {steamPath}"); - return steamPath; + if (steamPath != null) + { + if (Debug.Enabled()) + Terminal.Debug($"Steam folder found at {steamPath}"); + return steamPath; + } } } + + // If registry didn't work, try natively. + return steamPath = SteamNative.GetSteamInstallPath(); } public static bool IsInstalled() diff --git a/Wauncher/Utils/SteamNative.cs b/Wauncher/Utils/SteamNative.cs new file mode 100644 index 0000000..e0367be --- /dev/null +++ b/Wauncher/Utils/SteamNative.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Wauncher.Utils +{ + public class SteamNative + { + // 64-bit steam_api calls + [DllImport("platform/steam_api64", EntryPoint = "SteamAPI_InitFlat", CallingConvention = CallingConvention.Cdecl)] + private unsafe static extern int SteamAPI64_InitFlat(byte* steamErrMsg); + [DllImport("platform/steam_api64", EntryPoint = "SteamAPI_GetSteamInstallPath", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr SteamAPI64_GetSteamInstallPath(); + [DllImport("platform/steam_api64", EntryPoint = "SteamAPI_Shutdown", CallingConvention = CallingConvention.Cdecl)] + private static extern void SteamAPI64_Shutdown(); + + // 32-bit steam_api calls + [DllImport("platform/steam_api", EntryPoint = "SteamAPI_InitFlat", CallingConvention = CallingConvention.Cdecl)] + private unsafe static extern int SteamAPI_InitFlat(byte* steamErrMsg); + [DllImport("platform/steam_api", EntryPoint = "SteamAPI_GetSteamInstallPath", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr SteamAPI_GetSteamInstallPath(); + [DllImport("platform/steam_api", EntryPoint = "SteamAPI_Shutdown", CallingConvention = CallingConvention.Cdecl)] + private static extern void SteamAPI_Shutdown(); + + private static string? _steamPath = null; + + public static string? GetSteamInstallPath() + { + // If was already found return it right away. + if (_steamPath != null) + return _steamPath; + + // If it wasn't found before by registry, try using Steamworks. + // (Steamworks.NET doesn't give access to native methods) + string steamDll = Environment.Is64BitProcess ? "steam_api64.dll" : "steam_api.dll"; + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(steamDll) + ?? throw new Exception($"{steamDll} wasn't found in the binary!"); + + // If the needed steam_api(64).dll doesn't exist in the folder, unpack it from the binary. + var outputPath = Path.Combine(AppContext.BaseDirectory, "platform", steamDll); + if (!File.Exists(outputPath)) + { + Directory.CreateDirectory(Path.Combine(AppContext.BaseDirectory, "platform")); + using (var file = File.Create(outputPath)) + stream.CopyTo(file); + } + + // Make sure the steam_appid.txt exists, because if it doesn't steam throws an error. + if (!File.Exists("steam_appid.txt")) + File.WriteAllText("steam_appid.txt", "730"); + + unsafe + { + byte* errMsg = stackalloc byte[1024]; + int result = Environment.Is64BitProcess ? SteamAPI64_InitFlat(errMsg) : SteamAPI_InitFlat(errMsg); + if (result > 0) + { + ConsoleManager.ShowError($"Steamworks couldn't initialize to find Steam. (Error ({result}) {Marshal.PtrToStringUTF8((IntPtr)errMsg)})"); + return null; + } + _steamPath = Marshal.PtrToStringUTF8(Environment.Is64BitProcess ? SteamAPI64_GetSteamInstallPath() : SteamAPI_GetSteamInstallPath()); + if (Environment.Is64BitProcess) SteamAPI64_Shutdown(); else SteamAPI_Shutdown(); + } + + return _steamPath; + } + } +} diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index a720aa0..8d0911e 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -537,7 +537,7 @@ private async Task LaunchGameAsync() if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort) && Game.IsRunning()) { - Wauncher.Utils.ConsoleManager.ShowError( + ConsoleManager.ShowError( "ClassicCounter is already running.\n\nPlease close the game before joining a server from Wauncher."); return; } @@ -577,7 +577,7 @@ private async Task LaunchGameAsync() } catch (Exception ex) { - Wauncher.Utils.ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); + ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); } finally { diff --git a/Wauncher/Wauncher.csproj b/Wauncher/Wauncher.csproj index d6c8dc0..989cc0e 100644 --- a/Wauncher/Wauncher.csproj +++ b/Wauncher/Wauncher.csproj @@ -56,5 +56,11 @@ <PackageReference Include="SharpCompress" Version="0.47.0" /> <PackageReference Include="Spectre.Console" Version="0.49.1" /> <PackageReference Include="Svg.Controls.Skia.Avalonia" Version="11.3.0.4" /> + <EmbeddedResource Include="steam_api.dll"> + <LogicalName>steam_api.dll</LogicalName> + </EmbeddedResource> + <EmbeddedResource Include="steam_api64.dll"> + <LogicalName>steam_api64.dll</LogicalName> + </EmbeddedResource> </ItemGroup> </Project> diff --git a/Wauncher/steam_api.dll b/Wauncher/steam_api.dll new file mode 100644 index 0000000000000000000000000000000000000000..b7ae7971a76a1cffe5ff716ca6e3e54e97e65bf5 GIT binary patch literal 274072 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 D(BuO{ literal 0 HcmV?d00001 diff --git a/Wauncher/steam_api64.dll b/Wauncher/steam_api64.dll new file mode 100644 index 0000000000000000000000000000000000000000..f0a415440a89163c1d9ef2cf69fde329b73b5dba GIT binary patch literal 317080 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 AWB>pF literal 0 HcmV?d00001