├── publish.bat ├── Launcher ├── Utils │ ├── Debug.cs │ ├── Console.cs │ ├── Version.cs │ ├── Argument.cs │ ├── Api.cs │ ├── Terminal.cs │ ├── Discord.cs │ ├── Steam.cs │ ├── Dependency.cs │ ├── Game.cs │ ├── Patch.cs │ └── Download.cs ├── assets │ └── steamhappy.txt ├── Launcher.csproj └── Program.cs ├── .github └── workflows │ └── build.yml ├── dependencies.json ├── LICENSE ├── Launcher.sln ├── README.md └── .gitignore /publish.bat: -------------------------------------------------------------------------------- 1 | dotnet publish -c Release 2 | certutil -hashfile "Launcher\bin\Release\net8.0-windows7.0\win-x64\publish\launcher.exe" MD5 3 | pause -------------------------------------------------------------------------------- /Launcher/Utils/Debug.cs: -------------------------------------------------------------------------------- 1 | namespace Launcher.Utils 2 | { 3 | public static class Debug 4 | { 5 | public static bool Enabled() => Argument.Exists("--debug-mode"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Launcher/Utils/Console.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Launcher.Utils 4 | { 5 | public static class ConsoleManager 6 | { 7 | [DllImport("kernel32.dll")] 8 | private static extern IntPtr GetConsoleWindow(); 9 | 10 | [DllImport("user32.dll")] 11 | private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); 12 | 13 | private const int SW_HIDE = 0; 14 | 15 | private static IntPtr ConsoleHandle = GetConsoleWindow(); 16 | 17 | public static void HideConsole() 18 | { 19 | ShowWindow(ConsoleHandle, SW_HIDE); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: .NET Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: '8.0.x' 20 | 21 | - name: Restore dependencies 22 | run: dotnet restore 23 | 24 | - name: Build 25 | run: dotnet build --configuration Release --no-restore 26 | 27 | - name: Publish 28 | run: dotnet publish --configuration Release --no-build -p:PublishSingleFile=true -p:SelfContained=false --runtime win-x64 29 | 30 | - name: Upload artifact 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: launcher 34 | path: Launcher/bin/Release/net8.0-windows7.0/win-x64/publish/launcher.exe -------------------------------------------------------------------------------- /Launcher/Utils/Version.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace Launcher.Utils 4 | { 5 | public static class Version 6 | { 7 | public static string Current = "2.2.6.1"; 8 | 9 | public async static Task GetLatestVersion() 10 | { 11 | if (Debug.Enabled()) 12 | Terminal.Debug("Getting latest version."); 13 | 14 | try 15 | { 16 | string responseString = await Api.GitHub.GetLatestRelease(); 17 | JObject responseJson = JObject.Parse(responseString); 18 | 19 | if (responseJson["tag_name"] == null) 20 | throw new Exception("\"tag_name\" doesn't exist in response."); 21 | 22 | return (string?)responseJson["tag_name"] ?? Current; 23 | } 24 | catch 25 | { 26 | if (Debug.Enabled()) 27 | Terminal.Debug("Couldn't get latest version."); 28 | } 29 | 30 | return Current; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | { 4 | "name": "dxsetup.exe", 5 | "download_url": null, 6 | "path": "/directx_installer/dxsetup.exe", 7 | "registry": [ 8 | { 9 | "path": "SOFTWARE\\WOW6432Node\\Microsoft\\DirectX", 10 | "key": "InstalledVersion", 11 | "data": 0x0000000900000000 12 | } 13 | ] 14 | }, 15 | { 16 | "name": "vc_redist.x86.exe", 17 | "download_url": "https://aka.ms/vs/17/release/vc_redist.x86.exe", 18 | "path": "/dependencies/vc_redist.x86.exe", 19 | "registry": [ 20 | { 21 | "path": "SOFTWARE\\WOW6432Node\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X64", 22 | "key": "Installed", 23 | "data": 1 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "vc_redist.x64.exe", 29 | "download_url": "https://aka.ms/vs/17/release/vc_redist.x64.exe", 30 | "path": "/dependencies/vc_redist.x64.exe", 31 | "registry": [ 32 | { 33 | "path": "SOFTWARE\\WOW6432Node\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X64", 34 | "key": "Installed", 35 | "data": 1 36 | } 37 | ] 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 heapy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Launcher.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.11.35327.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Launcher", "Launcher\Launcher.csproj", "{C52CA2D0-3034-4F68-A523-BD7AED0A7479}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {C52CA2D0-3034-4F68-A523-BD7AED0A7479}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {C52CA2D0-3034-4F68-A523-BD7AED0A7479}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {C52CA2D0-3034-4F68-A523-BD7AED0A7479}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {C52CA2D0-3034-4F68-A523-BD7AED0A7479}.Release|Any CPU.Build.0 = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(ExtensibilityGlobals) = postSolution 25 | SolutionGuid = {1DCABFD6-A97B-4F36-844D-321062A69A32} 26 | EndGlobalSection 27 | EndGlobal 28 | -------------------------------------------------------------------------------- /Launcher/assets/steamhappy.txt: -------------------------------------------------------------------------------- 1 | . 2 | . .:.:. . 3 | .-:. -@%.%@= . 4 | .=.%@@=. +@@@@@@ . 5 | : :@@@@@@: .@@@@@+ : 6 | @#: .#@@@@@: :*%+:. .=@ 7 | @+:.. :*#*: .. .:.:*@ 8 | @=....:--==. ... ...:....:+@ 9 | @#.........:#-:-:---:::::=:......=@ 10 | @:..........+@+ .. .-@@@.........#@ 11 | @+=%@*..........*@@@@@@@@@@@@@%:.......:@@@@@@ 12 | %-...:.........-@@@@@@@@@@@@@@@#........=@@@@ 13 | @-............-@@@@@@@@@@@@@@@+........:%@@ 14 | @*:..........-@@@@@@@@@@@@@@@-.........#@ 15 | @-..........-@@@@@@@@@@@@@@%:.........+@ 16 | @=...........@@@++++++#@@@@+..........+@ 17 | @*...........#@=+++++++*@@%...........+@ 18 | %:..........=*+++++++++%@-...........*@ 19 | @=..........:=-+++++++++*:..........:% 20 | @-..........--=++++++++............+@ 21 | @-..........-++: . .+:..........:+@ 22 | @#:..........=-.:=:...........:*@ 23 | @%:........................*@ 24 | @@#-.................:*@@ 25 | @@*...:**+***#%@@@ 26 | @#:.=@ @@@@@@ 27 | @%@ @@@@ -------------------------------------------------------------------------------- /Launcher/Launcher.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0-windows7.0 6 | enable 7 | enable 8 | ClassicCounter Team 9 | 2.2.6.1 10 | $(Version) 11 | $(Version) 12 | launcher 13 | $(MSBuildProjectName.Replace(" ", "_")) 14 | ClassicCounter Launcher 15 | ClassicCounter Team, koolych 16 | 17 | NU1701 18 | 19 | true 20 | false 21 | win-x64 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Launcher/Utils/Argument.cs: -------------------------------------------------------------------------------- 1 | namespace Launcher.Utils 2 | { 3 | public static class Argument 4 | { 5 | private static List _launcherArguments = new() 6 | { 7 | "--debug-mode", 8 | "--skip-updates", 9 | "--skip-validating", 10 | "--validate-all", 11 | "--patch-only", 12 | "--gc", 13 | "--disable-rpc", 14 | "--install-dependencies" 15 | }; 16 | 17 | private static List _additionalArguments = new(); 18 | public static void AddArgument(string argument) 19 | { 20 | if (!_additionalArguments.Contains(argument.ToLowerInvariant())) 21 | { 22 | _additionalArguments.Add(argument.ToLowerInvariant()); 23 | } 24 | } 25 | 26 | public static bool Exists(string argument) 27 | { 28 | IEnumerable arguments = Environment.GetCommandLineArgs(); 29 | 30 | foreach (string arg in arguments) 31 | if (arg.ToLowerInvariant() == argument) return true; 32 | 33 | return false; 34 | } 35 | 36 | public static List GenerateGameArguments(bool passLauncherArguments = false) 37 | { 38 | IEnumerable launcherArguments = Environment.GetCommandLineArgs(); 39 | List gameArguments = new(); 40 | 41 | foreach (string arg in launcherArguments) 42 | if ((passLauncherArguments || !_launcherArguments.Contains(arg.ToLowerInvariant())) 43 | && !arg.EndsWith(".exe")) 44 | gameArguments.Add(arg.ToLowerInvariant()); 45 | 46 | gameArguments.AddRange(_additionalArguments); 47 | return gameArguments; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Launcher/Utils/Api.cs: -------------------------------------------------------------------------------- 1 | using Refit; 2 | 3 | namespace Launcher.Utils 4 | { 5 | public class FullGameDownload 6 | { 7 | public required string File { get; set; } 8 | public required string Link { get; set; } 9 | public required string Hash { get; set; } 10 | } 11 | 12 | public class FullGameDownloadResponse 13 | { 14 | public required List Files { get; set; } 15 | } 16 | 17 | public interface IGitHub 18 | { 19 | [Headers("User-Agent: ClassicCounter Launcher")] 20 | [Get("/repos/ClassicCounter/launcher/releases/latest")] 21 | Task GetLatestRelease(); 22 | 23 | [Headers("User-Agent: ClassicCounter Launcher", 24 | "Accept: application/vnd.github.raw+json")] 25 | [Get("/repos/ClassicCounter/launcher/contents/dependencies.json")] 26 | Task GetDependencies(); 27 | } 28 | 29 | public interface IClassicCounter 30 | { 31 | [Headers("User-Agent: ClassicCounter Launcher")] 32 | [Get("/patch/get")] 33 | Task GetPatches(); 34 | 35 | [Headers("User-Agent: ClassicCounter Launcher")] 36 | [Get("/game/get")] 37 | Task GetFullGameValidate(); 38 | 39 | [Headers("User-Agent: ClassicCounter Launcher")] 40 | [Get("/game/full")] 41 | Task GetFullGameDownload([Query] string steam_id); 42 | } 43 | 44 | public static class Api 45 | { 46 | private static RefitSettings _settings = new RefitSettings(new NewtonsoftJsonContentSerializer()); 47 | public static IGitHub GitHub = RestService.For("https://api.github.com", _settings); 48 | public static IClassicCounter ClassicCounter = RestService.For("https://classiccounter.cc/api", _settings); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Launcher/Utils/Terminal.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Console; 2 | using System.Reflection; 3 | 4 | namespace Launcher.Utils 5 | { 6 | public static class Terminal 7 | { 8 | private static string _prefix = "[orange1]Classic[/][blue]Counter[/]"; 9 | private static string _grey = "grey82"; 10 | private static string _seperator = "[grey50]|[/]"; 11 | private static Stream? stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Launcher.Assets.steamhappy.txt"); 12 | private static string steamhappy = new StreamReader(stream).ReadToEnd(); 13 | public static void Init() 14 | { 15 | AnsiConsole.MarkupLine($"{_prefix} {_seperator} [{_grey}]Launcher maintained by [/][purple4_1]koolych[/][{_grey}][/]"); 16 | AnsiConsole.MarkupLine($"{_prefix} {_seperator} [{_grey}]Coded by [/][lightcoral]heapy[/][{_grey}][/]"); 17 | AnsiConsole.MarkupLine($"{_prefix} {_seperator} [{_grey}]https://github.com/ClassicCounter [/]"); 18 | AnsiConsole.MarkupLine($"{_prefix} {_seperator} [{_grey}]Version: {Version.Current}[/]"); 19 | } 20 | 21 | public static void Print(object? message) 22 | => AnsiConsole.MarkupLine($"{_prefix} {_seperator} [{_grey}]{Markup.Escape(message?.ToString() ?? string.Empty)}[/]"); 23 | 24 | public static void Success(object? message) 25 | => AnsiConsole.MarkupLine($"{_prefix} {_seperator} [green1]{Markup.Escape(message?.ToString() ?? string.Empty)}[/]"); 26 | 27 | public static void Warning(object? message) 28 | => AnsiConsole.MarkupLine($"{_prefix} {_seperator} [yellow]{Markup.Escape(message?.ToString() ?? string.Empty)}[/]"); 29 | 30 | public static void Error(object? message) 31 | => AnsiConsole.MarkupLine($"{_prefix} {_seperator} [red]{Markup.Escape(message?.ToString() ?? string.Empty)}[/]"); 32 | 33 | public static void Debug(object? message) 34 | => AnsiConsole.MarkupLine($"[purple]{Markup.Escape(message?.ToString() ?? string.Empty)}[/]"); 35 | 36 | public static void SteamHappy() => 37 | AnsiConsole.Write(steamhappy); 38 | 39 | private static string Date() 40 | => $"[{_grey}]{DateTime.Now.ToString("HH:mm:ss")}[/]"; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Launcher/Utils/Discord.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC; 2 | using DiscordRPC.Logging; 3 | using DiscordRPC.Message; 4 | 5 | namespace Launcher.Utils 6 | { 7 | public static class Discord 8 | { 9 | private static readonly string _appId = "1133457462024994947"; 10 | private static DiscordRpcClient _client = new DiscordRpcClient(_appId); 11 | private static RichPresence _presence = new RichPresence(); 12 | public static string? CurrentUserId { get; private set; } // ! DEPRECATED ! for whitelist check 13 | 14 | public static void Init() 15 | { 16 | _client.OnReady += OnReady; 17 | 18 | _client.Logger = new ConsoleLogger() 19 | { 20 | Level = Debug.Enabled() ? LogLevel.Warning : LogLevel.None 21 | }; 22 | 23 | if (!_client.Initialize()) 24 | { 25 | return; 26 | } 27 | 28 | SetDetails("In Launcher"); 29 | SetTimestamp(DateTime.UtcNow); 30 | SetLargeArtwork("icon"); 31 | 32 | Update(); 33 | } 34 | 35 | public static void Update() => _client.SetPresence(_presence); 36 | 37 | public static void SetDetails(string? details) => _presence.Details = details; 38 | public static void SetState(string? state) => _presence.State = state; 39 | 40 | public static void SetTimestamp(DateTime? time) 41 | { 42 | if (_presence.Timestamps == null) _presence.Timestamps = new(); 43 | _presence.Timestamps.Start = time; 44 | } 45 | 46 | public static void SetLargeArtwork(string? key) 47 | { 48 | if (_presence.Assets == null) _presence.Assets = new(); 49 | _presence.Assets.LargeImageKey = key; 50 | } 51 | 52 | public static void SetSmallArtwork(string? key) 53 | { 54 | if (_presence.Assets == null) _presence.Assets = new(); 55 | _presence.Assets.SmallImageKey = key; 56 | } 57 | 58 | private static void OnReady(object sender, ReadyMessage e) 59 | { 60 | CurrentUserId = e.User.ID.ToString(); // ! DEPRECATED ! for passing current uid to api 61 | 62 | if (Debug.Enabled()) 63 | Terminal.Debug($"Discord RPC: User is ready => @{e.User.Username} ({e.User.ID})"); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

ClassicCounter Launcher

3 |

4 | Launcher for ClassicCounter with Discord RPC, Auto-Updates and More! 5 |
6 | Written in C# using .NET 8. 7 |

8 |

9 | 10 | [![Downloads][downloads-shield]][downloads-url] 11 | [![Stars][stars-shield]][stars-url] 12 | [![Issues][issues-shield]][issues-url] 13 | [![MIT License][license-shield]][license-url] 14 | 15 | > [!IMPORTANT] 16 | > .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). 17 | 18 | ## Arguments 19 | - `--debug-mode` - Enables debug mode, prints additional info. 20 | - `--disable-rpc` - Disables Discord RPC. 21 | - `--gc` - Launches the Game with custom Game Coordinator. 22 | - `--install-dependencies` - Launches setup process for required Game dependencies. 23 | - `--patch-only` - Will only check for patches, won't open the game. 24 | - `--skip-updates` - Skips checking for launcher updates. 25 | - `--skip-validating` - Skips validating patches. 26 | - `--validate-all` - Validates all game files. 27 | 28 | > [!CAUTION] 29 | > **Using `--skip-updates` or `--skip-validating` is NOT recommended!** 30 | > **An outdated launcher or patches might cause issues.** 31 | 32 | ## Packages Used 33 | - [CSGSI](https://github.com/rakijah/CSGSI) by [rakijah](https://github.com/rakijah) 34 | - [DiscordRichPresence](https://github.com/Lachee/discord-rpc-csharp) by [Lachee](https://github.com/Lachee) 35 | - [Downloader](https://github.com/bezzad/Downloader) by [bezzad](https://github.com/bezzad) 36 | - [Gameloop.Vdf](https://github.com/shravan2x/Gameloop.Vdf) by [shravan2x](https://github.com/shravan2x) 37 | - [Refit](https://github.com/reactiveui/refit) by [ReactiveUI](https://github.com/reactiveui) 38 | - [Spectre.Console](https://github.com/spectreconsole/spectre.console) by [Spectre Console](https://github.com/spectreconsole) 39 | 40 | [downloads-shield]: https://img.shields.io/github/downloads/classiccounter/launcher/total.svg?style=for-the-badge 41 | [downloads-url]: https://github.com/classiccounter/launcher/releases/latest 42 | [stars-shield]: https://img.shields.io/github/stars/classiccounter/launcher.svg?style=for-the-badge 43 | [stars-url]: https://github.com/classiccounter/launcher/stargazers 44 | [issues-shield]: https://img.shields.io/github/issues/classiccounter/launcher.svg?style=for-the-badge 45 | [issues-url]: https://github.com/classiccounter/launcher/issues 46 | [license-shield]: https://img.shields.io/github/license/classiccounter/launcher.svg?style=for-the-badge 47 | [license-url]: https://github.com/classiccounter/launcher/blob/main/LICENSE.txt 48 | -------------------------------------------------------------------------------- /Launcher/Utils/Steam.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using Gameloop.Vdf; 3 | 4 | namespace Launcher.Utils 5 | { 6 | public class Steam 7 | { 8 | public static string? recentSteamID64 { get; private set; } 9 | public static string? recentSteamID2 { get; private set; } 10 | 11 | private static string? steamPath { get; set; } 12 | private static string? GetSteamInstallPath() 13 | { 14 | using (RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)) 15 | { 16 | using (RegistryKey? key = hklm.OpenSubKey(@"SOFTWARE\Wow6432Node\Valve\Steam") ?? hklm.OpenSubKey(@"SOFTWARE\Valve\Steam")) 17 | { 18 | steamPath = key?.GetValue("InstallPath") as string; 19 | if (Debug.Enabled()) 20 | Terminal.Debug($"Steam folder found at {steamPath}"); 21 | return steamPath; 22 | } 23 | } 24 | } 25 | public static async Task GetRecentLoggedInSteamID() 26 | { 27 | steamPath = GetSteamInstallPath(); 28 | if (string.IsNullOrEmpty(steamPath)) 29 | { 30 | Terminal.Error("Your Steam install couldn't be found."); 31 | Terminal.Error("Closing launcher in 5 seconds..."); 32 | await Task.Delay(5000); 33 | Environment.Exit(1); 34 | } 35 | var loginUsersPath = Path.Combine(steamPath, "config", "loginusers.vdf"); 36 | dynamic loginUsers = VdfConvert.Deserialize(File.ReadAllText(loginUsersPath)); 37 | foreach (var user in loginUsers.Value) 38 | { 39 | var mostRecent = user.Value.MostRecent.Value; 40 | if (mostRecent == "1") 41 | { 42 | recentSteamID64 = user.Key; 43 | recentSteamID2 = ConvertToSteamID2(user.Key); 44 | } 45 | } 46 | if (Debug.Enabled() && !string.IsNullOrEmpty(recentSteamID64)) 47 | { 48 | Terminal.Debug($"Most recent Steam account (SteamID64): {recentSteamID64}"); 49 | Terminal.Debug($"Most recent Steam account (SteamID2): {recentSteamID2}"); 50 | } 51 | } 52 | 53 | private static string ConvertToSteamID2(string steamID64) 54 | { 55 | ulong id64 = ulong.Parse(steamID64); 56 | ulong constValue = 76561197960265728; 57 | ulong accountID = id64 - constValue; 58 | ulong y = accountID % 2; 59 | ulong z = accountID / 2; 60 | return $"STEAM_1:{y}:{z}"; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Launcher/Utils/Dependency.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using Spectre.Console; 5 | using System.Diagnostics; 6 | 7 | namespace Launcher.Utils 8 | { 9 | public class Dependency // to everyone seeing this: I am sorry, I think I am doing my best copying the rest of the code :innocent: 10 | { 11 | [JsonProperty(PropertyName = "name")] 12 | public required string Name { get; set; } 13 | 14 | [JsonProperty(PropertyName = "download_url")] 15 | public string? URL { get; set; } 16 | 17 | [JsonProperty(PropertyName = "path")] 18 | public required string Path { get; set; } 19 | 20 | [JsonProperty(PropertyName = "registry")] 21 | public required List RegistryList { get; set; } 22 | 23 | public class Registry 24 | { 25 | [JsonProperty(PropertyName = "path")] 26 | public required string Path { get; set; } 27 | 28 | [JsonProperty(PropertyName = "key")] 29 | public required string Key { get; set; } 30 | 31 | [JsonProperty(PropertyName = "value")] 32 | public required string Value { get; set; } 33 | } 34 | } 35 | 36 | public class Dependencies(bool success, List localDependencies, List remoteDependencies) 37 | { 38 | public bool Success = success; 39 | public List LocalDependencies = localDependencies; 40 | public List RemoteDependencies = remoteDependencies; 41 | } 42 | 43 | public static class DependencyManager 44 | { 45 | private static Process? _process; 46 | public static string directory = Directory.GetCurrentDirectory(); 47 | 48 | public async static Task> Get() 49 | { 50 | List dependencies = new List(); 51 | 52 | if (Debug.Enabled()) 53 | Terminal.Debug("Getting list of dependencies."); 54 | try 55 | { 56 | string responseString = await Api.GitHub.GetDependencies(); 57 | 58 | JObject responseJson = JObject.Parse(responseString); 59 | 60 | if (responseJson["files"] != null) 61 | dependencies = responseJson["files"]!.ToObject()!.ToList(); 62 | } 63 | catch 64 | { 65 | if (Debug.Enabled()) 66 | Terminal.Debug("Couldn't get list of dependencies."); 67 | } 68 | return dependencies; 69 | } 70 | 71 | public static bool IsInstalled(StatusContext ctx, Dependency dependency) 72 | { 73 | Dependency.Registry registry = dependency.RegistryList.First(); 74 | using (RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)) 75 | { 76 | using (RegistryKey? key = hklm.OpenSubKey($@"{registry.Path}")) 77 | { 78 | string? keyValue = key?.GetValue(registry.Key) as string; 79 | if (keyValue != registry.Value) 80 | { 81 | Terminal.Warning($"{dependency.Name} is installed already!"); 82 | return true; 83 | } 84 | else 85 | return false; 86 | } 87 | } 88 | } 89 | 90 | public async static Task Install(StatusContext ctx, Dependencies dependencies) 91 | { 92 | _process = new Process(); 93 | bool success = false; 94 | 95 | List allDependencies = new List( 96 | dependencies.LocalDependencies.Count + 97 | dependencies.RemoteDependencies.Count); 98 | allDependencies.AddRange(dependencies.LocalDependencies); 99 | allDependencies.AddRange(dependencies.RemoteDependencies); 100 | foreach (Dependency dependency in allDependencies) 101 | { 102 | if (Debug.Enabled()) 103 | Terminal.Debug($"Executing dependency installer: {dependency.Name}"); 104 | _process.StartInfo.FileName = $"{directory}{dependency.Path}"; 105 | _process.StartInfo.UseShellExecute = true; 106 | _process.StartInfo.Verb = "runas"; 107 | try 108 | { 109 | _process.Start(); 110 | await _process.WaitForExitAsync(); 111 | if (Debug.Enabled()) 112 | Terminal.Debug($"Dependency installer {dependency.Name} has exited with status code {_process.ExitCode}"); 113 | success = true; 114 | } 115 | catch 116 | { 117 | if (Debug.Enabled()) 118 | Terminal.Debug($"Couldn't execute setup for dependency: {dependency.Name}"); 119 | success = false; 120 | } 121 | } 122 | return success; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Launcher/Utils/Game.cs: -------------------------------------------------------------------------------- 1 | using CSGSI; 2 | using CSGSI.Nodes; 3 | using System.Diagnostics; 4 | using System.Net.NetworkInformation; 5 | 6 | namespace Launcher.Utils 7 | { 8 | public static class Game 9 | { 10 | private static Process? _process; 11 | private static GameStateListener? _listener; 12 | private static int _port; 13 | private static MapNode? _node; 14 | 15 | private static string _map = "main_menu"; 16 | private static int _scoreCT = 0; 17 | private static int _scoreT = 0; 18 | 19 | public static async Task Launch() 20 | { 21 | List arguments = Argument.GenerateGameArguments(); 22 | if (arguments.Count > 0) Terminal.Print($"Arguments: {string.Join(" ", arguments)}"); 23 | 24 | string directory = Directory.GetCurrentDirectory(); 25 | Terminal.Print($"Directory: {directory}"); 26 | 27 | string gameStatePath = $"{directory}/csgo/cfg/gamestate_integration_cc.cfg"; 28 | 29 | if (!Argument.Exists("--disable-rpc")) 30 | { 31 | _port = GeneratePort(); 32 | 33 | if (Argument.Exists("--debug-mode")) 34 | Terminal.Debug($"Starting Game State Integration with TCP port {_port}."); 35 | 36 | _listener = new($"http://localhost:{_port}/"); 37 | _listener.NewGameState += OnNewGameState; 38 | _listener.Start(); 39 | 40 | await File.WriteAllTextAsync(gameStatePath, 41 | @"""ClassicCounter"" 42 | { 43 | ""uri"" ""http://localhost:" + _port + @""" 44 | ""timeout"" ""5.0"" 45 | ""auth"" 46 | { 47 | ""token"" """ + $"ClassicCounter {Version.Current}" + @""" 48 | } 49 | ""data"" 50 | { 51 | ""provider"" ""1"" 52 | ""map"" ""1"" 53 | ""round"" ""1"" 54 | ""player_id"" ""1"" 55 | ""player_weapons"" ""1"" 56 | ""player_match_stats"" ""1"" 57 | ""player_state"" ""1"" 58 | ""allplayers_id"" ""1"" 59 | ""allplayers_state"" ""1"" 60 | ""allplayers_match_stats"" ""1"" 61 | } 62 | }" 63 | ); 64 | } 65 | else if (File.Exists(gameStatePath)) File.Delete(gameStatePath); 66 | 67 | _process = new Process(); 68 | bool useGC = Argument.Exists("--gc"); 69 | 70 | if (Argument.Exists("--debug-mode")) 71 | Terminal.Debug($"Launching the game with{(useGC ? "" : "out")} Game Coordinator..."); 72 | 73 | string gameExe = useGC ? "cc.exe" : "csgo.exe"; 74 | _process.StartInfo.FileName = $"{directory}\\{gameExe}"; 75 | _process.StartInfo.Arguments = string.Join(" ", arguments); 76 | 77 | return _process.Start(); 78 | } 79 | 80 | public static async Task Monitor() 81 | { 82 | while (true) 83 | { 84 | if (_process == null) 85 | break; 86 | 87 | try 88 | { 89 | Process.GetProcessById(_process.Id); 90 | } 91 | catch 92 | { 93 | Environment.Exit(1); 94 | } 95 | 96 | if (_node != null && _node.Name.Trim().Length != 0) 97 | { 98 | if (_map != _node.Name) 99 | { 100 | _map = _node.Name; 101 | _scoreCT = _node.TeamCT.Score; 102 | _scoreT = _node.TeamT.Score; 103 | 104 | Discord.SetDetails(_map); 105 | Discord.SetState($"Score → {_scoreCT}:{_scoreT}"); 106 | Discord.SetTimestamp(DateTime.UtcNow); 107 | Discord.SetLargeArtwork($"https://assets.classiccounter.cc/maps/default/{_map}.jpg"); 108 | Discord.SetSmallArtwork("icon"); 109 | Discord.Update(); 110 | } 111 | 112 | if (_scoreCT != _node.TeamCT.Score || _scoreT != _node.TeamT.Score) 113 | { 114 | _scoreCT = _node.TeamCT.Score; 115 | _scoreT = _node.TeamT.Score; 116 | 117 | Discord.SetState($"Score → {_scoreCT}:{_scoreT}"); 118 | Discord.Update(); 119 | } 120 | } 121 | else if (_map != "main_menu") 122 | { 123 | _map = "main_menu"; 124 | _scoreCT = 0; 125 | _scoreT = 0; 126 | 127 | Discord.SetDetails("In Main Menu"); 128 | Discord.SetState(null); 129 | Discord.SetTimestamp(DateTime.UtcNow); 130 | Discord.SetLargeArtwork("icon"); 131 | Discord.SetSmallArtwork(null); 132 | Discord.Update(); 133 | } 134 | 135 | await Task.Delay(2000); 136 | } 137 | } 138 | 139 | private static int GeneratePort() 140 | { 141 | int port = new Random().Next(1024, 65536); 142 | 143 | IPGlobalProperties properties = IPGlobalProperties.GetIPGlobalProperties(); 144 | while (properties.GetActiveTcpConnections().Any(x => x.LocalEndPoint.Port == port)) 145 | { 146 | port = new Random().Next(1024, 65536); 147 | } 148 | 149 | return port; 150 | } 151 | 152 | public static void OnNewGameState(GameState gs) => _node = gs.Map; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Launcher/Utils/Patch.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System.Security.Cryptography; 4 | 5 | namespace Launcher.Utils 6 | { 7 | public class Patch 8 | { 9 | [JsonProperty(PropertyName = "file")] 10 | public required string File { get; set; } 11 | 12 | [JsonProperty(PropertyName = "hash")] 13 | public required string Hash { get; set; } 14 | }; 15 | 16 | public class Patches(bool success, List missing, List outdated) 17 | { 18 | public bool Success = success; 19 | public List Missing = missing; 20 | public List Outdated = outdated; 21 | } 22 | 23 | public static class PatchManager 24 | { 25 | private static string GetOriginalFileName(string fileName) 26 | { 27 | return fileName.EndsWith(".7z") ? fileName[..^3] : fileName; 28 | } 29 | 30 | private static async Task> GetPatches(bool validateAll = false) 31 | { 32 | List patches = new List(); 33 | 34 | try 35 | { 36 | string responseString = await Api.ClassicCounter.GetPatches(); 37 | 38 | JObject responseJson = JObject.Parse(responseString); 39 | 40 | if (responseJson["files"] != null) 41 | patches = responseJson["files"]!.ToObject()!.ToList(); 42 | } 43 | catch 44 | { 45 | if (Debug.Enabled()) 46 | Terminal.Debug($"Couldn't get {(validateAll ? "full game" : "patch")} API data."); 47 | } 48 | 49 | return patches; 50 | } 51 | 52 | private static async Task GetHash(string filePath) 53 | { 54 | MD5 md5 = MD5.Create(); 55 | 56 | byte[] buffer = await File.ReadAllBytesAsync(filePath); 57 | byte[] hash = md5.ComputeHash(buffer, 0, buffer.Length); 58 | 59 | return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); 60 | } 61 | 62 | public static async Task ValidatePatches(bool validateAll = false) 63 | { 64 | List patches = await GetPatches(validateAll); 65 | List missing = new(); 66 | List outdated = new(); 67 | Patch? dirPatch = null; 68 | 69 | // first only check pak_dat.vpk 70 | var pakDatPatch = patches.FirstOrDefault(p => p.File == "csgo/pak_dat.vpk"); 71 | bool skipValidation = false; 72 | 73 | if (pakDatPatch != null && !validateAll) 74 | { 75 | string pakDatPath = $"{Directory.GetCurrentDirectory()}/csgo/pak_dat.vpk"; 76 | 77 | if (Debug.Enabled()) 78 | Terminal.Debug("Checking csgo/pak_dat.vpk first..."); 79 | 80 | if (File.Exists(pakDatPath)) 81 | { 82 | if (Debug.Enabled()) 83 | Terminal.Debug("Checking hash for: csgo/pak_dat.vpk"); 84 | 85 | string pakDatHash = await GetHash(pakDatPath); 86 | if (pakDatHash == pakDatPatch.Hash) 87 | { 88 | if (Debug.Enabled()) 89 | Terminal.Debug("csgo/pak_dat.vpk is up to date - skipping other file checks"); 90 | skipValidation = true; 91 | return new Patches(true, missing, outdated); 92 | } 93 | else 94 | { 95 | if (Debug.Enabled()) 96 | Terminal.Debug("csgo/pak_dat.vpk is outdated - will check all files"); 97 | File.Delete(pakDatPath); 98 | } 99 | } 100 | else 101 | { 102 | if (Debug.Enabled()) 103 | Terminal.Debug("Missing: csgo/pak_dat.vpk - will check all files"); 104 | } 105 | } 106 | 107 | if (!skipValidation) 108 | { 109 | // find pak01_dir.vpk from patch api 110 | dirPatch = patches.FirstOrDefault(p => p.File.Contains("pak01_dir.vpk")); 111 | bool needPak01Update = false; 112 | 113 | if (dirPatch != null) 114 | { 115 | string dirPath = $"{Directory.GetCurrentDirectory()}/csgo/pak01_dir.vpk"; 116 | 117 | if (Debug.Enabled()) 118 | Terminal.Debug("Checking csgo/pak01_dir.vpk first..."); 119 | 120 | if (File.Exists(dirPath)) 121 | { 122 | if (Debug.Enabled()) 123 | Terminal.Debug("Checking hash for: csgo/pak01_dir.vpk"); 124 | 125 | string dirHash = await GetHash(dirPath); 126 | if (dirHash != dirPatch.Hash) 127 | { 128 | if (Debug.Enabled()) 129 | Terminal.Debug("csgo/pak01_dir.vpk is outdated!"); 130 | 131 | File.Delete(dirPath); 132 | outdated.Add(dirPatch); 133 | needPak01Update = true; 134 | } 135 | else if (!Argument.Exists("--validate-all")) 136 | { 137 | if (Debug.Enabled()) 138 | Terminal.Debug("csgo/pak01_dir.vpk is up to date - will skip pak01 files"); 139 | } 140 | else 141 | { 142 | if (Debug.Enabled()) 143 | Terminal.Debug("csgo/pak01_dir.vpk is up to date - checking all files anyway due to --validate-all"); 144 | } 145 | } 146 | else 147 | { 148 | if (Debug.Enabled()) 149 | Terminal.Debug("Missing: csgo/pak01_dir.vpk!"); 150 | 151 | missing.Add(dirPatch); 152 | needPak01Update = true; 153 | } 154 | 155 | if (needPak01Update) 156 | { 157 | patches.Remove(dirPatch); 158 | } 159 | } 160 | 161 | foreach (Patch patch in patches) 162 | { 163 | string originalFileName = GetOriginalFileName(patch.File); 164 | 165 | // skip dir file (we already checked it) 166 | if (originalFileName.Contains("pak01_dir.vpk")) 167 | continue; 168 | 169 | // are you a pak01 file? 170 | bool isPak01File = originalFileName.Contains("pak01_"); 171 | string path = $"{Directory.GetCurrentDirectory()}/{originalFileName}"; 172 | 173 | if (isPak01File && !needPak01Update && !Argument.Exists("--validate-all")) 174 | { 175 | if (!File.Exists(path)) 176 | { 177 | if (Debug.Enabled()) 178 | Terminal.Debug($"Missing: {originalFileName}"); 179 | 180 | missing.Add(patch); 181 | continue; 182 | } 183 | 184 | if (Debug.Enabled()) 185 | Terminal.Debug($"Skipping hash check for: {originalFileName} (pak01_dir.vpk up to date)"); 186 | 187 | continue; 188 | } 189 | 190 | if (!File.Exists(path)) 191 | { 192 | if (Debug.Enabled()) 193 | Terminal.Debug($"Missing: {originalFileName}"); 194 | 195 | missing.Add(patch); 196 | continue; 197 | } 198 | 199 | if (Debug.Enabled()) 200 | Terminal.Debug($"Checking hash for: {originalFileName}{(isPak01File && Argument.Exists("--validate-all") ? " (--validate-all)" : "")}"); 201 | 202 | string hash = await GetHash(path); 203 | if (hash != patch.Hash) 204 | { 205 | if (Debug.Enabled()) 206 | Terminal.Debug($"Outdated: {originalFileName}"); 207 | 208 | File.Delete(path); 209 | outdated.Add(patch); 210 | } 211 | } 212 | 213 | // if pak01_dir.vpk needs update, move it to end of lists 214 | if (needPak01Update && dirPatch != null) 215 | { 216 | if (outdated.Remove(dirPatch)) 217 | outdated.Add(dirPatch); 218 | if (missing.Remove(dirPatch)) 219 | missing.Add(dirPatch); 220 | } 221 | } 222 | 223 | return new Patches(patches.Count > 0, missing, outdated); 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | launchSettings.json 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # Tye 67 | .tye/ 68 | 69 | # ASP.NET Scaffolding 70 | ScaffoldingReadMe.txt 71 | 72 | # StyleCop 73 | StyleCopReport.xml 74 | 75 | # Files built by Visual Studio 76 | *_i.c 77 | *_p.c 78 | *_h.h 79 | *.ilk 80 | *.meta 81 | *.obj 82 | *.iobj 83 | *.pch 84 | *.pdb 85 | *.ipdb 86 | *.pgc 87 | *.pgd 88 | *.rsp 89 | *.sbr 90 | *.tlb 91 | *.tli 92 | *.tlh 93 | *.tmp 94 | *.tmp_proj 95 | *_wpftmp.csproj 96 | *.log 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio LightSwitch build output 301 | **/*.HTMLClient/GeneratedArtifacts 302 | **/*.DesktopClient/GeneratedArtifacts 303 | **/*.DesktopClient/ModelManifest.xml 304 | **/*.Server/GeneratedArtifacts 305 | **/*.Server/ModelManifest.xml 306 | _Pvt_Extensions 307 | 308 | # Paket dependency manager 309 | .paket/paket.exe 310 | paket-files/ 311 | 312 | # FAKE - F# Make 313 | .fake/ 314 | 315 | # CodeRush personal settings 316 | .cr/personal 317 | 318 | # Python Tools for Visual Studio (PTVS) 319 | __pycache__/ 320 | *.pyc 321 | 322 | # Cake - Uncomment if you are using it 323 | # tools/** 324 | # !tools/packages.config 325 | 326 | # Tabs Studio 327 | *.tss 328 | 329 | # Telerik's JustMock configuration file 330 | *.jmconfig 331 | 332 | # BizTalk build output 333 | *.btp.cs 334 | *.btm.cs 335 | *.odx.cs 336 | *.xsd.cs 337 | 338 | # OpenCover UI analysis results 339 | OpenCover/ 340 | 341 | # Azure Stream Analytics local run output 342 | ASALocalRun/ 343 | 344 | # MSBuild Binary and Structured Log 345 | *.binlog 346 | 347 | # NVidia Nsight GPU debugger configuration file 348 | *.nvuser 349 | 350 | # MFractors (Xamarin productivity tool) working folder 351 | .mfractor/ 352 | 353 | # Local History for Visual Studio 354 | .localhistory/ 355 | 356 | # BeatPulse healthcheck temp database 357 | healthchecksdb 358 | 359 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 360 | MigrationBackup/ 361 | 362 | # Ionide (cross platform F# VS Code tools) working folder 363 | .ionide/ 364 | 365 | # Fody - auto-generated XML schema 366 | FodyWeavers.xsd 367 | 368 | ## 369 | ## Visual studio for Mac 370 | ## 371 | 372 | 373 | # globs 374 | Makefile.in 375 | *.userprefs 376 | *.usertasks 377 | config.make 378 | config.status 379 | aclocal.m4 380 | install-sh 381 | autom4te.cache/ 382 | *.tar.gz 383 | tarballs/ 384 | test-results/ 385 | 386 | # Mac bundle stuff 387 | *.dmg 388 | *.app 389 | 390 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 391 | # General 392 | .DS_Store 393 | .AppleDouble 394 | .LSOverride 395 | 396 | # Icon must end with two \r 397 | Icon 398 | 399 | 400 | # Thumbnails 401 | ._* 402 | 403 | # Files that might appear in the root of a volume 404 | .DocumentRevisions-V100 405 | .fseventsd 406 | .Spotlight-V100 407 | .TemporaryItems 408 | .Trashes 409 | .VolumeIcon.icns 410 | .com.apple.timemachine.donotpresent 411 | 412 | # Directories potentially created on remote AFP share 413 | .AppleDB 414 | .AppleDesktop 415 | Network Trash Folder 416 | Temporary Items 417 | .apdisk 418 | 419 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 420 | # Windows thumbnail cache files 421 | Thumbs.db 422 | ehthumbs.db 423 | ehthumbs_vista.db 424 | 425 | # Dump file 426 | *.stackdump 427 | 428 | # Folder config file 429 | [Dd]esktop.ini 430 | 431 | # Recycle Bin used on file shares 432 | $RECYCLE.BIN/ 433 | 434 | # Windows Installer files 435 | *.cab 436 | *.msi 437 | *.msix 438 | *.msm 439 | *.msp 440 | 441 | # Windows shortcuts 442 | *.lnk 443 | 444 | # JetBrains Rider 445 | .idea/ 446 | *.sln.iml 447 | 448 | ## 449 | ## Visual Studio Code 450 | ## 451 | .vscode/* 452 | !.vscode/settings.json 453 | !.vscode/tasks.json 454 | !.vscode/launch.json 455 | !.vscode/extensions.json 456 | -------------------------------------------------------------------------------- /Launcher/Program.cs: -------------------------------------------------------------------------------- 1 | using Launcher.Utils; 2 | using Spectre.Console; 3 | using System.Diagnostics; 4 | 5 | using Debug = Launcher.Utils.Debug; 6 | using Version = Launcher.Utils.Version; 7 | 8 | Console.Clear(); 9 | 10 | if (Debug.Enabled()) 11 | Terminal.Debug("Cleaning up any .7z files at startup..."); 12 | DownloadManager.Cleanup7zFiles(); 13 | 14 | if (!Argument.Exists("--disable-rpc")) 15 | Discord.Init(); 16 | 17 | Terminal.Init(); 18 | 19 | await Task.Delay(1000); 20 | 21 | // this int saves total download progress (duh) 22 | // its used for whenever HandlePatches is called so that the display for downloading files doesnt glitch the fuck out 23 | 24 | // TODO: Remove this crap ^ and come up with something else 25 | int totalDownloadProgress = 0; 26 | 27 | string updaterPath = $"{Directory.GetCurrentDirectory()}/updater.exe"; 28 | if (File.Exists(updaterPath)) 29 | { 30 | if (Debug.Enabled()) 31 | Terminal.Debug("Found and deleting: updater.exe"); 32 | 33 | try 34 | { 35 | File.Delete(updaterPath); 36 | } 37 | catch 38 | { 39 | if (Debug.Enabled()) 40 | Terminal.Debug("Couldn't delete updater.exe, possibly due to missing permissions."); 41 | } 42 | } 43 | 44 | if (!Argument.Exists("--skip-updates")) 45 | { 46 | string latestVersion = await Version.GetLatestVersion(); 47 | 48 | if (Version.Current != latestVersion) 49 | { 50 | Terminal.Print("You're using an outdated launcher - updating..."); 51 | await AnsiConsole 52 | .Status() 53 | .SpinnerStyle(Style.Parse("gray")) 54 | .StartAsync("Downloading auto-updater.", async ctx => 55 | { 56 | try 57 | { 58 | await DownloadManager.DownloadUpdater(updaterPath); 59 | 60 | if (!File.Exists(updaterPath)) 61 | { 62 | if (Debug.Enabled()) 63 | Terminal.Debug("Updater.exe that was just downloaded doesn't exist, possibly due to missing permissions."); 64 | 65 | return; 66 | } 67 | 68 | Process updaterProcess = new Process(); 69 | updaterProcess.StartInfo.FileName = updaterPath; 70 | updaterProcess.StartInfo.Arguments = $"--version={latestVersion} {string.Join(" ", Argument.GenerateGameArguments(true))}"; 71 | updaterProcess.Start(); 72 | } 73 | catch 74 | { 75 | Terminal.Error("Couldn't download or launch auto-updater. Closing launcher in 5 seconds."); 76 | await Task.Delay(5000); 77 | } 78 | 79 | Environment.Exit(1); 80 | }); 81 | } 82 | else 83 | Terminal.Success("Launcher is up-to-date!"); 84 | } 85 | 86 | string directory = Directory.GetCurrentDirectory(); 87 | 88 | Dependencies dependenciesToInstall = new Dependencies(false, new List(), new List()); 89 | 90 | if (Argument.Exists("--install-dependencies")) 91 | { 92 | bool success = new Boolean(); 93 | Terminal.Print("Forcing downloading and installing dependencies..."); 94 | await AnsiConsole 95 | .Status() 96 | .SpinnerStyle(Style.Parse("blue")) 97 | .StartAsync("Downloading dependencies...", async ctx => 98 | { 99 | List dependencies = await DependencyManager.Get(); 100 | dependenciesToInstall = await DownloadManager.DownloadDependencies(ctx, dependencies); 101 | }); 102 | 103 | if (!dependenciesToInstall.Success) await AnsiConsole 104 | .Status() 105 | .SpinnerStyle(Style.Parse("green")) 106 | .StartAsync("Installing dependencies...", async ctx => 107 | { 108 | success = await DependencyManager.Install(ctx, dependenciesToInstall); 109 | }); 110 | if (success) 111 | { 112 | Terminal.Success("Finished dependency installing! Closing launcher."); 113 | Terminal.SteamHappy(); 114 | } 115 | else 116 | Terminal.Error("No dependencies were installed. Closing launcher."); 117 | await Task.Delay(3000); 118 | Environment.Exit(0); 119 | return; 120 | } 121 | 122 | if (!File.Exists($"{directory}/csgo.exe")) 123 | { 124 | // if there's a .7z.001 file, start downloading 125 | if (Directory.GetFiles(directory, "*.7z.001").Length > 0) 126 | { 127 | await AnsiConsole 128 | .Status() 129 | .SpinnerStyle(Style.Parse("gray")) 130 | .StartAsync("Downloading full game...", async ctx => 131 | { 132 | await DownloadManager.DownloadFullGame(ctx); 133 | }); 134 | } 135 | else 136 | { 137 | Terminal.Error("(!) csgo.exe not found in the current directory!"); 138 | Terminal.Warning($"Game files will be installed to: {directory}"); 139 | Terminal.Warning("This will download approximately 7GB of data. Make sure you have enough disk space."); 140 | AnsiConsole.Markup($"[orange1]Classic[/][blue]Counter[/] [grey50]|[/] [grey82]Would you like to download the full game? (y/n): [/]"); 141 | var response = Console.ReadKey(true); 142 | Console.WriteLine(response.KeyChar); 143 | Console.WriteLine(); 144 | 145 | if (char.ToLower(response.KeyChar) == 'y') 146 | { 147 | string? rootPath = Path.GetPathRoot(directory); 148 | if (rootPath == null) 149 | { 150 | Terminal.Error("Could not determine drive root path!"); 151 | return; 152 | } 153 | 154 | DriveInfo driveInfo = new DriveInfo(rootPath); 155 | long requiredSpace = 24L * 1024 * 1024 * 1024; // 24 GB in bytes 156 | 157 | if (driveInfo.AvailableFreeSpace < requiredSpace) 158 | { 159 | Terminal.Error("(!) Not enough disk space available!"); 160 | Terminal.Error($"Required: 24 GB, Available: {driveInfo.AvailableFreeSpace / (1024.0 * 1024 * 1024):F2} GB"); 161 | Terminal.Error("Please free up some disk space and try again. Closing launcher in 10 seconds..."); 162 | await Task.Delay(10000); 163 | Environment.Exit(1); 164 | return; 165 | } 166 | 167 | await AnsiConsole 168 | .Status() 169 | .SpinnerStyle(Style.Parse("gray")) 170 | .StartAsync("Downloading full game...", async ctx => 171 | { 172 | await DownloadManager.DownloadFullGame(ctx); 173 | }); 174 | 175 | bool success = false; 176 | 177 | await AnsiConsole 178 | .Status() 179 | .SpinnerStyle(Style.Parse("blue")) 180 | .StartAsync("Downloading dependencies...", async ctx => 181 | { 182 | List dependencies = await DependencyManager.Get(); 183 | dependenciesToInstall = await DownloadManager.DownloadDependencies(ctx, dependencies); 184 | }); 185 | 186 | if (dependenciesToInstall != null) await AnsiConsole 187 | .Status() 188 | .SpinnerStyle(Style.Parse("green")) 189 | .StartAsync("Installing dependencies...", async ctx => 190 | { 191 | success = await DependencyManager.Install(ctx, dependenciesToInstall); 192 | }); 193 | if (success) 194 | { 195 | Terminal.Success("Finished dependency installing!"); 196 | } 197 | else 198 | Terminal.Error("No dependencies were installed."); 199 | } 200 | else 201 | { 202 | Terminal.Error("Game files are required to run ClassicCounter. Closing launcher in 10 seconds..."); 203 | await Task.Delay(10000); 204 | Environment.Exit(1); 205 | } 206 | } 207 | } 208 | 209 | if (!Argument.Exists("--skip-validating")) 210 | { 211 | await AnsiConsole 212 | .Status() 213 | .SpinnerStyle(Style.Parse("gray")) 214 | .StartAsync("Validating files...", async ctx => 215 | { 216 | bool validateAll = Argument.Exists("--validate-all"); 217 | 218 | if (validateAll) 219 | { 220 | // First validate all game files 221 | ctx.Status = "Validating game files..."; 222 | Patches gameFiles = await PatchManager.ValidatePatches(true); 223 | if (gameFiles.Success) 224 | { 225 | Terminal.Print("Finished validating game files!"); 226 | if (gameFiles.Missing.Count > 0 || gameFiles.Outdated.Count > 0) 227 | { 228 | if (gameFiles.Missing.Count > 0) 229 | Terminal.Warning($"Found {gameFiles.Missing.Count} missing {(gameFiles.Missing.Count == 1 ? "game file" : "game files")}!"); 230 | 231 | if (gameFiles.Outdated.Count > 0) 232 | Terminal.Warning($"Found {gameFiles.Outdated.Count} outdated {(gameFiles.Outdated.Count == 1 ? "game file" : "game files")}!"); 233 | 234 | Terminal.Print("If you're stuck at downloading - reopen the launcher."); 235 | 236 | await DownloadManager.HandlePatches(gameFiles, ctx, true, totalDownloadProgress); 237 | totalDownloadProgress += gameFiles.Missing.Count + gameFiles.Outdated.Count; 238 | } 239 | else 240 | { 241 | Terminal.Success("Game files are up-to-date!"); 242 | } 243 | } 244 | else 245 | { 246 | Terminal.Error("(!) Couldn't validate game files!"); 247 | Terminal.Error("(!) Is your ISP blocking CloudFlare? Check your Network settings."); 248 | return; 249 | } 250 | 251 | // Then validate patches 252 | ctx.Status = "Validating patches..."; 253 | Terminal.Print("\nNow checking for new patches..."); 254 | } 255 | 256 | // Regular patch validation 257 | Patches patches = await PatchManager.ValidatePatches(false); 258 | if (patches.Success) 259 | { 260 | Terminal.Print("Finished validating patches!"); 261 | if (patches.Missing.Count == 0 && patches.Outdated.Count == 0) 262 | { 263 | Terminal.Success("Patches are up-to-date!"); 264 | return; 265 | } 266 | } 267 | else 268 | { 269 | Terminal.Error("(!) Couldn't validate patches!"); 270 | Terminal.Error("(!) Is your ISP blocking CloudFlare? Check your Network settings."); 271 | if (!Argument.Exists("--patch-only")) 272 | { 273 | Terminal.Warning("Launching ClassicCounter anyways..."); 274 | } 275 | return; 276 | } 277 | 278 | if (patches.Missing.Count > 0) 279 | Terminal.Warning($"Found {patches.Missing.Count} missing {(patches.Missing.Count == 1 ? "patch" : "patches")}!"); 280 | 281 | if (patches.Outdated.Count > 0) 282 | Terminal.Warning($"Found {patches.Outdated.Count} outdated {(patches.Outdated.Count == 1 ? "patch" : "patches")}!"); 283 | 284 | Terminal.Print("If you're stuck at downloading patches - reopen the launcher."); 285 | 286 | await DownloadManager.HandlePatches(patches, ctx, false, totalDownloadProgress); 287 | totalDownloadProgress += patches.Missing.Count + patches.Outdated.Count; 288 | 289 | // Cleanup temporary files 290 | if (Debug.Enabled()) 291 | Terminal.Debug("Cleaning up temporary files..."); 292 | 293 | try 294 | { 295 | // Try to delete the 7z.dll 296 | string launcherDllPath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath) ?? "", "7z.dll"); 297 | if (File.Exists(launcherDllPath)) 298 | { 299 | try 300 | { 301 | File.Delete(launcherDllPath); 302 | if (Debug.Enabled()) 303 | Terminal.Debug($"Deleted 7z.dll: {launcherDllPath}"); 304 | } 305 | catch (Exception ex) 306 | { 307 | if (Debug.Enabled()) 308 | Terminal.Debug($"Failed to delete 7z.dll: {ex.Message}"); 309 | } 310 | } 311 | } 312 | catch (Exception ex) 313 | { 314 | if (Debug.Enabled()) 315 | Terminal.Debug($"Cleanup failed: {ex.Message}"); 316 | } 317 | }); 318 | } 319 | 320 | if (Argument.Exists("--patch-only")) 321 | { 322 | Terminal.Success("Finished patch validation and downloads! Closing launcher."); 323 | await Task.Delay(3000); 324 | Environment.Exit(0); 325 | return; 326 | } 327 | 328 | if (Debug.Enabled()) 329 | Terminal.Debug("Cleaning up any .7z files..."); 330 | DownloadManager.Cleanup7zFiles(); 331 | 332 | bool launched = await Game.Launch(); 333 | if (!launched) 334 | { 335 | Terminal.Error("ClassicCounter didn't launch properly. Make sure launcher.exe and csgo.exe are in the same directory. Closing launcher in 10 seconds."); 336 | await Task.Delay(10000); 337 | } 338 | else if (Argument.Exists("--disable-rpc")) 339 | { 340 | Terminal.Success("Launched ClassicCounter! Closing launcher in 5 seconds."); 341 | await Task.Delay(5000); 342 | } 343 | else 344 | { 345 | Terminal.Success("Launched ClassicCounter! Launcher will minimize in 5 seconds to manage Discord RPC."); 346 | await Task.Delay(5000); 347 | 348 | ConsoleManager.HideConsole(); 349 | Discord.SetDetails("In Main Menu"); 350 | Discord.Update(); 351 | await Game.Monitor(); 352 | } -------------------------------------------------------------------------------- /Launcher/Utils/Download.cs: -------------------------------------------------------------------------------- 1 | using Downloader; 2 | using Refit; 3 | using Spectre.Console; 4 | using System.Diagnostics; 5 | 6 | namespace Launcher.Utils 7 | { 8 | public static class DownloadManager 9 | { 10 | private static DownloadConfiguration _settings = new() 11 | { 12 | ChunkCount = 8, 13 | ParallelDownload = true 14 | }; 15 | private static DownloadService _downloader = new DownloadService(_settings); 16 | 17 | public static async Task DownloadUpdater(string path) 18 | { 19 | await _downloader.DownloadFileTaskAsync( 20 | $"https://github.com/ClassicCounter/updater/releases/download/updater/updater.exe", 21 | path 22 | ); 23 | } 24 | 25 | public static async Task DownloadDependencies(StatusContext ctx, List dependencies) 26 | { 27 | List local = new List(); 28 | List remote = new List(); 29 | Dependencies? _dependencies; 30 | foreach (var dependency in dependencies) 31 | { 32 | if (!DependencyManager.IsInstalled(ctx, dependency)) 33 | { 34 | if (dependency.URL != null) 35 | { 36 | string path = Directory.GetCurrentDirectory() + dependency.Path; 37 | if (File.Exists(path)) 38 | File.Delete(path); 39 | if (Debug.Enabled()) 40 | Terminal.Debug($"Downloading {dependency.Name}"); 41 | await _downloader.DownloadFileTaskAsync( 42 | $"{dependency.URL}", 43 | $"{Directory.GetCurrentDirectory()}{dependency.Path}"); 44 | remote.Add(dependency); 45 | } 46 | else 47 | { 48 | local.Add(dependency); 49 | } 50 | } 51 | } 52 | _dependencies = new Dependencies(false, local, remote); 53 | return _dependencies; 54 | } 55 | 56 | public static async Task DownloadPatch( 57 | Patch patch, 58 | bool validateAll = false, 59 | Action? onProgress = null, 60 | Action? onExtract = null) 61 | { 62 | string originalFileName = patch.File.EndsWith(".7z") ? patch.File[..^3] : patch.File; 63 | string downloadPath = $"{Directory.GetCurrentDirectory()}/{patch.File}"; 64 | 65 | if (Debug.Enabled()) 66 | Terminal.Debug($"Starting download of: {patch.File}"); 67 | 68 | if (patch.File.EndsWith(".7z") && File.Exists(downloadPath)) 69 | { 70 | try 71 | { 72 | if (Debug.Enabled()) 73 | Terminal.Debug($"Found existing .7z file, trying to delete: {downloadPath}"); 74 | File.Delete(downloadPath); 75 | } 76 | catch (Exception ex) 77 | { 78 | if (Debug.Enabled()) 79 | Terminal.Debug($"Failed to delete existing .7z file: {ex.Message}"); 80 | } 81 | } 82 | 83 | string baseUrl = "https://patch.classiccounter.cc"; 84 | 85 | if (onProgress != null) 86 | { 87 | EventHandler progressHandler = (sender, e) => onProgress(e); 88 | _downloader.DownloadProgressChanged += progressHandler; 89 | try 90 | { 91 | await _downloader.DownloadFileTaskAsync( 92 | $"{baseUrl}/{patch.File}", 93 | $"{Directory.GetCurrentDirectory()}/{patch.File}" 94 | ); 95 | } 96 | finally 97 | { 98 | _downloader.DownloadProgressChanged -= progressHandler; 99 | } 100 | } 101 | else 102 | { 103 | await _downloader.DownloadFileTaskAsync( 104 | $"{baseUrl}/{patch.File}", 105 | $"{Directory.GetCurrentDirectory()}/{patch.File}" 106 | ); 107 | } 108 | 109 | if (patch.File.EndsWith(".7z")) 110 | { 111 | if (Debug.Enabled()) 112 | Terminal.Debug($"Download complete, starting extraction of: {patch.File}"); 113 | onExtract?.Invoke(); // for "extracting" status 114 | string extractPath = $"{Directory.GetCurrentDirectory()}/{originalFileName}"; 115 | await Extract7z(downloadPath, extractPath); 116 | } 117 | } 118 | 119 | public static async Task HandlePatches(Patches patches, StatusContext ctx, bool isGameFiles, int startingProgress = 0) 120 | { 121 | string fileType = isGameFiles ? "game file" : "patch"; 122 | string fileTypePlural = isGameFiles ? "game files" : "patches"; 123 | 124 | var allFiles = patches.Missing.Concat(patches.Outdated).ToList(); 125 | int totalFiles = allFiles.Count; 126 | int completedFiles = startingProgress; 127 | int failedFiles = 0; 128 | 129 | // status update 130 | Action updateStatus = (progress, filename) => 131 | { 132 | var speed = progress.BytesPerSecondSpeed / (1024.0 * 1024.0); 133 | var progressText = $"{((float)completedFiles / totalFiles * 100):F1}% ({completedFiles}/{totalFiles})"; 134 | var status = filename.EndsWith(".7z") && progress.ProgressPercentage >= 100 ? "Extracting" : "Downloading new"; 135 | ctx.Status = $"{status} {fileTypePlural}{GetDots().PadRight(3)} [gray]|[/] {progressText} [gray]|[/] {GetProgressBar(progress.ProgressPercentage)} {progress.ProgressPercentage:F1}% [gray]|[/] {speed:F1} MB/s"; 136 | }; 137 | 138 | foreach (var patch in allFiles) 139 | { 140 | try 141 | { 142 | await DownloadPatch(patch, isGameFiles, progress => updateStatus(progress, patch.File)); 143 | completedFiles++; 144 | } 145 | catch 146 | { 147 | failedFiles++; 148 | Terminal.Warning($"Couldn't process {fileType}: {patch.File}, possibly due to missing permissions."); 149 | } 150 | } 151 | 152 | if (failedFiles > 0) 153 | Terminal.Warning($"Couldn't download {failedFiles} {(failedFiles == 1 ? fileType : fileTypePlural)}!"); 154 | } 155 | 156 | public static async Task DownloadFullGame(StatusContext ctx) 157 | { 158 | try 159 | { 160 | await Steam.GetRecentLoggedInSteamID(); 161 | if (string.IsNullOrEmpty(Steam.recentSteamID2)) 162 | { 163 | Terminal.Error("Steam does not seem to be installed. Please make sure that you have Steam installed."); 164 | Terminal.Error("Closing launcher in 5 seconds..."); 165 | await Task.Delay(5000); 166 | Environment.Exit(1); 167 | return; 168 | } 169 | 170 | // pass steam id to api 171 | var gameFiles = await Api.ClassicCounter.GetFullGameDownload(Steam.recentSteamID2); 172 | 173 | int totalFiles = gameFiles.Files.Count; 174 | int completedFiles = 0; 175 | List failedFiles = new List(); 176 | 177 | foreach (var file in gameFiles.Files) 178 | { 179 | string filePath = Path.Combine(Directory.GetCurrentDirectory(), file.File); 180 | bool needsDownload = true; 181 | 182 | if (File.Exists(filePath)) 183 | { 184 | string fileHash = CalculateMD5(filePath); 185 | if (fileHash.Equals(file.Hash, StringComparison.OrdinalIgnoreCase)) 186 | { 187 | needsDownload = false; 188 | completedFiles++; 189 | continue; 190 | } 191 | } 192 | 193 | if (needsDownload) 194 | { 195 | try 196 | { 197 | EventHandler progressHandler = (sender, e) => 198 | { 199 | var speed = e.BytesPerSecondSpeed / (1024.0 * 1024.0); 200 | var progressText = $"{((float)completedFiles / totalFiles * 100):F1}% ({completedFiles}/{totalFiles})"; 201 | ctx.Status = $"Downloading {file.File}{GetDots().PadRight(3)} [gray]|[/] {progressText} [gray]|[/] {GetProgressBar(e.ProgressPercentage)} {e.ProgressPercentage:F1}% [gray]|[/] {speed:F1} MB/s"; 202 | }; 203 | _downloader.DownloadProgressChanged += progressHandler; 204 | 205 | try 206 | { 207 | await _downloader.DownloadFileTaskAsync( 208 | file.Link, 209 | filePath 210 | ); 211 | 212 | string downloadedHash = CalculateMD5(filePath); 213 | if (!downloadedHash.Equals(file.Hash, StringComparison.OrdinalIgnoreCase)) 214 | { 215 | failedFiles.Add(file.File); 216 | Terminal.Error($"Hash mismatch for {file.File}"); 217 | continue; 218 | } 219 | 220 | completedFiles++; 221 | } 222 | finally 223 | { 224 | _downloader.DownloadProgressChanged -= progressHandler; 225 | } 226 | } 227 | catch (Exception ex) 228 | { 229 | failedFiles.Add(file.File); 230 | Terminal.Error($"Failed to download {file.File}: {ex.Message}"); 231 | } 232 | } 233 | } 234 | 235 | if (failedFiles.Count == 0) 236 | { 237 | string extractPath = Directory.GetCurrentDirectory(); 238 | string tempExtractPath = Path.Combine(extractPath, "ClassicCounter_temp"); 239 | 240 | // check for running 7za.exe processes 241 | var processes = Process.GetProcessesByName("7za"); 242 | if (processes.Length > 0) 243 | { 244 | if (Debug.Enabled()) 245 | Terminal.Debug("Found running 7za.exe process, waiting..."); 246 | 247 | // wait for existing 7za.exe to finish 248 | while (Process.GetProcessesByName("7za").Length > 0) 249 | { 250 | ctx.Status = "Found already running extraction. Waiting for it to complete..."; 251 | await Task.Delay(1000); 252 | } 253 | 254 | // this is just code from ExtractSplitArchive (the moving folder part) 255 | string classicCounterPath = Path.Combine(tempExtractPath, "ClassicCounter"); 256 | if (Directory.Exists(tempExtractPath) && Directory.Exists(classicCounterPath)) 257 | { 258 | // check if the directory has any contents 259 | if (Directory.GetFiles(classicCounterPath, "*.*", SearchOption.AllDirectories).Any()) 260 | { 261 | try 262 | { 263 | if (Debug.Enabled()) 264 | Terminal.Debug("Moving contents from ClassicCounter folder to root directory..."); 265 | 266 | foreach (string dirPath in Directory.GetDirectories(classicCounterPath, "*", SearchOption.AllDirectories)) 267 | { 268 | string newDirPath = dirPath.Replace(classicCounterPath, extractPath); 269 | Directory.CreateDirectory(newDirPath); 270 | } 271 | 272 | foreach (string filePath in Directory.GetFiles(classicCounterPath, "*.*", SearchOption.AllDirectories)) 273 | { 274 | string newFilePath = filePath.Replace(classicCounterPath, extractPath); 275 | 276 | // skip launcher.exe 277 | if (Path.GetFileName(filePath).Equals("launcher.exe", StringComparison.OrdinalIgnoreCase)) 278 | { 279 | if (Debug.Enabled()) 280 | Terminal.Debug("Skipping launcher.exe"); 281 | continue; 282 | } 283 | 284 | try 285 | { 286 | if (File.Exists(newFilePath)) 287 | { 288 | File.Delete(newFilePath); 289 | } 290 | File.Move(filePath, newFilePath); 291 | } 292 | catch (Exception ex) 293 | { 294 | Terminal.Warning($"Failed to move file {filePath}: {ex.Message}"); 295 | } 296 | } 297 | 298 | // cleanup temp directory 299 | try 300 | { 301 | Directory.Delete(tempExtractPath, true); 302 | if (Debug.Enabled()) 303 | Terminal.Debug("Deleted temporary extraction directory"); 304 | } 305 | catch (Exception ex) 306 | { 307 | Terminal.Warning($"Failed to cleanup temporary directory: {ex.Message}"); 308 | } 309 | 310 | // cleanup .7z.xxx files 311 | try 312 | { 313 | var splitArchiveFiles = Directory.GetFiles(extractPath, "*.7z.*") 314 | .Where(f => Path.GetFileName(f).StartsWith("ClassicCounter.7z.")); 315 | 316 | foreach (var file in splitArchiveFiles) 317 | { 318 | try 319 | { 320 | File.Delete(file); 321 | if (Debug.Enabled()) 322 | Terminal.Debug($"Deleted split archive file: {file}"); 323 | } 324 | catch (Exception ex) 325 | { 326 | Terminal.Warning($"Failed to delete split archive file {file}: {ex.Message}"); 327 | } 328 | } 329 | } 330 | catch (Exception ex) 331 | { 332 | Terminal.Warning($"Failed to cleanup some split archive files: {ex.Message}"); 333 | } 334 | } 335 | catch (Exception ex) 336 | { 337 | Terminal.Warning($"Some files may not have been moved correctly: {ex.Message}"); 338 | } 339 | } 340 | else if (Debug.Enabled()) 341 | { 342 | Terminal.Debug("ClassicCounter folder exists but is empty, skipping file movement"); 343 | } 344 | } 345 | else if (Debug.Enabled()) 346 | { 347 | Terminal.Debug("Temp directory or ClassicCounter folder not found, skipping file movement"); 348 | } 349 | 350 | Terminal.Success("Extraction finished! Closing launcher..."); 351 | Terminal.Warning("Make sure to run the launcher again if the game doesn't start afterwards."); 352 | ctx.Status = "Done!"; 353 | await Task.Delay(10000); 354 | Environment.Exit(0); 355 | } 356 | 357 | ctx.Status = "Extracting game files... Please do not close the launcher."; 358 | await ExtractSplitArchive(gameFiles.Files.Select(f => f.File).ToList()); 359 | Terminal.Success("Game files downloaded and extracted successfully!"); 360 | } 361 | else 362 | { 363 | Terminal.Error($"Failed to download {failedFiles.Count} files. Closing launcher in 5 seconds..."); 364 | await Task.Delay(5000); 365 | Environment.Exit(1); 366 | } 367 | } 368 | catch (ApiException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden) 369 | { 370 | Terminal.Error("You are not whitelisted on ClassicCounter! (https://classiccounter.cc/whitelist)"); 371 | Terminal.Error("If you are whitelisted, check if you have Steam installed & you're logged into the whitelisted account."); 372 | Terminal.Error("If you're still facing issues, use one of our other download links to download the game."); 373 | Terminal.Warning("Closing launcher in 10 seconds..."); 374 | await Task.Delay(10000); 375 | Environment.Exit(1); 376 | } 377 | catch (ApiException ex) 378 | { 379 | Terminal.Error($"Failed to get game files from API: {ex.Message}"); 380 | Terminal.Error("Closing launcher in 5 seconds..."); 381 | await Task.Delay(5000); 382 | Environment.Exit(1); 383 | } 384 | catch (Exception ex) 385 | { 386 | Terminal.Error($"An error occurred: {ex.Message}"); 387 | Terminal.Error("Closing launcher in 5 seconds..."); 388 | await Task.Delay(5000); 389 | Environment.Exit(1); 390 | } 391 | } 392 | 393 | private static string CalculateMD5(string filename) 394 | { 395 | using (var md5 = System.Security.Cryptography.MD5.Create()) 396 | using (var stream = File.OpenRead(filename)) 397 | { 398 | byte[] hash = md5.ComputeHash(stream); 399 | return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); 400 | } 401 | } 402 | 403 | // meant only for downloading whole game for now 404 | // todo maybe make it more modular/allow other functions to use this 405 | private static async Task ExtractSplitArchive(List files) 406 | { 407 | if (files == null || files.Count == 0) 408 | { 409 | throw new ArgumentException("No files provided for extraction"); 410 | } 411 | 412 | files.Sort(); 413 | 414 | if (Debug.Enabled()) 415 | { 416 | Terminal.Debug($"Starting extraction of split archive:"); 417 | foreach (var file in files) 418 | { 419 | Terminal.Debug($"Found part: {file}"); 420 | } 421 | } 422 | 423 | string firstFile = files[0]; 424 | string extractPath = Directory.GetCurrentDirectory(); 425 | string tempExtractPath = Path.Combine(extractPath, "ClassicCounter_temp"); 426 | 427 | try 428 | { 429 | Directory.CreateDirectory(tempExtractPath); 430 | 431 | await Download7za(); 432 | 433 | string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); 434 | if (launcherDir == null) 435 | { 436 | throw new InvalidOperationException("Could not determine launcher directory"); 437 | } 438 | 439 | string exePath = Path.Combine(launcherDir, "7za.exe"); 440 | 441 | using (var process = new Process()) 442 | { 443 | process.StartInfo = new ProcessStartInfo 444 | { 445 | FileName = exePath, 446 | Arguments = $"x \"{firstFile}\" -o\"{tempExtractPath}\" -y", 447 | UseShellExecute = false, 448 | RedirectStandardOutput = true, 449 | CreateNoWindow = true 450 | }; 451 | 452 | if (Debug.Enabled()) 453 | Terminal.Debug($"Starting extraction to temp directory..."); 454 | 455 | process.Start(); 456 | await process.WaitForExitAsync(); 457 | 458 | if (process.ExitCode != 0) 459 | { 460 | throw new Exception($"7za extraction failed with exit code: {process.ExitCode}"); 461 | } 462 | } 463 | 464 | string classicCounterPath = Path.Combine(tempExtractPath, "ClassicCounter"); 465 | if (Directory.Exists(classicCounterPath)) 466 | { 467 | if (Debug.Enabled()) 468 | Terminal.Debug("Moving contents from ClassicCounter folder to root directory..."); 469 | 470 | // first, get all files and directories from the ClassicCounter folder 471 | foreach (string dirPath in Directory.GetDirectories(classicCounterPath, "*", SearchOption.AllDirectories)) 472 | { 473 | // create directory in root, removing the "ClassicCounter" part from the path 474 | string newDirPath = dirPath.Replace(classicCounterPath, extractPath); 475 | Directory.CreateDirectory(newDirPath); 476 | } 477 | 478 | foreach (string filePath in Directory.GetFiles(classicCounterPath, "*.*", SearchOption.AllDirectories)) 479 | { 480 | string newFilePath = filePath.Replace(classicCounterPath, extractPath); 481 | 482 | // skip launcher.exe 483 | if (Path.GetFileName(filePath).Equals("launcher.exe", StringComparison.OrdinalIgnoreCase)) 484 | { 485 | if (Debug.Enabled()) 486 | Terminal.Debug("Skipping launcher.exe"); 487 | continue; 488 | } 489 | 490 | try 491 | { 492 | if (File.Exists(newFilePath)) 493 | { 494 | File.Delete(newFilePath); 495 | } 496 | File.Move(filePath, newFilePath); 497 | } 498 | catch (Exception ex) 499 | { 500 | Terminal.Warning($"Failed to move file {filePath}: {ex.Message}"); 501 | } 502 | } 503 | } 504 | else 505 | { 506 | throw new DirectoryNotFoundException("ClassicCounter folder not found in extracted contents"); 507 | } 508 | 509 | try 510 | { 511 | Directory.Delete(tempExtractPath, true); 512 | if (Debug.Enabled()) 513 | Terminal.Debug("Deleted temporary extraction directory"); 514 | 515 | foreach (string file in files) 516 | { 517 | File.Delete(file); 518 | if (Debug.Enabled()) 519 | Terminal.Debug($"Deleted archive part: {file}"); 520 | } 521 | } 522 | catch (Exception ex) 523 | { 524 | Terminal.Warning($"Failed to cleanup some temporary files: {ex.Message}"); 525 | } 526 | 527 | if (Debug.Enabled()) 528 | Terminal.Debug("Extraction and file movement completed successfully!"); 529 | } 530 | catch (Exception ex) 531 | { 532 | Terminal.Error($"Extraction failed: {ex.Message}"); 533 | if (Debug.Enabled()) 534 | Terminal.Debug($"Stack trace: {ex.StackTrace}"); 535 | 536 | try 537 | { 538 | if (Directory.Exists(tempExtractPath)) 539 | Directory.Delete(tempExtractPath, true); 540 | } 541 | catch { } 542 | 543 | throw; 544 | } 545 | } 546 | 547 | // FOR DOWNLOAD STATUS 548 | public static int dotCount = 0; 549 | public static DateTime lastDotUpdate = DateTime.Now; 550 | public static string GetDots() 551 | { 552 | if ((DateTime.Now - lastDotUpdate).TotalMilliseconds > 500) 553 | { 554 | dotCount = (dotCount + 1) % 4; 555 | lastDotUpdate = DateTime.Now; 556 | } 557 | return "...".Substring(0, dotCount); 558 | } 559 | public static string GetProgressBar(double percentage) 560 | { 561 | int blocks = 16; 562 | int level = (int)(percentage / (100.0 / (blocks * 3))); 563 | string bar = ""; 564 | 565 | for (int i = 0; i < blocks; i++) 566 | { 567 | int blockLevel = Math.Min(3, Math.Max(0, level - (i * 3))); 568 | bar += blockLevel switch 569 | { 570 | 0 => "░", 571 | 1 => "▒", 572 | 2 => "▓", 573 | 3 => "█", 574 | _ => "█" 575 | }; 576 | } 577 | return bar; 578 | } 579 | // DOWNLOAD STATUS OVER 580 | 581 | 582 | 583 | private static async Task Download7za() 584 | { 585 | string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); 586 | if (launcherDir == null) 587 | { 588 | throw new InvalidOperationException("Could not determine launcher directory"); 589 | } 590 | 591 | string exePath = Path.Combine(launcherDir, "7za.exe"); 592 | bool downloaded = false; 593 | int retryCount = 0; 594 | string[] fallbackUrls = new[] 595 | { 596 | "https://fastdl.classiccounter.cc/7za.exe", 597 | "https://ollumcc.github.io/7za.exe" 598 | }; 599 | 600 | while (!downloaded && retryCount < 10) 601 | { 602 | if (!File.Exists(exePath)) 603 | { 604 | if (Debug.Enabled()) 605 | Terminal.Debug($"7za.exe not found, downloading... (Attempt {retryCount + 1}/10)"); 606 | 607 | try 608 | { 609 | await _downloader.DownloadFileTaskAsync( 610 | fallbackUrls[retryCount % fallbackUrls.Length], 611 | exePath 612 | ); 613 | 614 | if (File.Exists(exePath)) 615 | { 616 | downloaded = true; 617 | if (Debug.Enabled()) 618 | Terminal.Debug($"Downloaded 7za.exe to: {exePath}"); 619 | } 620 | else 621 | { 622 | Terminal.Error($"Failed to download 7za.exe! Trying again... (Attempt {retryCount + 1})"); 623 | retryCount++; 624 | } 625 | } 626 | catch (Exception ex) 627 | { 628 | if (Debug.Enabled()) 629 | Terminal.Debug($"Failed to download 7za.exe: {ex.Message}"); 630 | retryCount++; 631 | } 632 | 633 | if (retryCount > 0) 634 | await Task.Delay(1000); 635 | } 636 | else 637 | { 638 | downloaded = true; 639 | } 640 | } 641 | 642 | if (!downloaded) 643 | { 644 | Terminal.Error("Couldn't download 7za.exe! Launcher will close in 5 seconds..."); 645 | await Task.Delay(5000); 646 | Environment.Exit(1); 647 | } 648 | } 649 | 650 | private static async Task Extract7z(string archivePath, string outputPath) 651 | { 652 | try 653 | { 654 | if (!File.Exists(archivePath)) 655 | { 656 | if (Debug.Enabled()) 657 | Terminal.Debug($"Archive file not found: {archivePath}"); 658 | return; 659 | } 660 | 661 | await Download7za(); 662 | 663 | string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); 664 | if (launcherDir == null) 665 | { 666 | throw new InvalidOperationException("Could not determine launcher directory"); 667 | } 668 | 669 | string exePath = Path.Combine(launcherDir, "7za.exe"); 670 | 671 | using (var process = new Process()) 672 | { 673 | process.StartInfo = new ProcessStartInfo 674 | { 675 | FileName = exePath, 676 | Arguments = $"x \"{archivePath}\" -o\"{Path.GetDirectoryName(outputPath)}\" -y", 677 | UseShellExecute = false, 678 | RedirectStandardOutput = true, 679 | CreateNoWindow = true 680 | }; 681 | 682 | if (Debug.Enabled()) 683 | Terminal.Debug($"Starting extraction..."); 684 | 685 | process.Start(); 686 | await process.WaitForExitAsync(); 687 | 688 | if (process.ExitCode != 0) 689 | { 690 | throw new Exception($"7za extraction failed with exit code: {process.ExitCode}"); 691 | } 692 | 693 | if (Debug.Enabled()) 694 | Terminal.Debug("Extraction completed successfully!"); 695 | 696 | Argument.AddArgument("+snd_mixahead 0.1"); 697 | } 698 | 699 | // delete 7z after extract 700 | try 701 | { 702 | File.Delete(archivePath); 703 | if (Debug.Enabled()) 704 | Terminal.Debug($"Deleted archive file: {archivePath}"); 705 | } 706 | catch (Exception ex) 707 | { 708 | if (Debug.Enabled()) 709 | Terminal.Debug($"Failed to delete archive file: {ex.Message}"); 710 | } 711 | } 712 | catch (Exception ex) 713 | { 714 | Terminal.Error($"Extraction failed: {ex.Message}\nStack trace: {ex.StackTrace}"); 715 | throw; 716 | } 717 | } 718 | 719 | public static void Cleanup7zFiles() 720 | { 721 | try 722 | { 723 | string directory = Directory.GetCurrentDirectory(); 724 | var files = Directory.GetFiles(directory, "*.7z", SearchOption.AllDirectories); 725 | 726 | foreach (string file in files) 727 | { 728 | try 729 | { 730 | File.Delete(file); 731 | if (Debug.Enabled()) 732 | Terminal.Debug($"Deleted .7z file: {file}"); 733 | } 734 | catch (Exception ex) 735 | { 736 | if (Debug.Enabled()) 737 | Terminal.Debug($"Failed to delete .7z file {file}: {ex.Message}"); 738 | } 739 | } 740 | 741 | // Delete 7za.exe if it exists 742 | string? launcherDir = Path.GetDirectoryName(Environment.ProcessPath); 743 | if (launcherDir != null) 744 | { 745 | string sevenZaPath = Path.Combine(launcherDir, "7za.exe"); 746 | if (File.Exists(sevenZaPath)) 747 | { 748 | try 749 | { 750 | File.Delete(sevenZaPath); 751 | if (Debug.Enabled()) 752 | Terminal.Debug("Deleted 7za.exe"); 753 | } 754 | catch (Exception ex) 755 | { 756 | if (Debug.Enabled()) 757 | Terminal.Debug($"Failed to delete 7za.exe: {ex.Message}"); 758 | } 759 | } 760 | } 761 | } 762 | catch (Exception ex) 763 | { 764 | if (Debug.Enabled()) 765 | Terminal.Debug($"Failed to perform cleanup: {ex.Message}"); 766 | } 767 | } 768 | } 769 | } 770 | --------------------------------------------------------------------------------