├── 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 |
--------------------------------------------------------------------------------