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