├── .clang-format ├── .clang-tidy ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── check-spelling.yml │ ├── cleanup-workflows.yml │ ├── cpp-quality.yml │ └── publish-PR-test-results.yml ├── .gitignore ├── CHANGELOG.md ├── CMakeLists.txt ├── CPPLINT.cfg ├── LICENSE ├── README.md ├── build-tools └── cspell │ ├── cspell.config.json │ └── repo-words.txt ├── cmake ├── FindSteamworks.cmake ├── PatchCsGoProtobufs.cmake ├── ShowBuildTargetProperties.cmake └── setup-vcpkg.cmake ├── cspell.json ├── downloads └── sw_sdk_153a.zip ├── format.sh ├── resources ├── CSGO_CLI.rc ├── csgo_cli.ico ├── steam_appid.txt └── update.bat ├── src ├── DataObject.cpp ├── DataObject.h ├── DateTimeUtils.cpp ├── DateTimeUtils.h ├── ErrorHandler.h ├── ExceptionHandler.cpp ├── ExceptionHandler.h ├── ShareCode.cpp ├── ShareCode.h ├── SteamId.cpp ├── SteamId.h ├── VersionAndConstants.h ├── VersionAndConstants.h.in ├── commands │ ├── cmd.globalstats.cpp │ ├── cmd.globalstats.h │ ├── cmd.help.cpp │ ├── cmd.help.h │ ├── cmd.matches.cpp │ ├── cmd.matches.h │ ├── cmd.scoreboard.cpp │ ├── cmd.scoreboard.h │ ├── cmd.upload.cpp │ ├── cmd.upload.h │ ├── cmd.user.cpp │ └── cmd.user.h ├── csgo │ ├── CSGOClient.cpp │ ├── CSGOClient.h │ ├── CSGOMMHello.cpp │ ├── CSGOMMHello.h │ ├── CSGOMatchData.h │ ├── CSGOMatchList.cpp │ ├── CSGOMatchList.h │ ├── CSGOMatchPlayerScore.cpp │ ├── CSGOMatchPlayerScore.h │ ├── CSGORankUpdate.cpp │ ├── CSGORankUpdate.h │ └── GCMsgHandler.h ├── csgostats │ ├── ShareCodeCache.cpp │ ├── ShareCodeCache.h │ ├── ShareCodeUpload.cpp │ └── ShareCodeUpload.h ├── main │ └── main.cpp └── platform │ └── windows │ ├── WinCliColors.cpp │ └── WinCliColors.h ├── tests ├── CMakeLists.txt ├── ShareCodeUpload.tests.cpp └── main.tests.cpp └── vcpkg.json /.clang-format: -------------------------------------------------------------------------------- 1 | # A complete list of style options can be found at: 2 | # https://clang.llvm.org/docs/ClangFormatStyleOptions.html 3 | # 4 | # Google C++ Style Guide 5 | # https://google.github.io/styleguide/cppguide.html 6 | 7 | # You can disable formatting of a piece of code like so: 8 | # 9 | # int formatted_code; 10 | # // clang-format off 11 | # void unformatted_code ; 12 | # // clang-format on 13 | # void formatted_code_again; 14 | # 15 | 16 | Language: Cpp 17 | 18 | # Basic indentation and spacing rules: 19 | IndentWidth: 4 20 | ContinuationIndentWidth: 4 21 | UseCRLF: false 22 | Standard: c++20 23 | UseTab: Never 24 | MaxEmptyLinesToKeep: 1 25 | 26 | AccessModifierOffset: -4 27 | AlignAfterOpenBracket: AlwaysBreak 28 | AlignConsecutiveAssignments: true 29 | AlignConsecutiveDeclarations: false 30 | AlignConsecutiveMacros: true 31 | AlignEscapedNewlinesLeft: true 32 | AlignOperands: true 33 | AlignTrailingComments: true 34 | AllowAllParametersOfDeclarationOnNextLine: true 35 | AllowShortFunctionsOnASingleLine: Empty 36 | AllowShortBlocksOnASingleLine: true 37 | AllowShortIfStatementsOnASingleLine: false 38 | AllowShortLoopsOnASingleLine: false 39 | AllowShortLambdasOnASingleLine: Empty 40 | AllowShortCaseLabelsOnASingleLine: false 41 | AlwaysBreakBeforeMultilineStrings: true 42 | AlwaysBreakTemplateDeclarations: Yes 43 | 44 | # Configure each individual brace in BraceWrapping 45 | BraceWrapping: 46 | AfterCaseLabel: false 47 | AfterClass: true 48 | AfterControlStatement: false 49 | AfterEnum: true 50 | AfterFunction: true 51 | AfterNamespace: true 52 | AfterObjCDeclaration: true 53 | AfterStruct: true 54 | AfterUnion: true 55 | BeforeCatch: false 56 | BeforeElse: false 57 | IndentBraces: false 58 | BreakBeforeBraces: Custom 59 | 60 | BinPackArguments: false 61 | BinPackParameters: false 62 | BreakBeforeBinaryOperators: false 63 | BreakBeforeTernaryOperators: false 64 | BreakConstructorInitializers: AfterColon 65 | BreakInheritanceList: AfterColon 66 | ColumnLimit: 120 67 | CompactNamespaces: true 68 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 69 | ConstructorInitializerIndentWidth: 4 70 | Cpp11BracedListStyle: true 71 | DeriveLineEnding: true 72 | ExperimentalAutoDetectBinPacking: true 73 | FixNamespaceComments: true 74 | IncludeBlocks: Preserve 75 | IndentCaseLabels: false 76 | IndentGotoLabels: false 77 | IndentPPDirectives: None 78 | IndentFunctionDeclarationAfterType: false 79 | NamespaceIndentation: All 80 | ObjCSpaceBeforeProtocolList: false 81 | PenaltyBreakBeforeFirstCallParameter: 10 82 | PenaltyBreakComment: 60 83 | PenaltyBreakFirstLessLess: 20 84 | PenaltyBreakString: 1000 85 | PenaltyExcessCharacter: 1000000 86 | PenaltyReturnTypeOnItsOwnLine: 200 87 | SortIncludes: false 88 | SpaceAfterControlStatementKeyword: true 89 | SpaceBeforeParens: ControlStatements 90 | SpaceBeforeAssignmentOperators: true 91 | SpaceBeforeRangeBasedForLoopColon: true 92 | SpaceBeforeSquareBrackets: false 93 | SpacesInAngles: false 94 | SpaceInEmptyBlock: true 95 | SpaceInEmptyParentheses: false 96 | SpacesBeforeTrailingComments: 1 97 | SpacesInCStyleCastParentheses: false 98 | SpacesInConditionalStatement: false 99 | SpacesInContainerLiterals: false 100 | SpacesInParentheses: false 101 | SpacesInSquareBrackets: false 102 | 103 | # int* value instead of int *value 104 | DerivePointerAlignment: false 105 | PointerAlignment: Left 106 | PointerBindsToType: true 107 | SpaceAroundPointerQualifiers: After 108 | 109 | # Prefer "east const", placing `const` after the type: int const instead of const int 110 | QualifierAlignment: Right 111 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | # Clang Tidy - Config 2 | --- 3 | Checks: '-* 4 | bugprone-*, 5 | -bugprone-easily-swappable-parameters 6 | cert-*, 7 | -cert-err33-c 8 | cppcoreguidelines-*, 9 | -cppcoreguidelines-pro-bounds-pointer-arithmetic 10 | -cppcoreguidelines-pro-type-vararg 11 | -cppcoreguidelines-avoid-c-arrays 12 | -cppcoreguidelines-no-malloc 13 | -cppcoreguidelines-owning-memory 14 | -cppcoreguidelines-pro-bounds-array-to-pointer-decay 15 | misc-*, 16 | -misc-include-cleaner 17 | modernize-*, 18 | -modernize-use-trailing-return-type 19 | performance-*, 20 | readability-*, 21 | -readability-identifier-length 22 | -readability-function-cognitive-complexity 23 | -readability-static-accessed-through-instance 24 | ' 25 | 26 | WarningsAsErrors: '*' 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=lf 3 | 4 | # Files that will be normalized and converted to native line endings. 5 | *.c text eol=lf 6 | *.h text eol=lf 7 | *.cpp text eol=lf 8 | *.hpp text eol=lf 9 | *.md text eol=lf 10 | *.yaml text eol=lf 11 | 12 | # Files that will always have CRLF line endings. 13 | *.sln text eol=crlf 14 | 15 | # Denote all files that are truly binary and should not be modified. 16 | *.png binary 17 | *.jpg binary 18 | *.ico binary -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # dependabot.yml 2 | # 3 | # Dependabot updates dependencies automatically to their latest versions. 4 | # 5 | # Docs: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 6 | 7 | version: 2 8 | 9 | updates: 10 | # Enable automatic version updates for Github Actions 11 | - package-ecosystem: "github-actions" 12 | # Look for `.github/workflows` in the `root` directory 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | ignore: 17 | # ignore all updates for "https://github.com/actions/github-script" 18 | - dependency-name: "github-script" 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # 2 | # .github/workflows/build-on-windows.yml 3 | # 4 | # Copyright 2021 Jens A. Koch. 5 | # SPDX-License-Identifier: BSL-1.0 6 | # This file is part of https://github.com/jakoch/csgo-cli 7 | # 8 | 9 | name: "Build on Windows" 10 | 11 | on: [push, pull_request] 12 | 13 | jobs: 14 | 15 | # --------------------------------------------------------------------------------------- 16 | 17 | build-on-windows-2022: 18 | 19 | # --------------------------------------------------------------------------------------- 20 | 21 | name: "Windows VC17" 22 | runs-on: windows-2022 23 | 24 | env: 25 | COMPILER: "VC17" 26 | BUILD_TYPE: Release 27 | VCPKG_INSTALLATION_ROOT: 'C:\vcpkg' 28 | VCPKG_TARGET_TRIPLET: 'x64-windows-static' 29 | BUILD_SHARED_LIBS: OFF 30 | 31 | defaults: 32 | run: 33 | shell: cmd 34 | 35 | steps: 36 | - name: 🤘 Checkout Code 37 | uses: actions/checkout@v4 # https://github.com/actions/checkout 38 | 39 | # https://community.chocolatey.org/packages/ccache 40 | #- name: 🔽 Install ccache 41 | # run: choco install ccache --no-progress 42 | 43 | # https://community.chocolatey.org/packages/ninja 44 | - name: 🔽 Install Ninja 45 | run: choco install ninja --no-progress 46 | 47 | # https://community.chocolatey.org/packages/cmake 48 | - name: 🔽 Install CMake 49 | run: choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' --no-progress 50 | 51 | # https://github.com/actions/runner-images/blob/main/images/windows/Windows2022-Readme.md#visual-studio-enterprise-2022 52 | - name: 📥 Setup VC17 Environment (→ vcvars64) 53 | run: call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" 54 | 55 | - name: 🔽 Update VCPKG 56 | run: | 57 | cd ${{ env.VCPKG_INSTALLATION_ROOT }} 58 | git reset --hard 59 | git pull --quiet 60 | bootstrap-vcpkg.bat -disableMetrics 61 | vcpkg integrate install --triplet=${{ env.VCPKG_TARGET_TRIPLET }} --feature-flags=manifests 62 | vcpkg version 63 | 64 | - name: 🎯 Cache VCPKG 65 | if: always() 66 | id: cache-vcpkg 67 | uses: actions/cache@v4 # https://github.com/actions/cache 68 | with: 69 | path: ~\AppData\Local\vcpkg\archives 70 | key: cache-windows-vcpkg-${{ env.VCPKG_TARGET_TRIPLET }}-${{ github.ref }}-${{ github.run_number }} 71 | restore-keys: | 72 | cache-windows-vcpkg-${{ env.VCPKG_TARGET_TRIPLET }}-${{ github.ref }} 73 | cache-windows-vcpkg-${{ env.VCPKG_TARGET_TRIPLET }} 74 | 75 | - name: Extract SW_SDK 76 | env: 77 | SW_SDK_SECRET: ${{ secrets.SW_SDK_SECRET }} 78 | run: | 79 | dir 80 | 7z x ${{ github.workspace }}\downloads\sw_sdk_153a.zip -aoa -y -odependencies -p%SW_SDK_SECRET% 81 | cd dependencies 82 | ren sdk sw_sdk 83 | 84 | - name: ✏ CMake ➔ Configure (including VCPKG → Install Dependencies) 85 | run: | 86 | cmake -G "Visual Studio 17 2022" -A x64 ^ 87 | -S ${{ github.workspace }} -B build ^ 88 | -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} ^ 89 | -DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_INSTALLATION_ROOT }}/scripts/buildsystems/vcpkg.cmake ^ 90 | -DVCPKG_TARGET_TRIPLET=${{ env.VCPKG_TARGET_TRIPLET }} ^ 91 | -DBUILD_SHARED_LIBS=${{ env.BUILD_SHARED_LIBS }} ^ 92 | -DCMAKE_VERBOSE_MAKEFILE=ON 93 | 94 | - name: ❔ CHECK important folders, to see if everything is present (before building) 95 | run: | 96 | dir ${{ github.workspace }} 97 | dir ${{ github.workspace }}\downloads 98 | dir ${{ github.workspace }}\dependencies 99 | 100 | #dir ${{ github.workspace }}\vcpkg_packages\${{ env.VCPKG_TARGET_TRIPLET }} 101 | 102 | - name: 🙏 CMake → Build 103 | run: cmake --build build --config %BUILD_TYPE% -j %NUMBER_OF_PROCESSORS% 104 | 105 | - name: ✔ ❌✔️ Test → Run TestSuite 106 | run: | 107 | cd build\tests\%BUILD_TYPE% 108 | test_suite.exe 109 | 110 | - name: ✔ ❌✔️ Test → Run CTest → Generate test_results.xml 111 | run: | 112 | cd build\tests 113 | ctest -V -C %BUILD_TYPE% --output-junit test_results_${{ env.VCPKG_TARGET_TRIPLET }}_${{ env.COMPILER }}.xml 114 | 115 | - name: 📦 CMake ➔ Install 116 | run: cmake --install build --config %BUILD_TYPE% --prefix install --verbose 117 | 118 | #dir /S /B build\vcpkg_packages\${{ env.VCPKG_TARGET_TRIPLET }} 119 | - name: ❔ CHECK important folders, to see if everything is present (after building & installing) 120 | run: | 121 | dir /S /B build 122 | dir /S /B install 123 | 124 | # We upload multiple files into the same "test-results" artifact file (zip). 125 | # Each file is differently named by adding the job name of the matrix as a suffix. 126 | # This enables the "Unit Test Result" display to show all individual test runs of the matrix. 127 | # The tests are published after all matrix runs finished (from job: "publish-test-results"). 128 | - name: 🔼 Upload Test Results 129 | uses: actions/upload-artifact@v4 # https://github.com/actions/upload-artifact 130 | if: always() 131 | with: 132 | name: test-results 133 | path: build\tests\test_results_*.xml 134 | 135 | - name: 🔼 Upload Build Artifact -> csgo_cli 136 | uses: actions/upload-artifact@v4 # https://github.com/actions/upload-artifact 137 | with: 138 | name: csgo_cli-${{ env.VCPKG_TARGET_TRIPLET }}-${{ env.COMPILER }} 139 | path: ${{ github.workspace }}/install/bin/ 140 | 141 | # --------------------------------------------------------------------------------------- 142 | 143 | publish-test-results: 144 | 145 | # --------------------------------------------------------------------------------------- 146 | 147 | # Only publish test results, when the action runs in your repository's context. 148 | # In other words: this disables publishing tests results from pull requests. 149 | # PR test results are published from the standalone workflow "publish-PR-test-results.yml". 150 | 151 | name: "Publish Tests Results" 152 | needs: build-on-windows-2022 153 | runs-on: ubuntu-latest 154 | if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository ) 155 | 156 | steps: 157 | 158 | - name: 🔽 Download Artifacts 159 | uses: actions/download-artifact@v4 # https://github.com/actions/download-artifact 160 | with: 161 | path: artifacts 162 | 163 | - name: 👌 Publish Test Results 164 | uses: EnricoMi/publish-unit-test-result-action@v2 # https://github.com/EnricoMi/publish-unit-test-result-action 165 | if: always() 166 | with: 167 | junit_files: artifacts/**/test_results*.xml 168 | -------------------------------------------------------------------------------- /.github/workflows/check-spelling.yml: -------------------------------------------------------------------------------- 1 | name: "Check Spelling" 2 | 3 | on: 4 | push: 5 | # You can manually run this workflow. 6 | workflow_dispatch: 7 | 8 | jobs: 9 | 10 | # --------------------------------------------------------------------------------------- 11 | 12 | check-spelling: 13 | 14 | # --------------------------------------------------------------------------------------- 15 | 16 | name: "Spell Checker" 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: 🤘 Checkout Code 21 | uses: actions/checkout@v4 # https://github.com/actions/checkout 22 | 23 | - name: 🔽 Install Dependencies 24 | run: sudo npm install -g cspell 25 | 26 | - name: 🙏 Check Spelling using cspell 27 | run: | 28 | export CSPELL_OUTPUT=$(sudo cspell lint --no-summary --no-progress .) 29 | if [ -n "$CSPELL_OUTPUT" ]; then echo "$CSPELL_OUTPUT" && exit 1; fi; -------------------------------------------------------------------------------- /.github/workflows/cleanup-workflows.yml: -------------------------------------------------------------------------------- 1 | # 2 | # .github/workflows/cleanup-workflows.yml 3 | # 4 | # Copyright 2021 Ching Chow, Jens A. Koch. 5 | # SPDX-License-Identifier: BSL-1.0 6 | # Origin: https://github.community/t/delete-old-workflow-results/16152/42 7 | # This file is part of https://github.com/jakoch/csgo-cli 8 | # 9 | 10 | name: Clean Workflow Runs 11 | 12 | on: 13 | # This workflow runs at 00:00 daily. 14 | schedule: 15 | - cron: '0 0 * * *' # GMT 16 | # You can manually run this workflow. 17 | workflow_dispatch: 18 | 19 | jobs: 20 | 21 | # --------------------------------------------------------------------------------------- 22 | 23 | cleanup-workflows: 24 | 25 | # --------------------------------------------------------------------------------------- 26 | 27 | name: "Clean Workflows" 28 | runs-on: ubuntu-latest 29 | timeout-minutes: 10 30 | 31 | steps: 32 | - name: ✂ Remove cancelled or skipped workflow runs 33 | uses: actions/github-script@v7 # https://github.com/actions/github-script 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | script: | 37 | const cancelled = await github.rest.actions.listWorkflowRunsForRepo({ 38 | owner: context.repo.owner, 39 | per_page: 100, 40 | repo: context.repo.repo, 41 | status: 'cancelled', 42 | }); 43 | const skipped = await github.rest.actions.listWorkflowRunsForRepo({ 44 | owner: context.repo.owner, 45 | per_page: 100, 46 | repo: context.repo.repo, 47 | status: 'skipped', 48 | }); 49 | for (const response of [cancelled, skipped]) { 50 | for (const run of response.data.workflow_runs) { 51 | console.log(`[Deleting] Run id ${run.id} of '${run.name}' is a cancelled or skipped run.`); 52 | await github.rest.actions.deleteWorkflowRun({ 53 | owner: context.repo.owner, 54 | repo: context.repo.repo, 55 | run_id: run.id 56 | }); 57 | } 58 | } 59 | 60 | - name: ✂ Remove 30 days old workflows runs 61 | uses: actions/github-script@v7 # https://github.com/actions/github-script 62 | with: 63 | github-token: ${{ secrets.GITHUB_TOKEN }} 64 | script: | 65 | const days_to_expiration = 14; 66 | const ms_in_day = 86400000; 67 | const now = Date.now(); 68 | const pages = 5; 69 | // add the workflows runs to remove here 70 | const workflows = [ 71 | 'build.yml', 72 | 'publish-PR-test-results.yml' 73 | ] 74 | let runs_to_delete = []; 75 | for (const workflow of workflows) { 76 | for (let page = 0; page < pages; page += 1) { 77 | let response = await github.rest.actions.listWorkflowRuns({ 78 | owner: context.repo.owner, 79 | page: page, 80 | per_page: 100, 81 | repo: context.repo.repo, 82 | workflow_id: workflow 83 | }); 84 | if (response.data.workflow_runs.length > 0) { 85 | for (const run of response.data.workflow_runs) { 86 | if (now - Date.parse(run.created_at) > ms_in_day * days_to_expiration) { 87 | runs_to_delete.push([run.id, run.name]); 88 | } 89 | } 90 | } 91 | } 92 | } 93 | for (const run of runs_to_delete) { 94 | console.log(`[Deleting] Run id ${run[0]} of '${run[1]}' is older than ${days_to_expiration} days.`); 95 | try { 96 | await github.rest.actions.deleteWorkflowRun({ 97 | owner: context.repo.owner, 98 | repo: context.repo.repo, 99 | run_id: run[0] 100 | }); 101 | } catch (error) { 102 | // ignore errors 103 | } 104 | } 105 | 106 | - name: ✂ Remove runs of the cleanup workflow itself 107 | uses: actions/github-script@v7 # https://github.com/actions/github-script 108 | with: 109 | github-token: ${{ secrets.GITHUB_TOKEN }} 110 | script: | 111 | const pages = 5; 112 | let runs_to_delete = []; 113 | for (let page = 0; page < pages; page += 1) { 114 | let response = await github.rest.actions.listWorkflowRuns({ 115 | owner: context.repo.owner, 116 | page: page, 117 | per_page: 100, 118 | repo: context.repo.repo, 119 | workflow_id: 'cleanup-workflows.yml' 120 | }); 121 | if (response.data.workflow_runs.length > 0) { 122 | for (const run of response.data.workflow_runs) { 123 | runs_to_delete.push([run.id, run.name]); 124 | } 125 | } 126 | } 127 | for (const run of runs_to_delete) { 128 | console.log(`[Deleting] Run id ${run[0]} of '${run[1]}'.`); 129 | try { 130 | await github.actions.deleteWorkflowRun({ 131 | owner: context.repo.owner, 132 | repo: context.repo.repo, 133 | run_id: run[0] 134 | }); 135 | } catch (error) { 136 | // ignore errors 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /.github/workflows/cpp-quality.yml: -------------------------------------------------------------------------------- 1 | # 2 | # .github/workflows/cpp-quality.yml 3 | # 4 | # Copyright 2024 Jens A. Koch. 5 | # SPDX-License-Identifier: MIT 6 | # This file is part of jakoch/wikifolio_universe_converter. 7 | # 8 | 9 | name: "C++ Quality" 10 | 11 | on: 12 | - push 13 | - pull_request 14 | # You can manually run this workflow. 15 | - workflow_dispatch 16 | 17 | # improve CI concurrency by automatically cancelling outdated jobs 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 20 | cancel-in-progress: true 21 | 22 | permissions: 23 | contents: read 24 | 25 | jobs: 26 | 27 | # --------------------------------------------------------------------------------------- 28 | 29 | clang-format: 30 | 31 | # --------------------------------------------------------------------------------------- 32 | 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | 37 | - name: 🤘 Checkout Code 38 | uses: actions/checkout@v4 # https://github.com/actions/checkout 39 | 40 | - name: 🔽 Install dos2unix 41 | run: sudo apt-get install -y dos2unix 42 | 43 | - name: 🔽 Install clang-format-18 44 | run: | 45 | wget https://apt.llvm.org/llvm.sh 46 | chmod +x ./llvm.sh 47 | sudo ./llvm.sh 18 48 | sudo apt-get install -y clang-format-18 49 | 50 | - name: Check formatting 51 | run: ./format.sh && git diff --exit-code 52 | env: 53 | CLANG_FORMAT: clang-format-18 54 | 55 | # --------------------------------------------------------------------------------------- 56 | 57 | cpplint: 58 | 59 | # --------------------------------------------------------------------------------------- 60 | 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | 65 | - name: 🤘 Checkout Code 66 | uses: actions/checkout@v4 # https://github.com/actions/checkout 67 | 68 | - name: Setup Python 69 | run: sudo apt-get -y install python3 python3-pip python3-venv 70 | 71 | - name: Setup Python Virtual Env 72 | run: | 73 | export VIRTUAL_ENV=/opt/venv 74 | python3 -m venv $VIRTUAL_ENV 75 | export PATH="$VIRTUAL_ENV/bin:$PATH" 76 | 77 | - name: Install cpplint 78 | run: pip install --prefer-binary --no-cache-dir cpplint 79 | 80 | - name: Run lint 81 | run: cpplint --recursive src --exclude=src/steamapi/* 82 | -------------------------------------------------------------------------------- /.github/workflows/publish-PR-test-results.yml: -------------------------------------------------------------------------------- 1 | # 2 | # .github/workflows/publish-PR-test-results.yml 3 | # 4 | # Copyright 2021 Jens A. Koch. 5 | # SPDX-License-Identifier: BSL-1.0 6 | # This file is part of https://github.com/jakoch/csgo-cli 7 | # 8 | 9 | name: "Publish PR Test Results" 10 | 11 | on: 12 | workflow_run: 13 | workflows: ["Build on Windows"] 14 | types: 15 | - completed 16 | 17 | jobs: 18 | 19 | # --------------------------------------------------------------------------------------- 20 | 21 | publish-test-results: 22 | 23 | # --------------------------------------------------------------------------------------- 24 | 25 | name: "Publish Test Results" 26 | runs-on: ubuntu-latest 27 | if: > 28 | github.event.workflow_run.conclusion != 'skipped' && ( 29 | github.event.sender.login == 'dependabot[bot]' || 30 | github.event.workflow_run.head_repository.full_name != github.repository 31 | ) 32 | 33 | steps: 34 | - name: 🔽 Download Artifacts 35 | uses: actions/github-script@v7 # https://github.com/actions/github-script 36 | with: 37 | script: | 38 | var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ 39 | owner: context.repo.owner, 40 | repo: context.repo.repo, 41 | run_id: ${{ github.event.workflow_run.id }}, 42 | }); 43 | console.log(artifacts); 44 | var testArtifact = artifacts.data.artifacts.filter((artifact) => { 45 | return artifact.name == "test_results" 46 | })[0]; 47 | var download = await github.rest.actions.downloadArtifact({ 48 | owner: context.repo.owner, 49 | repo: context.repo.repo, 50 | artifact_id: testArtifact.id, 51 | archive_format: 'zip', 52 | }); 53 | var fs = require('fs'); 54 | fs.writeFileSync('${{github.workspace}}/test_results.zip', Buffer.from(download.data)); 55 | 56 | - name: Extract Artifacts 57 | run: | 58 | mkdir -p tests 59 | unzip -d tests test_results.zip 60 | 61 | - name: 👌 Publish Test Results 62 | uses: EnricoMi/publish-unit-test-result-action@v2 # https://github.com/EnricoMi/publish-unit-test-result-action 63 | with: 64 | commit: ${{ github.event.workflow_run.head_sha }} 65 | junit_files: tests/**/test_results*.xml 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .vscode/ 3 | out/ 4 | build/ 5 | dependencies/sw_sdk 6 | downloads 7 | vcpkg_packages/ 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [Unreleased] 5 | 6 | - Changed things. 7 | 8 | ## [1.4.0] - 2020-09-12 9 | 10 | - switched continuos integration service from Appveyor to Github Actions 11 | - added error message for "Cloudflare Captcha" as HTML response to ShareCodeUploader 12 | - removed batch based csgo-protobuf patch util, wrote new PatchCsGoProtobufs.cmake 13 | - fixed vcpkg issues on Github Actions 14 | - removed cacert.pem from dependencies, downloaded on-the-fly during cmake as curl-ca-bundle.crt 15 | 16 | ## [1.3.0] - 2020-11-22 17 | 18 | - added rank information for wingman and dangerzone to "-user" 19 | - added GlobalStats command 20 | - replaced RapidJson with nlohmann::json 21 | - added GameTypes enum for mapnames, not sure if this is correct 22 | - table rendering using fmt, dropped ConsoleTable and TableFormat 23 | - large code refactoring 24 | - new src folder structure, including /commands folder 25 | - added display of "xp_percentage progress" for "current xp level" 26 | - added display of "clan_name" and "clan_tag" to "-user" 27 | - fixed the calculation and display of "current_player_xp" 28 | - fixed crash after printing "-matches" 29 | - renamed all occurrences of "demo" to "replay" 30 | - added fmt to dependencies 31 | - added cli color support 32 | - switch output printing from std::cout to fmt::print/fmt::format 33 | 34 | ## [1.2.0] - 2020-11-05 35 | 36 | - added curl request handling with prepopulated dns cache 37 | - refactored uploadShareCode function to do a single POST request 38 | - changed compilation triplet from "x64-windows-dynamic" to "x64-windows-static" 39 | - all 3th-party dependencies are now baked-in 40 | - except redistributable steam64.dll, cacert.pem 41 | - switched to automatic c++ dependency management using vcpkg 42 | - see vcpkg.json: rapidjson, zlib, curl, protobuf 43 | - updated sw_sdk to v150 44 | - switched from secure-file to password protected zip for sw_sdk 45 | - updated csgo protobufs 46 | 47 | ## [1.1.0] - 2019-08-23 48 | 49 | - added ShareCodeCache to avoid re-posting sharecodes, [#2] 50 | - fixes: tabs 2 spaces, removed invalid includes 51 | 52 | ## [1.0.5] - 2019-07-15 53 | 54 | - Sharecode construction without MPIR dependency, [PR#5] 55 | 56 | ## [1.0.4] - 2019-07-13 57 | 58 | - added missing comma in rank list 59 | - added padding with zeros on match results output, [PR#4] 60 | 61 | ## [1.0.3] - 2019-07-01 62 | 63 | ### Bugfix 64 | - Posting demo sharecodes to csgostats.gg (implemented two step request ), [#1] 65 | - fixed curl multi-request and SSL issues 66 | 67 | ### Added 68 | - ConsoleTable for table printing on the console 69 | 70 | ### Changed 71 | - updated dependency: sw_sdk to v144 72 | - updated dependency: protobuf to v3.7.0 73 | - updated dependency: csgo-protobuf to #ab60446 74 | - refactoring 75 | 76 | ## [1.0.1] - 2018-07-14 77 | ### Added 78 | - added deployment of `csgo-cli-dependencies.zip` 79 | 80 | ## [1.0.0] - 2018-04-22 81 | ### Added 82 | - Initial Release 83 | 84 | [Unreleased]: https://github.com/jakoch/csgo-cli/compare/v1.4.0...HEAD 85 | [1.4.0]: https://github.com/jakoch/csgo-cli/compare/v1.3.0...v1.4.0 86 | [1.3.0]: https://github.com/jakoch/csgo-cli/compare/v1.2.0...v1.3.0 87 | [1.2.0]: https://github.com/jakoch/csgo-cli/compare/v1.1.0...v1.2.0 88 | [1.1.0]: https://github.com/jakoch/csgo-cli/compare/v1.0.5...v1.1.0 89 | [1.0.5]: https://github.com/jakoch/csgo-cli/compare/v1.0.4...v1.0.5 90 | [1.0.4]: https://github.com/jakoch/csgo-cli/compare/v1.0.3...v1.0.4 91 | [1.0.3]: https://github.com/jakoch/csgo-cli/compare/v1.0.1...v1.0.3 92 | [1.0.1]: https://github.com/jakoch/csgo-cli/compare/v1.0.0...v1.0.1 93 | [1.0.0]: https://github.com/jakoch/csgo-cli/releases/tag/v1.0.0 94 | 95 | [#1]: https://github.com/jakoch/csgo-cli/issues/1 96 | [#2]: https://github.com/jakoch/csgo-cli/issues/2 97 | [PR#4]: https://github.com/jakoch/csgo-cli/pull/4 98 | [PR#5]: https://github.com/jakoch/csgo-cli/pull/5 99 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5 FATAL_ERROR) 2 | 3 | #------------------------------------------------------------------- 4 | # Setup CMake Policies 5 | #------------------------------------------------------------------- 6 | 7 | # Policy CMP0074 is required to let find_package() use _ROOT variables. 8 | # https://cmake.org/cmake/help/git-stage/policy/CMP0074.html 9 | if(POLICY CMP0074) 10 | cmake_policy(SET CMP0074 NEW) 11 | endif() 12 | 13 | # Policy CMP0091 is required to enable MSVC_RUNTIME_LIBRARY property. 14 | # This needs to be set before the first project for the policy to have an effect. 15 | # https://cmake.org/cmake/help/git-stage/policy/CMP0091.html 16 | if(POLICY CMP0091) 17 | cmake_policy(SET CMP0091 NEW) 18 | endif() 19 | 20 | # Policy CMP0069 is required for INTERPROCEDURAL_OPTIMIZATION 21 | # https://cmake.org/cmake/help/git-stage/policy/CMP0069.html 22 | if(POLICY CMP0069) 23 | cmake_policy(SET CMP0069 NEW) 24 | endif() 25 | 26 | #------------------------------------------------------------------- 27 | # CMake Project 28 | #------------------------------------------------------------------- 29 | 30 | # vcpkg.json is the primary source for version data 31 | file(READ ${CMAKE_SOURCE_DIR}/vcpkg.json VCPKG_JSON_STRING) 32 | string(JSON APP_VERSION GET ${VCPKG_JSON_STRING} "version") 33 | 34 | project(csgo_cli VERSION ${APP_VERSION} LANGUAGES CXX) 35 | 36 | # inject version data into version header 37 | # write to build folder (not to source folder!) 38 | configure_file( 39 | "${PROJECT_SOURCE_DIR}/src/VersionAndConstants.h.in" 40 | "${CMAKE_CURRENT_BINARY_DIR}/src/VersionAndConstants.h" 41 | ) 42 | include_directories(${CMAKE_CURRENT_BINARY_DIR}/src) 43 | 44 | #------------------------------------------------------------------- 45 | # Setup CMake Includes 46 | #------------------------------------------------------------------- 47 | 48 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") 49 | 50 | include(FindPackageHandleStandardArgs) 51 | include(ShowBuildTargetProperties) 52 | 53 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 54 | 55 | #------------------------------------------------------------------- 56 | # Setup Dependency Manager: VCPKG 57 | #------------------------------------------------------------------- 58 | 59 | include(setup-vcpkg) 60 | 61 | # ================================================================================================== 62 | # Options 63 | # ================================================================================================== 64 | 65 | option(BUILD_SHARED_LIBS "Build shared or static libs" OFF) 66 | option(ENABLE_LTO "Enable link-time optimizations if supported by the compiler" ON) 67 | option(ENABLE_ASAN "Enable address sanitizer (ASAN)" OFF) 68 | option(BUILD_TESTS "Build tests" ON) 69 | 70 | # ================================================================================================== 71 | # Link time optimizations (LTO) 72 | # ================================================================================================== 73 | 74 | if (ENABLE_LTO) 75 | # https://cmake.org/cmake/help/git-stage/policy/CMP0069.html 76 | if(POLICY CMP0069) 77 | cmake_policy(SET CMP0069 NEW) 78 | set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) 79 | endif() 80 | 81 | include(CheckIPOSupported) 82 | check_ipo_supported(RESULT IPO_SUPPORT OUTPUT IPO_OUTPUT) 83 | 84 | if (IPO_SUPPORT) 85 | message(STATUS "[INFO] Link time optimizations (LTO) is ON.") 86 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) 87 | else() 88 | message(WARNING "[INFO] Link time optimizations (LTO) is not supported: ${IPO_OUTPUT}") 89 | endif() 90 | 91 | if(MSVC) 92 | # CMake IPO doesn't set the LTCG flag, which causes the linker to restart 93 | add_link_options($<$:/LTCG>) 94 | endif() 95 | endif() 96 | 97 | # ================================================================================================== 98 | # OS specific 99 | # ================================================================================================== 100 | 101 | if (WIN32) 102 | # Build for a Windows 10 host system. 103 | set(CMAKE_SYSTEM_VERSION 10.0) 104 | 105 | message(STATUS "[INFO] BUILD_SHARED_LIBS -> '${BUILD_SHARED_LIBS}'.") 106 | 107 | # When we build statically (MT): 108 | if(NOT BUILD_SHARED_LIBS) 109 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 110 | endif() 111 | endif() 112 | 113 | # ================================================================================================== 114 | # Compiler flags 115 | # ================================================================================================== 116 | 117 | if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 118 | 119 | message(STATUS "\n-- Compiler and platform toolset:\n") 120 | 121 | # Report compiler and platform toolset 122 | message(STATUS "USING COMPILER: ${CMAKE_CXX_COMPILER_ID}") 123 | message(STATUS "MSVC_VERSION: ${MSVC_VERSION}") 124 | message(STATUS "MSVC_TOOLSET_VERSION: ${MSVC_TOOLSET_VERSION}") 125 | message(STATUS "CMAKE_MSVC_RUNTIME_LIBRARY: ${CMAKE_MSVC_RUNTIME_LIBRARY}") 126 | 127 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /std:c++latest") 128 | 129 | # MSVC warning suppressions 130 | add_definitions(-D_CRT_SECURE_NO_WARNINGS) 131 | add_definitions(-D_SCL_SECURE_NO_WARNINGS) 132 | add_definitions(-D_SILENCE_CXX20_IS_POD_DEPRECATION_WARNING) # protobuf triggers this warning 133 | add_definitions(-DNOMINMAX) 134 | 135 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /permissive-") 136 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /nologo") 137 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") 138 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /LARGEADDRESSAWARE") # Allow more than 2 GB in 32 bit application. 139 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4146") # Warning C4146: unary minus operator applied to unsigned type, result still unsigned 140 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4244") # Warning C4244: 'initializing': conversion from 'double' to 'int', possible loss of data 141 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4267") # Warning C4267: 'var' : conversion from 'size_t' to 'type', possible loss of data 142 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4305") # Warning C4305: 'initializing': truncation from 'double' to 'float' 143 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4800") # Warning C4800: 'uint32_t' : forcing value to bool 'true' or 'false' (performance warning) 144 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4838") # Warning C4838: conversion from 'double' to 'float' requires a narrowing conversion 145 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4996") # Warning C4996: Call to 'function' with parameters that may be unsafe 146 | # You don't want to know why. 147 | # https://github.com/protocolbuffers/protobuf/blob/master/cmake/README.md#notes-on-compiler-warnings 148 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4251") # Warning C4251: 'identifier': class 'type' needs to have dll-interface to be used by clients of class 'type2' 149 | 150 | # /EHsc: Specifies the model of exception handling. 151 | # /GL: Enables whole program optimization. 152 | # /Gm: Enable minimal rebuilds; conflicts with /MP 153 | # /GS: Buffers security check. 154 | # /MD: Creates a multi-threaded DLL using MSVCRT.lib. 155 | # /MDd: Creates a debug multi-threaded DLL using MSVCRTD.lib. 156 | # /O2: Creates fast code. 157 | # /Od: Disables optimization. 158 | # /Oi: Generates intrinsic functions. 159 | # /RTC1: Enables run-time error checking. 160 | # /W4: Sets which warning level to output. 161 | # /Zi: Generates complete debugging information. 162 | # /JMC: Just my code enables the VS debugger to step over internal (system, framework, library, and other non-user) calls. 163 | set(CMAKE_CXX_FLAGS "/EHsc /GS /Zi /Gm-") #/W4 164 | set(CMAKE_CXX_FLAGS_DEBUG "/Od /RTC1 /JMC") 165 | set(CMAKE_CXX_FLAGS_RELEASE "/GL /O2 /Oi") 166 | 167 | message(STATUS "USING THESE CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}") 168 | endif() 169 | 170 | if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 171 | # MSVC warning suppressions 172 | add_definitions(-D_CRT_SECURE_NO_WARNINGS) 173 | add_definitions(-D_SCL_SECURE_NO_WARNINGS) 174 | add_definitions(-D_SILENCE_CXX20_IS_POD_DEPRECATION_WARNING) # protobuf triggers this warning 175 | add_definitions(-DNOMINMAX) 176 | 177 | #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++ ") # -lc++abi 178 | endif() 179 | 180 | # /MP: build with multiple processors 181 | # https://docs.microsoft.com/en-us/cpp/build/reference/mp-build-with-multiple-processes?view=msvc-160 182 | if(CMAKE_GENERATOR MATCHES "Visual Studio" AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 183 | if(CMake_MSVC_PARALLEL) 184 | if(CMake_MSVC_PARALLEL GREATER 0) 185 | string(APPEND CMAKE_C_FLAGS " /MP${CMake_MSVC_PARALLEL}") 186 | string(APPEND CMAKE_CXX_FLAGS " /MP${CMake_MSVC_PARALLEL}") 187 | else() 188 | string(APPEND CMAKE_C_FLAGS " /MP") 189 | string(APPEND CMAKE_CXX_FLAGS " /MP") 190 | endif() 191 | endif() 192 | endif() 193 | 194 | # ================================================================================================== 195 | # Linker flags 196 | # ================================================================================================== 197 | 198 | # Link static runtime libraries 199 | #set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") # /MD 200 | 201 | if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 202 | add_link_options("/nologo") 203 | add_link_options("/VERBOSE:LIB") 204 | endif() 205 | 206 | # ================================================================================================== 207 | # Setup Dependencies 208 | # ================================================================================================== 209 | 210 | message(STATUS "\n-- Dependencies:\n") 211 | 212 | # 213 | # Steamworks SDK 214 | # 215 | set(STEAMWORKS_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/dependencies/sw_sdk/public") 216 | set(STEAMWORKS_REDISTBIN_DIR "${CMAKE_SOURCE_DIR}/dependencies/sw_sdk/redistributable_bin") 217 | 218 | configure_file("${CMAKE_SOURCE_DIR}/resources/steam_appid.txt" "steam_appid.txt" COPYONLY) 219 | 220 | if(WIN32) 221 | set(STEAMWORKS_LIBRARY "${STEAMWORKS_REDISTBIN_DIR}/win64/steam_api64.lib") 222 | configure_file("${STEAMWORKS_REDISTBIN_DIR}/win64/steam_api64.dll" "steam_api64.dll" COPYONLY) 223 | else() 224 | set(STEAMWORKS_LIBRARY "${STEAMWORKS_REDISTBIN_DIR}/linux64/libsteam_api.so") 225 | configure_file("${STEAMWORKS_LIBRARY}" "libsteam_api.so" COPYONLY) 226 | endif() 227 | 228 | find_package(Steamworks REQUIRED) 229 | 230 | # 231 | # Google Protobuf 232 | # 233 | set(protobuf_MSVC_STATIC_RUNTIME on) 234 | 235 | if(MSVC AND protobuf_MSVC_STATIC_RUNTIME) 236 | set(CompilerFlags 237 | CMAKE_CXX_FLAGS 238 | CMAKE_CXX_FLAGS_DEBUG 239 | CMAKE_CXX_FLAGS_RELEASE 240 | CMAKE_CXX_FLAGS_MINSIZEREL 241 | CMAKE_CXX_FLAGS_RELWITHDEBINFO 242 | CMAKE_C_FLAGS 243 | CMAKE_C_FLAGS_DEBUG 244 | CMAKE_C_FLAGS_RELEASE 245 | CMAKE_C_FLAGS_MINSIZEREL 246 | CMAKE_C_FLAGS_RELWITHDEBINFO 247 | ) 248 | foreach(CompilerFlag ${CompilerFlags}) 249 | string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}") 250 | set(${CompilerFlag} "${${CompilerFlag}}" CACHE STRING "msvc compiler flags" FORCE) 251 | message("MSVC flags: ${CompilerFlag}: ${${CompilerFlag}}") 252 | endforeach() 253 | endif() 254 | 255 | # 256 | # Protobuf is non-trivial to include. 257 | # 258 | set(Protobuf_USE_STATIC_LIBS ON) 259 | set(Protobuf_DIR "${VCPKG_DIR}/share/protobuf") 260 | list(APPEND CMAKE_PREFIX_PATH "${VCPKG_DIR}/tools/protobuf") 261 | 262 | # 263 | # Please fix your protobuf_generate(), okay? 264 | # https://stackoverflow.com/questions/52533396/cmake-cant-find-protobuf-protobuf-generate-cpp 265 | # 266 | set(protobuf_MODULE_COMPATIBLE ON CACHE BOOL "") 267 | 268 | find_program(Protobuf_PROTOC_EXECUTABLE 269 | NAMES protoc 270 | DOC "Google Protocol Buffers Compiler" 271 | PATHS 272 | "${VCPKG_DIR}/tools/protobuf" 273 | "${VCPKG_ROOT}/packages/protobuf_${VCPKG_TARGET_TRIPLET}/tools/protobuf" 274 | ) 275 | 276 | if(NOT Protobuf_PROTOC_EXECUTABLE) 277 | if(WIN32) 278 | set(PROTOC "${VCPKG_DIR}/tools/protobuf/protoc.exe") 279 | set(Protobuf_PROTOC_EXECUTABLE "${VCPKG_DIR}/tools/protobuf/protoc.exe") 280 | else() 281 | set(PROTOC "${VCPKG_DIR}/tools/protobuf/protoc") 282 | set(Protobuf_PROTOC_EXECUTABLE "${VCPKG_DIR}/tools/protobuf/protoc") 283 | endif() 284 | endif() 285 | 286 | message(STATUS "Found protoc: ${Protobuf_PROTOC_EXECUTABLE}") 287 | 288 | # Find Protobuf package 289 | find_package(Protobuf CONFIG REQUIRED) 290 | 291 | # 292 | # SteamDatabase Protobufs Data Files 293 | # 294 | include(FetchContent) 295 | 296 | # Define the repository and the destination directory 297 | FetchContent_Declare(steamdatabase_protobufs 298 | GIT_REPOSITORY "https://github.com/SteamDatabase/Protobufs.git" 299 | GIT_TAG "origin/master" 300 | ) 301 | FetchContent_MakeAvailable(steamdatabase_protobufs) 302 | message("--[CSGO Protobuf] Fetched SteamDatabase Protobufs Data Files to steamdatabase_protobufs_SOURCE_DIR: ${steamdatabase_protobufs_SOURCE_DIR}") 303 | 304 | # Patch CSGO Protobufs 305 | # After patch was applied ${Protobuf_INCLUDE_DIRS} should contain the file "patch_applied_marker". 306 | include(PatchCsGoProtobufs) 307 | if(NOT EXISTS "${steamdatabase_protobufs_SOURCE_DIR}/csgo/patch_applied_marker") 308 | patch_csgo_protobufs() 309 | else() 310 | message("--[CSGO Protobuf] Already patched. Skipping.") 311 | endif() 312 | 313 | # let the custom target depend on the patching run 314 | add_custom_target(patch_steamdatabase_csgo_protobufs ALL DEPENDS "${steamdatabase_protobufs_SOURCE_DIR}/csgo/patch_applied_marker") 315 | 316 | # 317 | # Compile CS:GO Protobufs (proto -> cpp) 318 | # 319 | protobuf_generate_cpp(PROTO_ENGINE_GCMESSAGES_SRC PROTO_ENGINE_GCMESSAGES_HDR 320 | ${steamdatabase_protobufs_SOURCE_DIR}/csgo/engine_gcmessages.proto 321 | ) 322 | protobuf_generate_cpp(PROTO_CSTRIKE15_GCMESSAGES_SRC PROTO_CSTRIKE15_GCMESSAGES_HDR 323 | ${steamdatabase_protobufs_SOURCE_DIR}/csgo/cstrike15_gcmessages.proto 324 | ) 325 | protobuf_generate_cpp(PROTO_STEAMMESSAGES_SRC PROTO_STEAMMESSAGES_HDR 326 | ${steamdatabase_protobufs_SOURCE_DIR}/csgo/steammessages.proto 327 | ) 328 | protobuf_generate_cpp(PROTO_GCSDK_GCMESSAGES_SRC PROTO_GCSDK_GCMESSAGES_HDR 329 | ${steamdatabase_protobufs_SOURCE_DIR}/csgo/gcsdk_gcmessages.proto 330 | ) 331 | protobuf_generate_cpp(PROTO_GCSYSTEMMSGS_SRC PROTO_GCSYSTEMMSGS_HDR 332 | ${steamdatabase_protobufs_SOURCE_DIR}/csgo/gcsystemmsgs.proto 333 | ) 334 | 335 | # 336 | # zlib 337 | # 338 | set(ZLIB_ROOT "${VCPKG_DIR}") 339 | find_package(ZLIB REQUIRED) 340 | 341 | # 342 | # Curl 343 | # 344 | set(CURL_DIR "${VCPKG_DIR}/share/curl") 345 | find_package(CURL CONFIG REQUIRED) 346 | 347 | # 348 | # Download "cacert.pem" for Curl -> "curl-ca-bundle.crt" 349 | # 350 | if(NOT EXISTS "${CMAKE_BINARY_DIR}/curl-ca-bundle.crt") 351 | message(STATUS "Downloading: https://curl.haxx.se/ca/cacert.pem -> curl-ca-bundle.crt") 352 | file(DOWNLOAD "https://curl.haxx.se/ca/cacert.pem" "${CMAKE_BINARY_DIR}/curl-ca-bundle.crt" TLS_VERIFY ON) 353 | endif() 354 | 355 | # 356 | # FMT 357 | # 358 | set(fmt_DIR "${VCPKG_DIR}/share/fmt") 359 | find_package(fmt CONFIG REQUIRED) 360 | 361 | # 362 | # nlohman-json 363 | # 364 | set(nlohmann_json_DIR "${VCPKG_DIR}/share/nlohmann_json") 365 | find_package(nlohmann_json CONFIG REQUIRED) 366 | 367 | # 368 | # spdlog 369 | # 370 | set(spdlog_DIR "${VCPKG_DIR}/share/spdlog") 371 | find_package(spdlog CONFIG REQUIRED) 372 | 373 | # 374 | # Copy Additional Files 375 | # 376 | configure_file("${CMAKE_SOURCE_DIR}/README.md" "readme.txt" COPYONLY) 377 | configure_file("${CMAKE_SOURCE_DIR}/LICENSE" "license.txt" COPYONLY) 378 | configure_file("${CMAKE_SOURCE_DIR}/resources/update.bat" "update.bat" COPYONLY) 379 | 380 | # 381 | # Add Resources to Executable 382 | # 383 | if(WIN32) 384 | set(ADDITIONAL_RESOURCES "resources/CSGO_CLI.rc") 385 | else() 386 | set(ADDITIONAL_RESOURCES "") 387 | endif() 388 | 389 | #------------------------------------------------------------------- 390 | # Build Target: csgo_cli_lib (library) 391 | #------------------------------------------------------------------- 392 | 393 | add_library(csgo_cli_lib 394 | # base 395 | src/DataObject.h 396 | src/DataObject.cpp 397 | src/DateTimeUtils.h 398 | src/DateTimeUtils.cpp 399 | src/ErrorHandler.h 400 | src/ExceptionHandler.h 401 | src/ExceptionHandler.cpp 402 | src/ShareCode.h 403 | src/ShareCode.cpp 404 | src/SteamId.h 405 | src/SteamId.cpp 406 | src/VersionAndConstants.h # = ${CMAKE_CURRENT_BINARY_DIR}/src/VersionAndConstants.h 407 | # commands 408 | src/commands/cmd.help.h 409 | src/commands/cmd.help.cpp 410 | src/commands/cmd.user.h 411 | src/commands/cmd.user.cpp 412 | src/commands/cmd.upload.h 413 | src/commands/cmd.upload.cpp 414 | src/commands/cmd.matches.h 415 | src/commands/cmd.matches.cpp 416 | src/commands/cmd.scoreboard.h 417 | src/commands/cmd.scoreboard.cpp 418 | src/commands/cmd.globalstats.h 419 | src/commands/cmd.globalstats.cpp 420 | # csgo api 421 | src/csgo/CSGOClient.h 422 | src/csgo/CSGOClient.cpp 423 | src/csgo/GCMsgHandler.h 424 | src/csgo/CSGOMMHello.h 425 | src/csgo/CSGOMMHello.cpp 426 | src/csgo/CSGOMatchList.h 427 | src/csgo/CSGOMatchList.cpp 428 | src/csgo/CSGOMatchData.h 429 | src/csgo/CSGOMatchPlayerScore.h 430 | src/csgo/CSGOMatchPlayerScore.cpp 431 | src/csgo/CSGORankUpdate.cpp 432 | src/csgo/CSGORankUpdate.h 433 | # csgostats 434 | src/csgostats/ShareCodeUpload.h 435 | src/csgostats/ShareCodeUpload.cpp 436 | src/csgostats/ShareCodeCache.h 437 | src/csgostats/ShareCodeCache.cpp 438 | # windows platform 439 | src/platform/windows/WinCliColors.h 440 | src/platform/windows/WinCliColors.cpp 441 | 442 | # Generated Protobuf Includes 443 | ${PROTO_ENGINE_GCMESSAGES_SRC} 444 | ${PROTO_ENGINE_GCMESSAGES_HDR} 445 | ${PROTO_CSTRIKE15_GCMESSAGES_SRC} 446 | ${PROTO_CSTRIKE15_GCMESSAGES_HDR} 447 | ${PROTO_STEAMMESSAGES_SRC} 448 | ${PROTO_STEAMMESSAGES_HDR} 449 | ${PROTO_GCSDK_GCMESSAGES_SRC} 450 | ${PROTO_GCSDK_GCMESSAGES_HDR} 451 | ${PROTO_GCSYSTEMMSGS_SRC} 452 | ${PROTO_GCSYSTEMMSGS_HDR} 453 | ) 454 | 455 | # Ensure that any targets that depend on the patched protobufs are built after the patching is done 456 | add_dependencies(csgo_cli_lib patch_steamdatabase_csgo_protobufs) 457 | 458 | # 459 | # Process CS:GO Protobufs 460 | # See comment above @ "protobuf_MODULE_COMPATIBLE" 461 | # 462 | #protobuf_generate( 463 | # LANGUAGE cpp 464 | # TARGET csgo_cli_lib 465 | # PROTOS 466 | # ${SteamDatabase_Protobufs_SOURCE_DIR}/csgo/steammessages.proto 467 | # ${SteamDatabase_Protobufs_SOURCE_DIR}/csgo/gcsdk_gcmessages.proto 468 | # ${SteamDatabase_Protobufs_SOURCE_DIR}/csgo/gcsystemmsgs.proto 469 | # ${SteamDatabase_Protobufs_SOURCE_DIR}/csgo/engine_gcmessages.proto 470 | # ${SteamDatabase_Protobufs_SOURCE_DIR}/csgo/cstrike15_gcmessages.proto 471 | #) 472 | 473 | target_include_directories(csgo_cli_lib 474 | PUBLIC 475 | ${STEAMWORKS_INCLUDE_DIR} 476 | ${PROTOBUF_INCLUDE_DIR} 477 | ${CURL_INCLUDE_DIR} 478 | ${fmt_INCLUDE_DIRS} 479 | ) 480 | 481 | target_link_libraries(csgo_cli_lib 482 | PRIVATE 483 | fmt::fmt 484 | #spdlog::spdlog 485 | nlohmann_json::nlohmann_json 486 | CURL::libcurl 487 | ZLIB::ZLIB 488 | ${STEAMWORKS_LIBRARY} 489 | protobuf::libprotobuf 490 | ) 491 | 492 | set_property(TARGET csgo_cli_lib PROPERTY 493 | MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 494 | 495 | target_compile_features(csgo_cli_lib PUBLIC cxx_std_20) 496 | 497 | if(MSVC) 498 | if(ENABLE_ASAN) 499 | target_compile_options(csgo_cli_lib PUBLIC -fsanitize=address) 500 | endif() 501 | endif() 502 | 503 | target_link_options(csgo_cli_lib 504 | PRIVATE 505 | $<$: /NODEFAULTLIB:LIBCMTD;> 506 | #$<$: /NODEFAULTLIB:LIBCMT;> 507 | ) 508 | 509 | 510 | #------------------------------------------------------------------- 511 | # Build Target: csgo_cli (app) 512 | #------------------------------------------------------------------- 513 | 514 | add_executable(csgo_cli 515 | # icon 516 | ${ADDITIONAL_RESOURCES} 517 | # application files 518 | src/main/main.cpp 519 | ) 520 | 521 | target_include_directories(csgo_cli 522 | PUBLIC 523 | ${STEAMWORKS_INCLUDE_DIR} 524 | ) 525 | 526 | target_link_libraries(csgo_cli 527 | PRIVATE 528 | csgo_cli_lib 529 | fmt::fmt-header-only 530 | ) 531 | 532 | set_property(TARGET csgo_cli PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 533 | 534 | target_compile_features(csgo_cli PUBLIC cxx_std_20) 535 | 536 | if(MSVC) 537 | if(ENABLE_ASAN) 538 | target_compile_options(csgo_cli PUBLIC -fsanitize=address) 539 | endif() 540 | endif() 541 | 542 | # 543 | # Create "source-group" folders for these files in Visual Studio 544 | # 545 | # source_group("CSGO Protobuf Protocol Files" FILES 546 | # proto/engine_gcmessages.proto 547 | # proto/cstrike15_gcmessages.proto 548 | # proto/steammessages.proto 549 | # proto/gcsdk_gcmessages.proto 550 | # proto/gcsystemmsgs.proto 551 | # ) 552 | 553 | #------------------------------------------------------------------- 554 | # Tests 555 | #------------------------------------------------------------------- 556 | 557 | if(BUILD_TESTS) 558 | add_subdirectory(tests) 559 | endif() 560 | 561 | #------------------------------------------------------------------- 562 | # Installation Rules: csgo_cli (app) 563 | #------------------------------------------------------------------- 564 | 565 | include(GNUInstallDirs) 566 | 567 | install(TARGETS csgo_cli RESOURCE) 568 | 569 | install(FILES "${CMAKE_BINARY_DIR}/readme.txt" DESTINATION ${CMAKE_INSTALL_BINDIR}) 570 | install(FILES "${CMAKE_BINARY_DIR}/license.txt" DESTINATION ${CMAKE_INSTALL_BINDIR}) 571 | install(FILES "${CMAKE_BINARY_DIR}/update.bat" DESTINATION ${CMAKE_INSTALL_BINDIR}) 572 | install(FILES "${CMAKE_BINARY_DIR}/curl-ca-bundle.crt" DESTINATION ${CMAKE_INSTALL_BINDIR}) 573 | install(FILES "${CMAKE_BINARY_DIR}/steam_appid.txt" DESTINATION ${CMAKE_INSTALL_BINDIR}) 574 | 575 | if(WIN32) 576 | install(FILES "${CMAKE_BINARY_DIR}/steam_api64.dll" DESTINATION ${CMAKE_INSTALL_BINDIR}) 577 | else() 578 | install(FILES "${CMAKE_BINARY_DIR}/libsteam_api.so" DESTINATION ${CMAKE_INSTALL_BINDIR}) 579 | endif() 580 | 581 | #------------------------------------------------------------------- 582 | # Display Compiler and Linker properties of Build Targets 583 | #------------------------------------------------------------------- 584 | 585 | show_build_target_properties(csgo_cli_lib) 586 | show_build_target_properties(csgo_cli) -------------------------------------------------------------------------------- /CPPLINT.cfg: -------------------------------------------------------------------------------- 1 | # Do not search for other config files. 2 | set noparent 3 | 4 | # You have 30 inch TFTs at the Google HQ. Stop using 80. 5 | linelength=120 6 | includeorder=standardcfirst 7 | 8 | # 9 | # You can list all filters with: 10 | # cpplint --filter=. 11 | # ------------------------------------------------------- 12 | 13 | # Disable is an unapproved C++11 header warnings 14 | filter=-build/c++11 15 | 16 | filter=+build/include_what_you_use 17 | filter=+build/include_alpha 18 | filter=-build/include_order 19 | filter=-build/include_subdir 20 | 21 | filter=-runtime/references 22 | 23 | filter=-whitespace 24 | 25 | exclude_files=src/steamapi/steamapi.cpp 26 | exclude_files=src/steamapi/steamapi.h 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # csgo-cli ![GitHub Release](https://img.shields.io/github/v/release/jakoch/csgo-cli?style=flat-square&label=Latest%20Release) [![License](https://img.shields.io/github/license/jakoch/csgo-cli.svg)](https://github.com/jakoch/csgo-cli/blob/main/LICENSE) 2 | 3 | This console tool enables you to output pieces of information about your CS:GO user profile and latest matches. 4 | You can also use the tool to upload demo sharecodes to https://csstats.gg/ (formerly csgostats.gg). 5 | 6 | | Branch | CI | Code Quality | Spell Check | 7 | |--------|----|--------------|-------------| 8 | | [![Version](https://img.shields.io/badge/dynamic/json?url=https://raw.githubusercontent.com/jakoch/csgo-cli/main/vcpkg.json&label=Dev%20Version%20(main)&query=$[%27version%27]&color=blue)](https://github.com/jakoch/csgo-cli/releases/latest) | [![Build](https://github.com/jakoch/csgo-cli/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/jakoch/csgo-cli/actions/workflows/build.yml) | [![C++ Quality](https://github.com/jakoch/csgo-cli/actions/workflows/cpp-quality.yml/badge.svg)](https://github.com/jakoch/csgo-cli/actions/workflows/cpp-quality.yml) | [![Spell Check](https://github.com/jakoch/csgo-cli/actions/workflows/check-spelling.yml/badge.svg)](https://github.com/jakoch/csgo-cli/actions/workflows/check-spelling.yml) 9 | 10 | ## Installation 11 | 12 | The installation is easy. It's just a zipped folder containing the application. 13 | 14 | Download the zip file, extract it to a location of your choice and then run the app. 15 | 16 | - Download the latest release of `csgo-cli-vX.Y.Z.zip` from Github: https://github.com/jakoch/csgo-cli/releases/latest 17 | - Extract the zip file. Switch to the "csgo_cli" folder. 18 | - Then run `csgo_cli` on the command-line interface or just run `update.bat` to upload your recent matches. 19 | 20 | ## Requirements 21 | 22 | - Windows 23 | - Steam application running 24 | - CS:GO application not running 25 | 26 | ## Usage 27 | 28 | Display commands: 29 | 30 | ```bash 31 | csgo_cli 32 | ``` 33 | 34 | Output: 35 | 36 | ```bash 37 | csgo_cli v1.0.3, https://github.com/jakoch/csgo-cli 38 | Copyright (c) 2018-2019 Jens A. Koch. 39 | 40 | CS:GO Console shows your user account, stats and latest matches. 41 | You can also use the tool to upload demo sharecodes to csgostats.gg. 42 | 43 | Usage: 44 | command [options] [arguments] 45 | 46 | Available commands: 47 | -user Show your profile (SteamID, AccountID, MM-Rank, Likes, VAC-Status) 48 | -matches Show your past matches in table form 49 | -upload Upload your past matches to csgostats.gg 50 | -s, sharecode Upload a demo sharecode to csgostats.gg 51 | 52 | Options: 53 | -h, help Display this help message 54 | -v, verbose Increase verbosity of messages 55 | -V, Version Display application version 56 | ``` 57 | 58 | Fetch matches and upload to csgostats (or just use `update.bat`): 59 | 60 | ```bash 61 | csgo_cli -matches -upload 62 | ``` 63 | 64 | If you encounter any issues, please use the verbose mode for debugging purposes: 65 | 66 | ```bash 67 | csgo_cli -matches -upload -verbose 68 | ``` 69 | 70 | ## How does this work internally? 71 | 72 | The tool connects to your running Steam as CS:GO game client (SteamApp 730). 73 | It communicates with the Steam API to request the serialized player and match infos. 74 | The structure of the serialized data is described by the csgo-protobufs. 75 | These infos are then deserialized using Protobuf and placed into 76 | iterable objects for further processing and output. 77 | 78 | #### Automatic Upload of Demo ShareCodes to http://csgostats.gg/ 79 | 80 | The ShareCode is an URL, which you might pass around to your friends. 81 | 82 | It looks like so: steam://rungame/730/76561202255233023/+csgo_download_match%20CSGO-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx 83 | 84 | If you click on this URL or open it in a browser tab it will tell Steam to launch CS:GO, 85 | automatically download the demo and open it for replay. 86 | The code at the end of the URL is the id of the match. 87 | 88 | The tool requests your latest matches and builds the ShareCodes for the demos. 89 | The ShareCodes are then automatically uploaded to https://csgostats.gg/ 90 | 91 | This avoids the manual posting of the sharecode via the csgostats webinterface 92 | or the posting to the csgostats steam group, where the bots pick it up. 93 | https://csgostats.gg/getting-the-sharecode 94 | 95 | ##### Development Notes: ShareCode Uploading to csgostats.gg 96 | 97 | The file containing the logic for uploading the ShareCode is ShareCodeUpload.cpp. 98 | 99 | uploadShareCode() uses cURL to POST the ShareCode. 100 | Posting data to csgostats.gg is difficult because the server is Cloudflare protected. 101 | Even normal browsing behavior can trigger a cloudflare redirect to a captcha page or a website ban. 102 | 103 | Before we can POST one or multiple sharecodes, a GET request to csgostats.gg is needed to get a cURL connection handle, including all relevant cookies. 104 | The cURL handle is then re-used for one or more POST requests (sending the cookies as header data and the sharecode(s) as post data). 105 | 106 | The response is then parsed by processJsonResponse(). 107 | There are 4 response possibilities: 108 | There is a HTML response by Cloudflare, the HTML captcha page. 109 | There are 3 JSON response types by csgostats.gg: error, queued, complete. See testProcessJsonResponse() 110 | 111 | For testing purposes: Posting a ShareCode to csgostats.gg using cURL on the CLI 112 | - `curl "https://csgostats.gg/match/upload/ajax" -H "accept-language: en" -H "content-type: application/x-www-form-urlencoded; charset=UTF-8" -H "accept: application/json, text/javascript, */*; q=0.01" -H "x-requested-with: XMLHttpRequest" --data "sharecode=CSGO-WSACM-qX5Gv-ikbi3-Z6uOW-TGwPB&index=0"` 113 | 114 | ## Dependencies 115 | 116 | - [Steamworks SDK](https://partner.steamgames.com/) [API Docs](https://partner.steamgames.com/doc/sdk/api) 117 | - The official Steamworks SDK is used for making the Steam API connection and requesting data. 118 | - [Google Protobuf](https://developers.google.com/protocol-buffers/) 119 | - A language-neutral, platform-neutral, extensible mechanism for serializing structured data. 120 | - Protobuf is used for deserializing the player and match infos. 121 | - [CS:GO Protobufs](https://github.com/SteamDatabase/Protobufs/tree/master/csgo) 122 | - The csgo-protobufs are provided by the [steamdb.info](https://steamdb.info/) project. 123 | - The Protobuf Compiler (protoc) is used during the build process to compile the csgo-protobufs to C++ objects for data access. 124 | - [nlohmann/json](https://github.com/nlohmann/json) 125 | - nlohmann/json is used to parse the JSON responses from the csgostats server (error, complete, queued). 126 | - [ConsoleTable](https://github.com/766F6964/ConsoleTable) (inside /src) 127 | - ConsoleTable is used to print tables to the console. 128 | - [Curl](https://github.com/curl/curl) 129 | - libcurl is used for transferring data. 130 | - [CMake](https://cmake.org/) 131 | - CMake is used as build tool. 132 | - [Github Actions](https://docs.github.com/en/actions) 133 | - Github Actions is used as build and deployment service. 134 | 135 | ##### Release Checklist 136 | 137 | Before releasing a new version: 138 | 139 | - [ ] Update Version number in vcpkg.json 140 | - [ ] Update CHANGELOG.md 141 | 142 | # Todo 143 | 144 | - [x] CI setup 145 | - [x] cmake 146 | - [x] ~appveyor.yaml~ - switched to Github Actions @ 09/2021 147 | - [x] ~VC15 x64~ 148 | - [x] added Github Actions (./github/workflows) 149 | - [x] MSVC22 - VC17 - x64-windows-static 150 | - [x] vendor dependencies 151 | - [x] steam sdk 152 | - [x] encrypt 153 | - [x] decrypt via env secret 154 | - [x] protobuf 155 | - [x] cs:go protobufs 156 | - [x] patch cs:go protobuf syntax 157 | - [x] curl 158 | - [x] ~rapidjson~ 159 | - [x] nlohmann-json 160 | - [x] fmt 161 | - [x] spdlog 162 | - [x] package and deploy the application 163 | - [x] package and deploy the dependencies 164 | - [x] exclude sw_sdk (not redistributable), ship encrypted version 165 | - [x] connect as CSGO GameClient to Steam 166 | - [x] fetch player info 167 | - [x] calculate steamids 168 | - [x] fetch latest matches 169 | - [x] calculate demo share-codes 170 | - [x] upload demo share-codes to csgostats.gg 171 | - [x] curl based share-code uploader 172 | - [x] ~rapid-json based response parser~ 173 | - [x] nlohmann-json based response parser 174 | - [x] to avoid re-posting sharecodes (ShareCodeCache) 175 | - [ ] store matches locally (json, cvs, sqlite) 176 | - [ ] request additional steam profile data via web-api 177 | - [ ] colors on the CLI (LOSS red, WIN green) 178 | -------------------------------------------------------------------------------- /build-tools/cspell/cspell.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", 3 | "version": "0.2", 4 | "id": "csgo-cli-cspell-config", 5 | "name": "csgo-cli cspell Config", 6 | "language": "en-US,de-DE", 7 | "globRoot": "../../", 8 | "files": [ 9 | "*.md", 10 | "docs/**/*.md", 11 | "src/**/*.h", 12 | "src/**/*.cpp", 13 | "tests/**/*.h", 14 | "tests/**/*.cpp" 15 | ], 16 | "ignorePaths": [ 17 | ".git/*/**", 18 | ".git/!(COMMIT_EDITMSG)", 19 | ".vscode/**", 20 | ".clang-format", 21 | "*.bat", 22 | "*.cmake", 23 | "*.json", 24 | "*.lock", 25 | "*.log", 26 | "Changelog.md", 27 | "CMakeLists.txt", 28 | "CPPLINT.cfg", 29 | "cspell.json", 30 | "cspell*.{json,yaml}", 31 | "build-tools/**", 32 | "cmake/**", 33 | "dependencies/**", 34 | "downloads/**", 35 | "node_modules/**", 36 | "out/**", 37 | "resources/**", 38 | "vcpkg_packages/**", 39 | "src/ShareCode.cpp", 40 | "src/ShareCode.h", 41 | "src/steamapi/steamapi.cpp", 42 | "src/VersionAndConstants.h.in", 43 | "tests/ShareCodeUpload.tests.cpp" 44 | ], 45 | "dictionaryDefinitions": [ 46 | { 47 | "name": "repo-words", 48 | "addWords": true, 49 | "scope": [ 50 | "workspace" 51 | ], 52 | "path": "./repo-words.txt", 53 | "description": "A list of words related to this repository." 54 | } 55 | ], 56 | "dictionaries": [ 57 | "repo-words" 58 | ], 59 | "languageSettings": [ 60 | { 61 | "languageId": "*", 62 | "allowCompoundWords": false 63 | }, 64 | { 65 | "languageId": "markdown", 66 | "caseSensitive": true 67 | } 68 | ], 69 | "patterns": [ 70 | { 71 | "name": "src-csgo-headers", 72 | "pattern": "SRC_CSGO_[A-Z]+_H_" 73 | }, 74 | { 75 | "name": "src-csgostats-headers", 76 | "pattern": "SRC_CSGOSTATS_[A-Z]+_H_" 77 | }, 78 | { 79 | "name": "src-headers", 80 | "pattern": "SRC_[A-Z]+_H_" 81 | }, 82 | { 83 | "name": "src-platform-windows-headers", 84 | "pattern": "SRC_PLATFORM_WINDOWS_[A-Z]+_H_" 85 | } 86 | ], 87 | "ignoreRegExpList": [ 88 | "src-headers", "src-csgo-headers", "src-csgostats-headers", "src-platform-windows-headers" 89 | ] 90 | } -------------------------------------------------------------------------------- /build-tools/cspell/repo-words.txt: -------------------------------------------------------------------------------- 1 | accountid 2 | APIKEY 3 | appveyor 4 | BINARYNAME 5 | bitnum 6 | cloudflare 7 | cmake 8 | commentpermission 9 | communityvisibilitystate 10 | complete 11 | cout 12 | csgo 13 | CSGOMM 14 | csgostats 15 | dangerzone 16 | DATETIMEUTILS 17 | EDITMSG 18 | ERRORHANDLER 19 | Gametype 20 | globalstats 21 | headshot 22 | headshots 23 | hellothread 24 | HWND 25 | ikbi 26 | infos 27 | jakoch 28 | Jens 29 | libcurl 30 | mapgroup 31 | mapnames 32 | matchid 33 | matchinfo 34 | matchlist 35 | matchthread 36 | matchtime 37 | Minorly 38 | MINV 39 | mlocati 40 | mmhello 41 | MPIR 42 | MSVC 43 | mvps 44 | nlohmann 45 | ntdll 46 | NTSTATUS 47 | OSVERSIONINFOW 48 | Overwatch 49 | playername 50 | profileurl 51 | protobuf 52 | protobufs 53 | Protobufs 54 | protoc 55 | PRTL 56 | rapidjson 57 | redistributable 58 | replaylink 59 | reservationid 60 | roundstats 61 | roundstatsall 62 | rungame 63 | sharecode 64 | sharecodes 65 | Sharecodes 66 | spdlog 67 | sqlite 68 | steamapi 69 | steamdb 70 | STEAMGAMECOORDINATOR 71 | steamid 72 | steamids 73 | Steamworks 74 | toolset 75 | tvport 76 | vcpkg 77 | watchablematchinfo 78 | webinterface 79 | WSACM 80 | -------------------------------------------------------------------------------- /cmake/FindSteamworks.cmake: -------------------------------------------------------------------------------- 1 | # This script locates the Steamworks SDK 2 | # -------------------------------------- 3 | # 4 | # usage: 5 | # find_package(Steamworks ...) 6 | # 7 | # searches in STEAMWORKS_ROOT and usual locations 8 | # 9 | # Sets STEAMWORKS_INCLUDE_DIR 10 | # STEAMWORKS_LIBRARY_STATIC 11 | # STEAMWORKS_LIBRARY_DYNAMIC 12 | 13 | set(STEAMWORKS_POSSIBLE_PATHS 14 | ${STEAMWORKS_SDK} 15 | $ENV{STEAMWORKS_SDK} 16 | ) 17 | 18 | find_path(STEAMWORKS_INCLUDE_DIR 19 | NAMES steam/steam_api.h 20 | PATH_SUFFIXES "public/steam" 21 | PATHS ${STEAMWORKS_POSSIBLE_PATHS} 22 | ) 23 | 24 | find_library(STEAMWORKS_LIBRARY 25 | NAMES steam_api 26 | PATH_SUFFIXES "redistributable_bin" 27 | PATHS ${STEAMWORKS_POSSIBLE_PATHS} 28 | ) 29 | 30 | MARK_AS_ADVANCED(STEAMWORKS_INCLUDE_DIR STEAMWORKS_LIBRARIES) 31 | 32 | find_package_handle_standard_args(Steamworks DEFAULT_MSG STEAMWORKS_LIBRARY STEAMWORKS_INCLUDE_DIR) -------------------------------------------------------------------------------- /cmake/PatchCsGoProtobufs.cmake: -------------------------------------------------------------------------------- 1 | function(patch_csgo_protobufs) 2 | message("--[CSGO Protobuf] Patch CSGO Protobuf Files") 3 | 4 | #message("CSGO Protobufs are in SteamDatabase_Protobufs_SOURCE_DIR: ${steamdatabase_protobufs_SOURCE_DIR}") 5 | #message("Google Protobuf Descriptor is in Google Protobuf_INCLUDE_DIRS: ${Protobuf_INCLUDE_DIRS}/google/protobuf/descriptor.proto") 6 | 7 | # Step 1: Copy the Protobuf Descriptor file 8 | message("--[CSGO Protobuf] 1. Copy the Protobuf Descriptor file from the Protobuf repository...") 9 | 10 | # Copy the descriptor.proto file from Protobuf_INCLUDE_DIRS to SteamDatabase_Protobufs_SOURCE_DIR 11 | set(DESCRIPTOR_FILE "${Protobuf_INCLUDE_DIRS}/google/protobuf/descriptor.proto") 12 | file(MAKE_DIRECTORY "${steamdatabase_protobufs_SOURCE_DIR}/google/protobuf") 13 | file(COPY "${DESCRIPTOR_FILE}" DESTINATION "${steamdatabase_protobufs_SOURCE_DIR}/google/protobuf") 14 | 15 | # Step 2: Define the patch content 16 | set(PATCH_CONTENT "syntax = \"proto2\";\n") 17 | 18 | # Step 3: Prepend syntax patch to every CSGO .proto file 19 | message("--[CSGO Protobuf] 2. Patch all CSGO protobuf files of the SteamDatabase_Protobufs repository...") 20 | file(GLOB PROTO_FILES "${steamdatabase_protobufs_SOURCE_DIR}/csgo/*.proto") 21 | 22 | foreach(PROTO_FILE ${PROTO_FILES}) 23 | file(READ ${PROTO_FILE} PROTO_FILE_CONTENT) 24 | file(WRITE ${PROTO_FILE} "${PATCH_CONTENT}${PROTO_FILE_CONTENT}") 25 | endforeach() 26 | 27 | message("--[CSGO Protobuf] 3. Create a marker file to indicate that the patch has been applied.") 28 | file(TOUCH "${steamdatabase_protobufs_SOURCE_DIR}/csgo/patch_applied_marker") 29 | 30 | endfunction() 31 | -------------------------------------------------------------------------------- /cmake/ShowBuildTargetProperties.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Show Build Target Properties 3 | # 4 | # Usage: 5 | # add_executable(my_exe WIN32 ${APP_SOURCES}) 6 | # show_build_target_properties(my_exe) 7 | # 8 | 9 | function(show_build_target_property target property) 10 | if(NOT TARGET ${target}) 11 | message("There is no target named '${target}'.") 12 | return() 13 | endif() 14 | 15 | get_target_property(values ${target} ${property}) 16 | if(values) 17 | if(NOT "${values}" STREQUAL "${property}-NOTFOUND") 18 | message(STATUS "[${target}] ${property} -> '${values}'") 19 | endif() 20 | endif() 21 | endfunction() 22 | 23 | function(show_build_target_properties target) 24 | message(STATUS "|") 25 | message(STATUS "[INFO] Properties of Build Target '${target}':") 26 | set(properties 27 | SOURCE_DIR 28 | BINARY_DIR 29 | INCLUDE_DIRECTORIES 30 | LINK_LIBRARIES 31 | LINK_FLAGS 32 | COMPILE_OPTIONS 33 | COMPILE_DEFINITIONS 34 | CMAKE_EXE_LINKER_FLAGS 35 | ) 36 | foreach (prop ${properties}) 37 | show_build_target_property(${target} ${prop}) 38 | endforeach() 39 | endfunction() 40 | -------------------------------------------------------------------------------- /cmake/setup-vcpkg.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # SetupVcpkg 3 | # 4 | # This configures vcpkg using environment variables instead of a command-line options. 5 | # 6 | # https://github.com/microsoft/vcpkg/blob/master/docs/users/integration.md#using-an-environment-variable-instead-of-a-command-line-option 7 | # 8 | # Environment Variables: https://vcpkg.readthedocs.io/en/latest/users/config-environment/ 9 | # 10 | 11 | if(DEFINED ENV{VCPKG_VERBOSE} AND NOT DEFINED VCPKG_VERBOSE) 12 | set(VCPKG_VERBOSE "$ENV{VCPKG_VERBOSE}" CACHE BOOL "") 13 | endif() 14 | 15 | # 16 | # -- Automatic install of vcpkg dependencies. 17 | # 18 | # This is experimental. 19 | # See https://github.com/Microsoft/vcpkg/issues/1653 20 | # 21 | 22 | set(VCPKG_APPLOCAL_DEPS_INSTALL ON) 23 | 24 | # Copy dependencies into the output directory for executables. 25 | if(DEFINED ENV{VCPKG_APPLOCAL_DEPS} AND NOT DEFINED VCPKG_APPLOCAL_DEPS) 26 | set(VCPKG_APPLOCAL_DEPS "$ENV{VCPKG_APPLOCAL_DEPS}" CACHE BOOL "") 27 | endif() 28 | 29 | # Copy dependencies into the install target directory for executables. 30 | if(DEFINED ENV{X_VCPKG_APPLOCAL_DEPS_INSTALL} AND NOT DEFINED VCPKG_APPLOCAL_DEPS) 31 | 32 | # X_VCPKG_APPLOCAL_DEPS_INSTALL depends on CMake policy CMP0087 33 | if(POLICY CMP0087) 34 | cmake_policy(SET CMP0087 NEW) 35 | endif() 36 | 37 | set(X_VCPKG_APPLOCAL_DEPS_INSTALL "$ENV{X_VCPKG_APPLOCAL_DEPS_INSTALL}" CACHE BOOL "") 38 | endif() 39 | 40 | # 41 | # -- Set "vcpkg.cmake" as CMAKE_TOOLCHAIN_FILE 42 | # 43 | # Please set VCPKG_ROOT on your env: export VCPKG_ROOT=/opt/vcpkg/bin 44 | # This avoids passing it on the configure line: -DCMAKE_TOOLCHAIN_FILE=C:\vcpkg\scripts\buildsystems\vcpkg.cmake 45 | # 46 | if(NOT DEFINED CMAKE_TOOLCHAIN_FILE) 47 | 48 | # VCPKG_ROOT 49 | if(DEFINED VCPKG_ROOT AND EXISTS ${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake) 50 | message(STATUS "[VCPKG] Using system vcpkg at ${VCPKG_ROOT}: ${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake") 51 | set(vcpkg_toolchain_file "${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "") 52 | 53 | # ENV.VCPKG_ROOT 54 | elseif(DEFINED ENV{VCPKG_ROOT} AND EXISTS $ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake) 55 | message(STATUS "[VCPKG] Using system vcpkg at ENV{VCPKG_ROOT}: $ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake") 56 | set(vcpkg_toolchain_file "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "") 57 | 58 | # VCPKG_INSTALLATION_ROOT is defined on Github Actions CI 59 | elseif (DEFINED ENV{VCPKG_INSTALLATION_ROOT} AND EXISTS $ENV{VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake) 60 | message(STATUS "[VCPKG] Using system vcpkg at Github Action VCPKG_INSTALLATION_ROOT: $ENV{VCPKG_INSTALLATION_ROOT}") 61 | set(vcpkg_toolchain_file $ENV{VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake) 62 | 63 | # otherwise VCPKG is missing and we install VCPKG from Github 64 | #else() 65 | # message(STATUS "[vcpkg] Fetching latest VCPKG from Github...") 66 | # include(FetchContent) 67 | # FetchContent_Declare(vcpkg GIT_REPOSITORY https://github.com/microsoft/vcpkg.git) 68 | # FetchContent_MakeAvailable(vcpkg) 69 | # set(vcpkg_toolchain_file ${vcpkg_SOURCE_DIR}/scripts/buildsystems/vcpkg.cmake) 70 | endif() 71 | 72 | set(CMAKE_TOOLCHAIN_FILE ${vcpkg_toolchain_file}) 73 | endif() 74 | 75 | # 76 | # -- VCPKG_DEFAULT_TRIPLET 77 | # 78 | if(DEFINED ENV{VCPKG_DEFAULT_TRIPLET} AND NOT DEFINED VCPKG_TARGET_TRIPLET) 79 | set(VCPKG_TARGET_TRIPLET "$ENV{VCPKG_DEFAULT_TRIPLET}" CACHE STRING "") 80 | endif() 81 | 82 | # 83 | # -- VCPKG_DIR 84 | # 85 | # VCPKG_DIR is the root folder for all compiled packages, e.g. 86 | # the local /project/vcpkg_packages/x64-windows 87 | # or the global /opt/vcpkg/packages/x64-windows. 88 | # 89 | # Because finding dependencies automatically is still on the todo list of vcpkg, we need to guide it. 90 | # 91 | # Please use this variable to point to the locations of your packages share folder like below: 92 | # set(spdlog_DIR "${VCPKG_DIR}/share/spdlog") 93 | # find_package(spdlog CONFIG REQUIRED) 94 | # 95 | if(NOT DEFINED VCPKG_DIR) 96 | if(WIN32) 97 | set(VCPKG_DIR "${CMAKE_SOURCE_DIR}/vcpkg_packages/${VCPKG_TARGET_TRIPLET}") 98 | else() 99 | set(VCPKG_DIR "${CMAKE_SOURCE_DIR}/build/${CMAKE_BUILD_TYPE}/vcpkg_packages/${VCPKG_TARGET_TRIPLET}") 100 | endif() 101 | endif() 102 | 103 | iF(NOT DEFINED VCPKG_MANIFEST_FILE) 104 | set(VCPKG_MANIFEST_FILE "${CMAKE_SOURCE_DIR}/vcpkg.json") 105 | endif() 106 | 107 | # Add this file and the VCPKG_MANIFEST_FILE as a "vcpkg" source_group to the IDE. 108 | # They are not automatically picked up and listed as "important project" files by IDEs, yet. 109 | source_group("vcpkg" FILES 110 | "${CMAKE_SOURCE_DIR}/cmake/SetupVcpkg.cmake" 111 | "${CMAKE_SOURCE_DIR}/vcpkg.json" 112 | ) 113 | 114 | # 115 | # Check to make sure the VCPKG_TARGET_TRIPLET matches BUILD_SHARED_LIBS 116 | # 117 | if (DEFINED VCPKG_TARGET_TRIPLET) 118 | if ("${VCPKG_TARGET_TRIPLET}" MATCHES ".*-static") 119 | if (BUILD_SHARED_LIBS) 120 | message(FATAL_ERROR "When the VCPKG_TARGET_TRIPLET ends with '-static' the BUILD_SHARED_LIBS must be 'OFF'.") 121 | endif() 122 | else() 123 | if (NOT BUILD_SHARED_LIBS) 124 | message(FATAL_ERROR "When the VCPKG_TARGET_TRIPLET does not end with '-static' the BUILD_SHARED_LIBS must be 'ON'.") 125 | endif() 126 | endif() 127 | endif() 128 | 129 | # 130 | # Print VCPKG configuration overview 131 | # 132 | message(STATUS "\n-- [VCPKG] Configuration Overview:\n") 133 | message(STATUS "[VCPKG] - VCPKG_VERBOSE -> '${VCPKG_VERBOSE}'") 134 | message(STATUS "[VCPKG] - VCPKG_APPLOCAL_DEPS -> '${VCPKG_APPLOCAL_DEPS}'") 135 | message(STATUS "[VCPKG] - E:VCPKG_FEATURE_FLAGS -> '$ENV{VCPKG_FEATURE_FLAGS}'") 136 | message(STATUS "[VCPKG] - E:VCPKG_ROOT -> '$ENV{VCPKG_ROOT}'") 137 | message(STATUS "[VCPKG] - E:VCPKG_INSTALLATION_ROOT -> '$ENV{VCPKG_INSTALLATION_ROOT}'") 138 | message(STATUS "[VCPKG] - CMAKE_TOOLCHAIN_FILE -> '${CMAKE_TOOLCHAIN_FILE}'") 139 | message(STATUS "[VCPKG] - VCPKG_MANIFEST_FILE -> '${VCPKG_MANIFEST_FILE}'") 140 | message(STATUS "[VCPKG] - VCPKG_TARGET_TRIPLET -> '${VCPKG_TARGET_TRIPLET}'") 141 | message(STATUS "[VCPKG] - VCPKG_DIR -> '${VCPKG_DIR}'") 142 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "build-tools/cspell/cspell.config.json" 4 | ] 5 | } -------------------------------------------------------------------------------- /downloads/sw_sdk_153a.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakoch/csgo-cli/310f77532c7a64cdef060a7e2a9d3c7e1383a4be/downloads/sw_sdk_153a.zip -------------------------------------------------------------------------------- /format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Allow ENV.CLANG_FORMAT to define the path to the binary or default to clang-format 4 | CLANG_FORMAT=${CLANG_FORMAT:-clang-format} 5 | 6 | # Check clang-format version 7 | VERSION=$("$CLANG_FORMAT" --version) 8 | 9 | # Require clang-format 17 or 18 for consistent formatting features 10 | if [[ ! $VERSION =~ "version 17" && ! $VERSION =~ "version 18" ]]; then 11 | echo "Error: Unsupported clang-format version. Must be version 17 or 18." 12 | echo "Found version: $VERSION" 13 | exit 1 14 | fi 15 | 16 | find src -name *.h -o -name *.cpp -exec dos2unix {} \; 17 | find src -name *.h -o -name *.cpp|xargs $CLANG_FORMAT -i -style=file 18 | -------------------------------------------------------------------------------- /resources/CSGO_CLI.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakoch/csgo-cli/310f77532c7a64cdef060a7e2a9d3c7e1383a4be/resources/CSGO_CLI.rc -------------------------------------------------------------------------------- /resources/csgo_cli.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakoch/csgo-cli/310f77532c7a64cdef060a7e2a9d3c7e1383a4be/resources/csgo_cli.ico -------------------------------------------------------------------------------- /resources/steam_appid.txt: -------------------------------------------------------------------------------- 1 | 730 -------------------------------------------------------------------------------- /resources/update.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | csgo_cli -matches -upload 4 | 5 | pause -------------------------------------------------------------------------------- /src/DataObject.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "DataObject.h" 5 | 6 | std::string DataObject::getPlayerRank(int rank_type_id) 7 | { 8 | int rank_id; 9 | if (rank_type_id == 6) { 10 | rank_id = rankings.at(0).id; 11 | } 12 | if (rank_type_id == 7) { 13 | rank_id = rankings.at(1).id; 14 | } 15 | if (rank_type_id == 10) { 16 | rank_id = rankings.at(2).id; 17 | } 18 | 19 | int rank = (rank_id < 0) ? rank_id - 1 : rank_id; 20 | 21 | return getRankName(rank); 22 | } 23 | 24 | std::string DataObject::getPlayerLevel() 25 | { 26 | int level = (player_level < 0) ? player_level - 1 : player_level; 27 | 28 | return getLevelName(level); 29 | } 30 | 31 | int const DataObject::calcPlayerXpBase() const 32 | { 33 | return player_cur_xp - 327680000; // xp minus base value, gives normalized player xp 34 | } 35 | 36 | std::string DataObject::getPlayerXp() 37 | { 38 | return std::to_string(calcPlayerXpBase()); 39 | } 40 | 41 | float const DataObject::getPlayerXpPercentage() 42 | { 43 | return (static_cast(calcPlayerXpBase()) / 5000) * 100; 44 | } 45 | 46 | std::string DataObject::getVacStatus() 47 | { 48 | return (vac_banned == 1) ? "banned" : "ok"; 49 | } 50 | 51 | std::string DataObject::getLevelName(int i) 52 | { 53 | return levels[i]; 54 | } 55 | 56 | std::string DataObject::getRankName(int i) 57 | { 58 | return ranks[i]; 59 | } 60 | 61 | std::string DataObject::getRankType(int i) 62 | { 63 | if (i == 6) { 64 | return "MatchMaking"; 65 | } 66 | if (i == 7) { 67 | return "Wingman"; 68 | } 69 | if (i == 10) { 70 | return "DangerZone"; 71 | } 72 | return "Unknown RankType"; 73 | } 74 | 75 | std::string DataObject::getDangerzoneRankName(int i) 76 | { 77 | return dangerzone_ranks[i]; 78 | } 79 | 80 | std::string DataObject::getSteamId() 81 | { 82 | return std::to_string(steam_id); 83 | } 84 | 85 | std::string DataObject::getSteamProfileUrl() 86 | { 87 | return steam_profile_url_base + getSteamId(); 88 | } 89 | std::string DataObject::getCanDoOverwatch() 90 | { 91 | RankingInfo matchmaking_rank = rankings.at(0); 92 | int rank_id = matchmaking_rank.id; 93 | int rank_wins = matchmaking_rank.wins; 94 | 95 | if (rank_id < 7) { 96 | return "Your rank is too low. " + getRankName(7) + " required."; 97 | } 98 | if (rank_wins < 150) { 99 | return "You don't have enough wins: 150 required."; 100 | } 101 | return "Qualified to request replays."; 102 | } 103 | std::string DataObject::getAverageSearchTime() 104 | { 105 | return format_duration_get_minutes(global_stats.search_time_avg); 106 | } 107 | 108 | std::string DataObject::getPenaltyReasonShort(int i) 109 | { 110 | return penalty_reasons_short[i]; 111 | } 112 | 113 | std::string DataObject::getPenaltyReasonLong(int i) 114 | { 115 | return penalty_reasons_long[i]; 116 | } 117 | 118 | /*std::string DataObject::getDemoFilename(const CDataGCCStrike15_v2_MatchInfo& 119 | match, const CMsgGCCStrike15_v2_MatchmakingServerRoundStats& roundstats) { 120 | std::ostringstream out; 121 | out << "match730_"; 122 | out << std::setfill('0') << std::setw(21) << roundstats.reservationid(); 123 | out << "_" << std::setw(10) << match.watchablematchinfo().tv_port(); 124 | out << "_" << match.watchablematchinfo().server_ip() << ".dem"; 125 | return out.str(); 126 | }*/ 127 | -------------------------------------------------------------------------------- /src/DataObject.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_DATAOBJECT_H_ 5 | #define SRC_DATAOBJECT_H_ 6 | 7 | #include "DateTimeUtils.h" 8 | #include "csgo/CSGOMatchData.h" // for vector matches 9 | #include "cstrike15_gcmessages.pb.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | class DataObject 17 | { 18 | private: 19 | std::vector const ranks = { 20 | "Not Ranked", 21 | "Silver 1", 22 | "Silver 2", 23 | "Silver 3", 24 | "Silver 4", 25 | "Silver Elite", 26 | "Silver Elite Master", 27 | "Gold Nova 1", 28 | "Gold Nova 2", 29 | "Gold Nova 3", 30 | "Gold Nova 4", 31 | "Master Guardian 1", // single AK 32 | "Master Guardian 2", // AK with wings 33 | "Master Guardian Elite", // double AK 34 | "Distinguished Master Guardian", // star 35 | "Legendary Eagle", // eagle 36 | "Legendary Eagle Master", // eagle with wings 37 | "Supreme Master First Class", // small globe 38 | "Global Elite", // big globe 39 | }; 40 | 41 | std::vector const levels = { 42 | "Not Recruited", "Recruit", "Private I", "Private II", 43 | "Private III", "Corporal I", "Corporal II", "Corporal III", 44 | "Corporal IV", "Sergeant I", "Sergeant II", "Sergeant III", 45 | "Sergeant IV", "Master Sergeant I", "Master Sergeant II", "Master Sergeant III", 46 | "Master Sergeant IV", "Sergeant Major I", "Sergeant Major II", "Sergeant Major III", 47 | "Sergeant Major IV", "Lieutenant I", "Lieutenant II", "Lieutenant III", 48 | "Lieutenant IV", "Captain I", "Captain II", "Captain III", 49 | "Captain IV", "Major I", "Major II", "Major III", 50 | "Major IV", "Colonel I", "Colonel II", "Colonel III", 51 | "Brigadier General", "Major General", "Lieutenant General", "General", 52 | "Global General"}; 53 | 54 | std::vector const dangerzone_ranks = { 55 | "Hidden", 56 | "Lab Rat I", 57 | "Lab Rat II", 58 | "Sprinting Hare I", 59 | "Sprinting Hare II", 60 | "Wild Scout I", 61 | "Wild Scout II", 62 | "Wild Scout Elite", 63 | "Hunter Fox I", 64 | "Hunter Fox II", 65 | "Hunter Fox II", 66 | "Hunter Fox Elite", 67 | "Timber Wolf", 68 | "Ember Wolf", 69 | "Wildfire Wolf", 70 | "The Howling Alpha"}; 71 | 72 | std::vector const penalty_reasons_long = { 73 | "Temporary Matchmaking Cooldown (No reason)", 74 | "You have been kicked from your last matchmaking game.", 75 | "You killed too many teammates.", 76 | "You killed a teammate at round start.", 77 | "You failed to reconnect to your last match.", 78 | "You abandoned your last match.", 79 | "You dealt too much damage to your teammates.", 80 | "You dealt too much damage to your teammates at round start.", 81 | "Your account is permanently untrusted. (Illegal Angles)", 82 | "You were kicked from too many recent matches.", 83 | "Convicted by Overwatch: Majorly Disruptive", 84 | "Convicted by Overwatch: Minorly Disruptive", 85 | "Resolving Matchmaking state for your account.", 86 | "Resolving Matchmaking state for your last match.", 87 | "Your account is permanently untrusted. (VAC)", 88 | "Permanent Matchmaking Cooldown (No reason)", 89 | "You failed to connect by match start.", 90 | "You kicked too many teammates in recent matches.", 91 | "Your account is under skill placement calibration.", 92 | "A server using your game server token has been banned."}; 93 | 94 | std::vector const penalty_reasons_short = { 95 | "CooldownNone" 96 | "Kicked" 97 | "KilledMate" 98 | "RoundStartKill" 99 | "FailReconnect" 100 | "Abandon" 101 | "DamagedMate" 102 | "DamagedMateStart" 103 | "UntrustedAngles" 104 | "KickedTooMuch" 105 | "MajorlyDisruptive" 106 | "MinorlyDisruptive" 107 | "ResolveState" 108 | "ResolveStateLastMatch" 109 | "UntrustedVac" 110 | "PermanentCooldownNone" 111 | "FailConnect" 112 | "KickedMates" 113 | "NewbieCooldown" 114 | "GameServerBanned"}; 115 | 116 | std::string const steam_profile_url_base = "https://steamcommunity.com/profiles/"; 117 | 118 | // TODO(jakoch): add wingman. 119 | std::string const tpl_url_wingman_replays = "https://steamcommunity.com/id/{}/gcpd/730/?tab=matchhistorywingman"; 120 | 121 | int const calcPlayerXpBase() const; 122 | 123 | public: 124 | std::string getSteamId(); 125 | std::string getSteamProfileUrl(); 126 | std::string getPlayerLevel(); 127 | std::string getPlayerRank(int rank_type_id); 128 | std::string getPlayerXp(); 129 | float const getPlayerXpPercentage(); 130 | std::string getVacStatus(); 131 | std::string getLevelName(int i); 132 | std::string getRankName(int i); 133 | std::string getRankType(int i); 134 | std::string getDangerzoneRankName(int i); 135 | std::string getCanDoOverwatch(); 136 | std::string getAverageSearchTime(); 137 | std::string getPenaltyReasonShort(int i); 138 | std::string getPenaltyReasonLong(int i); 139 | 140 | // SteamUser 141 | uint32 account_id = 0; 142 | uint64 steam_id = 0; 143 | int steam_player_level = 0; 144 | std::string steam_profile_url; 145 | 146 | // SteamFriends -> playername, clan_name, clan_tag 147 | std::string playername; 148 | std::string clan_name; 149 | std::string clan_tag; 150 | 151 | // ranking 152 | struct RankingInfo 153 | { 154 | uint32 id = 0; 155 | uint32 type = 0; 156 | uint32 wins = 0; 157 | float change; 158 | }; 159 | std::vector rankings; 160 | 161 | // commendation 162 | uint32 cmd_friendly = 0; 163 | uint32 cmd_teaching = 0; 164 | uint32 cmd_leader = 0; 165 | 166 | // player level 167 | int32 player_level = 0; 168 | int32 player_cur_xp = 0; // starts at 327680000 (level % = (player_cur_xp - 327680000) / 5000) 169 | int32 player_xp_bonus_flags = 0; // TODO(jakoch): add bonus flag. 170 | 171 | // medals 172 | uint32 medals_arms = 0; 173 | uint32 medals_combat = 0; 174 | uint32 medals_global = 0; 175 | uint32 medals_team = 0; 176 | uint32 medals_weapon = 0; 177 | 178 | // vac status 179 | int32 vac_banned = 0; 180 | uint32 penalty_reason = 0; 181 | uint32 penalty_seconds = 0; 182 | 183 | // matches 184 | bool has_matches_played = 0; 185 | int num_matches_played = 0; 186 | std::vector matches; 187 | 188 | // statistics 189 | struct GameTypeStats 190 | { 191 | uint32 game_type = 0; 192 | uint32 players_searching = 0; 193 | uint32 search_time_avg = 0; 194 | }; 195 | struct GlobalStats 196 | { 197 | uint32 players_online = 0; 198 | uint32 servers_online = 0; 199 | uint32 players_searching = 0; 200 | uint32 servers_available = 0; 201 | uint32 ongoing_matches = 0; 202 | uint32 search_time_avg = 0; 203 | std::vector gameTypeStats; 204 | }; 205 | GlobalStats global_stats; 206 | }; 207 | #endif // SRC_DATAOBJECT_H_ 208 | -------------------------------------------------------------------------------- /src/DateTimeUtils.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "DateTimeUtils.h" 5 | 6 | std::string getYear() 7 | { 8 | auto now = std::chrono::system_clock::now(); 9 | auto in_time_t = std::chrono::system_clock::to_time_t(now); 10 | 11 | std::stringstream ss; 12 | ss << std::put_time(std::localtime(&in_time_t), "%Y"); 13 | return ss.str(); 14 | } 15 | 16 | std::string getDateTime(time_t const & time, char const * time_format) 17 | { 18 | std::stringstream ss; 19 | ss << std::put_time(std::localtime(&time), time_format); 20 | return ss.str(); 21 | } 22 | 23 | std::string format_duration_get_minutes(int msecs) 24 | { 25 | using std::chrono::duration_cast; 26 | using std::chrono::hours; 27 | using std::chrono::milliseconds; 28 | using std::chrono::minutes; 29 | using std::chrono::seconds; 30 | 31 | auto ms = milliseconds(msecs); 32 | auto secs = duration_cast(ms); 33 | ms -= duration_cast(secs); 34 | auto mins = duration_cast(secs); 35 | secs -= duration_cast(mins); 36 | auto hour = duration_cast(mins); 37 | mins -= duration_cast(hour); 38 | 39 | std::stringstream ss; 40 | // ss << hour.count() << " Hours : " 41 | // << mins.count() << " Minutes : " 42 | // << secs.count() << " Seconds : " 43 | // << ms.count() << " Milliseconds"; 44 | ss << mins.count() << "m " << secs.count() << "s"; 45 | return ss.str(); 46 | } 47 | -------------------------------------------------------------------------------- /src/DateTimeUtils.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_DATETIMEUTILS_H_ 5 | #define SRC_DATETIMEUTILS_H_ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | std::string getYear(); 14 | std::string getDateTime(time_t const & time, char const * time_format = "%Y-%m-%d %H:%M:%S"); 15 | std::string format_duration_get_minutes(int milliseconds); 16 | 17 | #endif // SRC_DATETIMEUTILS_H_ -------------------------------------------------------------------------------- /src/ErrorHandler.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_ERRORHANDLER_H_ 5 | #define SRC_ERRORHANDLER_H_ 6 | 7 | #include 8 | #include 9 | 10 | static inline void printError(char const * title, char const * text) 11 | { 12 | fprintf(stdout, "\x1B[91m%s:\033[0m %s\n", title, text); 13 | } 14 | 15 | #endif // SRC_ERRORHANDLER_H_ -------------------------------------------------------------------------------- /src/ExceptionHandler.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "ExceptionHandler.h" 5 | 6 | ExceptionHandler::ExceptionHandler(std::string const & what) : m_what(what) { } 7 | 8 | char const * ExceptionHandler::what() const throw() 9 | { 10 | return m_what.c_str(); 11 | } 12 | -------------------------------------------------------------------------------- /src/ExceptionHandler.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_EXCEPTIONHANDLER_H_ 5 | #define SRC_EXCEPTIONHANDLER_H_ 6 | 7 | #include 8 | #include 9 | 10 | class ExceptionHandler : public std::exception 11 | { 12 | public: 13 | explicit ExceptionHandler(std::string const & what); 14 | virtual char const * what() const throw(); 15 | 16 | private: 17 | std::string m_what; 18 | }; 19 | 20 | #endif // SRC_EXCEPTIONHANDLER_H_ 21 | -------------------------------------------------------------------------------- /src/ShareCode.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "ShareCode.h" 5 | 6 | void quotientAndRemainder(uint64_t& a0, uint64_t& a1, uint16_t& a2, uint16_t m, uint16_t& r) 7 | { 8 | r = 0; 9 | uint64_t q0 = 0; 10 | uint64_t q1 = 0; 11 | uint16_t q2 = 0; 12 | unsigned int const ull_bitnum = sizeof(uint64_t) * 8; 13 | unsigned int const us_bitnum = sizeof(uint16_t) * 8; 14 | 15 | for (int i = 2 * ull_bitnum + us_bitnum - 1; i >= 0; --i) { 16 | r <<= 1; 17 | bool a_ith_bit; 18 | if (i >= ull_bitnum + us_bitnum) { 19 | a_ith_bit = a0 & (1ull << (i - ull_bitnum - us_bitnum)); 20 | } else if (i >= us_bitnum) { 21 | a_ith_bit = a1 & (1ull << (i - us_bitnum)); 22 | } else { 23 | a_ith_bit = a2 & (1u << i); 24 | } 25 | r += a_ith_bit; 26 | if (r >= m) { 27 | r -= m; 28 | if (i >= ull_bitnum + us_bitnum) { 29 | q0 |= (1ull << (i - ull_bitnum - us_bitnum)); 30 | } else if (i >= us_bitnum) { 31 | q1 |= (1ull << (i - us_bitnum)); 32 | } else { 33 | q2 |= (1u << i); 34 | } 35 | } 36 | } 37 | a0 = q0; 38 | a1 = q1; 39 | a2 = q2; 40 | } 41 | 42 | /** 43 | * Generate a demo share code from its object data and return its string representation. 44 | * Required fields should come from a CDataGCCStrike15_v2_MatchInfo protobuf message. 45 | * https://github.com/SteamDatabase/Protobufs/blob/3a112d8ece0baa8facf6b503cccb0ad6af826b8f/csgo/cstrike15_gcmessages.proto#L942 46 | * 47 | * Steps to generate the share code: 48 | * 49 | * Required data from the protobuf message are: 50 | * - uint64 match_id 51 | * - uint64 reservationid from the last repeated entry in roundstatsall, or if that doesn�t exist from roundstats_legacy 52 | * - uint16 low bits of uint32 tv_port 53 | * 54 | * 1. From these 3 items, we generate a big 144-bit number. 55 | * 2. The big number has to be run with a base57 encoding process 56 | * against the string "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789". 57 | * 3. Construct ShareCode string 58 | * 59 | * @param matchId {Object|Long} matchId 60 | * @param reservationId {Object|Long} reservationId 61 | * @param tvPort number tvPort 62 | * @return {string} Share code as string 63 | */ 64 | std::string getShareCode(uint64_t matchid, uint64_t reservationid, uint32_t tvport) 65 | { 66 | // charset for base57 67 | std::string const dictionary = "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789"; 68 | 69 | std::string code; 70 | uint64_t matchid_reversed = _byteswap_uint64(matchid); 71 | uint64_t reservationid_reversed = _byteswap_uint64(reservationid); 72 | uint16_t tvport_reversed = _byteswap_ushort(*reinterpret_cast(&tvport)); 73 | uint16_t r = 0; 74 | uint16_t dl = dictionary.length(); 75 | 76 | for (int i = 0; i < 25; ++i) { 77 | quotientAndRemainder(matchid_reversed, reservationid_reversed, tvport_reversed, dl, r); 78 | code += dictionary[r]; 79 | // std::cout << "i " << i << " r " << r << " code " << code << std::endl; 80 | } 81 | 82 | // example: "CSGO-GADqf-jjyJ8-cSP2r-smZRo-TO2xK" 83 | char shareCode[35]; 84 | 85 | snprintf( 86 | shareCode, 87 | sizeof(shareCode), 88 | "CSGO-%s-%s-%s-%s-%s", 89 | code.substr(0, 5).c_str(), 90 | code.substr(5, 5).c_str(), 91 | code.substr(10, 5).c_str(), 92 | code.substr(15, 5).c_str(), 93 | code.substr(20).c_str()); 94 | 95 | return shareCode; 96 | } 97 | 98 | /* 99 | std::string fromDemoShareCode(std::string sharecode) 100 | { 101 | // std::string sharecode("CSGO-U6MWi-hYFWJ-opPwD-JciHm-qOijD"); 102 | // 3106049990460440633 103 | // 3106056003414655216 104 | // 11842 105 | 106 | sharecode = std::regex_replace(sharecode, std::regex("CSGO|-"), ""); 107 | sharecode = std::string(sharecode.rbegin(), sharecode.rend()); 108 | 109 | const std::string dictionary = 110 | "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789"; std::array result = {}; 112 | 113 | for (char cur_char : sharecode) 114 | { 115 | std::array tmp = {}; 116 | 117 | int addval = static_cast(dictionary.find(cur_char)); 118 | int carry = 0; 119 | int v = 0; 120 | 121 | for (int t = 17; t >= 0; t--) { 122 | carry = 0; 123 | for (int s = t; s >= 0; s--) { 124 | if (t - s == 0) { 125 | v = tmp[s] + result[t] * 57; 126 | } 127 | else { 128 | v = 0; 129 | } 130 | v = v + carry; 131 | carry = v >> 8; 132 | tmp[s] = v & 0xFF; 133 | } 134 | } 135 | 136 | result = tmp; 137 | carry = 0; 138 | 139 | for (int t = 17; t >= 0; t--) { 140 | if (t == 17) { 141 | v = result[t] + addval; 142 | } 143 | else { 144 | v = result[t]; 145 | } 146 | v = v + carry; 147 | carry = v >> 8; 148 | result[t] = v & 0xFF; 149 | } 150 | } 151 | 152 | uint64_t matchid = *reinterpret_cast(result.data()); 153 | uint64_t outcomeId = *reinterpret_cast(result.data() + 8); 154 | uint16_t tokenId = *reinterpret_cast(result.data() + 16); 155 | } 156 | */ 157 | -------------------------------------------------------------------------------- /src/ShareCode.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_SHARECODE_H_ 5 | #define SRC_SHARECODE_H_ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | void quotientAndRemainder(uint64_t& a0, uint64_t& a1, uint16_t& a2, uint16_t m, uint16_t& r); 14 | 15 | /** 16 | * Generate a demo share code from its object data and return its string representation. 17 | * Required fields should come from a CDataGCCStrike15_v2_MatchInfo protobuf message. 18 | * https://github.com/SteamDatabase/Protobufs/blob/3a112d8ece0baa8facf6b503cccb0ad6af826b8f/csgo/cstrike15_gcmessages.proto#L942 19 | * 20 | * Steps to generate the share code: 21 | * 22 | * Required data from the protobuf message are: 23 | * - uint64 match_id 24 | * - uint64 reservationid from the last repeated entry in roundstatsall, or if that doesn�t exist from roundstats_legacy 25 | * - uint16 low bits of uint32 tv_port 26 | * 27 | * 1. From these 3 items, we generate a big 144-bit number. 28 | * 2. The big number has to be run with a base57 encoding process 29 | * against the string "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789". 30 | * 3. Construct ShareCode string 31 | * 32 | * @param matchId {Object|Long} matchId 33 | * @param reservationId {Object|Long} reservationId 34 | * @param tvPort number tvPort 35 | * @return {string} Share code as string 36 | */ 37 | std::string getShareCode(uint64_t matchid, uint64_t reservationid, uint32_t tvport); 38 | 39 | /* 40 | std::string fromDemoShareCode(std::string sharecode); 41 | */ 42 | 43 | #endif // SRC_SHARECODE_H_ -------------------------------------------------------------------------------- /src/SteamId.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "SteamId.h" 5 | 6 | // 76561197960493477 -> 113874 7 | uint32 calcSteamID32(uint64 steamID64) 8 | { 9 | uint64 const steamID64Base = 76561197960265728; 10 | uint32 iSteamID32 = (steamID64 - steamID64Base); 11 | return iSteamID32; 12 | } 13 | 14 | // steam3ID: 76561197960493477 -> [U:1:227749] 15 | std::string toSteamID32(uint64 steamID64) 16 | { 17 | std::stringstream ss; 18 | ss << "[U:1:"; 19 | ss << calcSteamID32(steamID64); 20 | ss << "]"; 21 | return ss.str(); 22 | } 23 | 24 | // steamID32: STEAM_0:1:113874 25 | std::string toSteamIDClassic(uint64 steamID64) 26 | { 27 | // modulus operator = modulo(a,2) 28 | int a = (steamID64 % 2 == 0) ? 0 : 1; 29 | 30 | int b = std::floor(calcSteamID32(steamID64) / 2); 31 | 32 | std::stringstream ss; 33 | ss << "STEAM_0:" << a << ":" << b; 34 | return ss.str(); 35 | } 36 | -------------------------------------------------------------------------------- /src/SteamId.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_STEAMID_H_ 5 | #define SRC_STEAMID_H_ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // 76561197960493477 -> 113874 13 | uint32 calcSteamID32(uint64 steamID64); 14 | 15 | // steam3ID: 76561197960493477 -> [U:1:227749] 16 | std::string toSteamID32(uint64 steamID64); 17 | 18 | // steamID32: STEAM_0:1:113874 19 | std::string toSteamIDClassic(uint64 steamID64); 20 | 21 | #endif // SRC_STEAMID_H_ -------------------------------------------------------------------------------- /src/VersionAndConstants.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_VERSIONANDCONSTANTS_H_ 5 | #define SRC_VERSIONANDCONSTANTS_H_ 6 | 7 | // Application Name 8 | #define CSGO_CLI_BINARYNAME "csgo_cli" 9 | 10 | // Application Version 11 | #define CSGO_CLI_VERSION_MAJOR "1" 12 | #define CSGO_CLI_VERSION_MINOR "4" 13 | #define CSGO_CLI_VERSION_PATCH "0" 14 | 15 | #define CSGO_CLI_VERSION "1.4.0" 16 | 17 | // CURL user-agent identifier 18 | #define CSGO_CLI_WEBSITE "https://github.com/jakoch/csgo-cli" 19 | #define CSGO_CLI_USERAGENT_ID CSGO_CLI_BINARYNAME "/" CSGO_CLI_VERSION "; " CSGO_CLI_WEBSITE 20 | 21 | // STEAM API connection timeouts in milliseconds 22 | #define CSGO_CLI_STEAM_CALLBACK_INTERVAL 50 // time between callback requests in the loop 23 | #define CSGO_CLI_STEAM_CMSG_TIMEOUT 10000 // time to wait for answer from steam 24 | #define CSGO_CLI_STEAM_HELLO_DELAY 500 // wait before requesting matchmaking hello 25 | #define CSGO_CLI_STEAM_MATCHLIST_DELAY 500 // wait before requesting matchlist 26 | 27 | struct CSGO_CLI_TimeoutException 28 | { 29 | }; 30 | 31 | #endif // SRC_VERSIONANDCONSTANTS_H_ 32 | -------------------------------------------------------------------------------- /src/VersionAndConstants.h.in: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef VersionAndConstants_H 5 | #define VersionAndConstants_H 6 | 7 | // Application Name 8 | #define CSGO_CLI_BINARYNAME "csgo_cli" 9 | 10 | // Application Version 11 | #define CSGO_CLI_VERSION_MAJOR "@PROJECT_VERSION_MAJOR@" 12 | #define CSGO_CLI_VERSION_MINOR "@PROJECT_VERSION_MINOR@" 13 | #define CSGO_CLI_VERSION_PATCH "@PROJECT_VERSION_PATCH@" 14 | 15 | #define CSGO_CLI_VERSION "@PROJECT_VERSION@" 16 | 17 | // CURL user-agent identifier 18 | #define CSGO_CLI_WEBSITE "https://github.com/jakoch/csgo-cli" 19 | #define CSGO_CLI_USERAGENT_ID CSGO_CLI_BINARYNAME "/" CSGO_CLI_VERSION "; " CSGO_CLI_WEBSITE 20 | 21 | // STEAM API connection timeouts in milliseconds 22 | #define CSGO_CLI_STEAM_CALLBACK_INTERVAL 50 // time between callback requests in the loop 23 | #define CSGO_CLI_STEAM_CMSG_TIMEOUT 10000 // time to wait for answer from steam 24 | #define CSGO_CLI_STEAM_HELLO_DELAY 500 // wait before requesting matchmaking hello 25 | #define CSGO_CLI_STEAM_MATCHLIST_DELAY 500 // wait before requesting matchlist 26 | 27 | struct CSGO_CLI_TimeoutException 28 | { 29 | }; 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /src/commands/cmd.globalstats.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "cmd.globalstats.h" 5 | 6 | bool requestGlobalStats(DataObject& data, bool& verbose) 7 | { 8 | 9 | if (verbose) { 10 | spdlog::info("global stats are part of user profile data"); 11 | spdlog::info("[ Start ] [ Thread ] getUserInfo"); 12 | } 13 | 14 | bool result = false; 15 | 16 | auto hellothread = std::thread([&data, verbose, &result]() { 17 | try { 18 | std::this_thread::sleep_for(std::chrono::milliseconds(CSGO_CLI_STEAM_HELLO_DELAY)); 19 | 20 | CSGOMMHello mmhello; 21 | if (verbose) 22 | spdlog::info(" Requesting: Hello"); 23 | mmhello.RefreshWait(); 24 | if (verbose) 25 | spdlog::info(" Got Hello"); 26 | 27 | result = true; 28 | 29 | if (verbose) { 30 | spdlog::debug("mmhello.data.DebugString {}", mmhello.data.DebugString()); 31 | } 32 | 33 | data.global_stats.ongoing_matches = mmhello.data.global_stats().ongoing_matches(); 34 | data.global_stats.players_online = mmhello.data.global_stats().players_online(); 35 | data.global_stats.players_searching = mmhello.data.global_stats().players_searching(); 36 | data.global_stats.servers_online = mmhello.data.global_stats().servers_online(); 37 | data.global_stats.servers_available = mmhello.data.global_stats().servers_available(); 38 | data.global_stats.search_time_avg = mmhello.data.global_stats().search_time_avg(); 39 | 40 | // Detailed Search Statistics (players searching per game_type) 41 | // data.global_stats. mmhello.data.global_stats().search_statistics(); 42 | 43 | } catch (CSGO_CLI_TimeoutException) { 44 | printError("Warning", "Timeout on receiving UserInfo."); 45 | result = false; 46 | } catch (ExceptionHandler& e) { 47 | printError("Fatal error", e.what()); 48 | result = false; 49 | } 50 | if (verbose) 51 | spdlog::info("[ End ] [ Thread ] getUserInfo"); 52 | return 0; 53 | }); 54 | 55 | hellothread.join(); 56 | 57 | return result; 58 | } 59 | 60 | void printGlobalStats(DataObject& data) 61 | { 62 | // ---------- Format Output Strings 63 | 64 | // ---------- Output Table 65 | 66 | auto const printAligned{[=](std::string const & a, std::string const & b = "") { 67 | return fmt::print(" {0:<23} {1}\n", a, b); 68 | }}; 69 | 70 | fmt::print("\n Hello {}!\n", data.playername); 71 | fmt::print("\n Here are the CS:GO global statistics:\n\n"); 72 | 73 | printAligned("Players Online", std::to_string(data.global_stats.players_online)); 74 | printAligned(" "); 75 | printAligned("Servers Online", std::to_string(data.global_stats.servers_online)); 76 | printAligned("Servers Available", std::to_string(data.global_stats.servers_available)); 77 | printAligned(" "); 78 | printAligned("Players Searching", std::to_string(data.global_stats.players_searching)); 79 | printAligned("Ongoing Matches", std::to_string(data.global_stats.ongoing_matches)); 80 | printAligned("Average Search Time", data.getAverageSearchTime()); 81 | } -------------------------------------------------------------------------------- /src/commands/cmd.globalstats.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_COMMANDS_CMD_GLOBALSTATS_H_ 5 | #define SRC_COMMANDS_CMD_GLOBALSTATS_H_ 6 | 7 | #include "../csgo/CSGOMMHello.h" 8 | #include "../DataObject.h" 9 | #include "../ErrorHandler.h" 10 | #include "../ExceptionHandler.h" 11 | #include "../VersionAndConstants.h" 12 | #include 13 | 14 | bool requestGlobalStats(DataObject& data, bool& verbose); 15 | void printGlobalStats(DataObject& data); 16 | 17 | #endif // SRC_COMMANDS_CMD_GLOBALSTATS_H_ -------------------------------------------------------------------------------- /src/commands/cmd.help.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "cmd.help.h" 5 | 6 | void printHelp() 7 | { 8 | std::string const binary = WinCliColors::formatLightGreen(CSGO_CLI_BINARYNAME); 9 | std::string const version = WinCliColors::formatYellow(CSGO_CLI_VERSION); 10 | 11 | fmt::print("{} v{}, {}\n", binary, version, CSGO_CLI_WEBSITE); 12 | fmt::print("Copyright (c) 2018-{} Jens A. Koch.\n", getYear()); 13 | fmt::print("\n"); 14 | fmt::print(" CS:GO Console shows your user account, stats and latest matches.\n"); 15 | fmt::print(" You can also use the tool to upload replay sharecodes to csgostats.gg.\n"); 16 | fmt::print("\n"); 17 | fmt::print(" Usage:\n"); 18 | fmt::print(" command [options] [arguments]\n"); 19 | fmt::print("\n"); 20 | fmt::print(" Available commands:\n"); 21 | fmt::print(" -user Show your Steam and CS:GO profile\n"); 22 | fmt::print(" -matches Show your past matches in table form\n"); 23 | fmt::print(" -upload Upload your past matches to csgostats.gg\n"); 24 | fmt::print(" -s, sharecode Upload a replay sharecode to csgostats.gg\n"); 25 | // fmt::print(" -scoreboard Show your past matches in scoreboard form\n"); 26 | fmt::print(" -globalstats Show global server stats\n"); 27 | fmt::print("\n"); 28 | fmt::print(" -V, Version Display application version\n"); 29 | fmt::print(" -h, help Display this help message\n"); 30 | fmt::print("\n"); 31 | fmt::print("Options:\n"); 32 | fmt::print(" -v, verbose Increase verbosity of messages\n"); 33 | fmt::print(" -vv Raise verbosity level to debug\n"); 34 | fmt::print("\n"); 35 | } -------------------------------------------------------------------------------- /src/commands/cmd.help.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_COMMANDS_CMD_HELP_H_ 5 | #define SRC_COMMANDS_CMD_HELP_H_ 6 | 7 | #include "../DateTimeUtils.h" 8 | #include "../platform/windows/WinCliColors.h" 9 | #include "../VersionAndConstants.h" 10 | 11 | #include 12 | #include 13 | 14 | void printHelp(); 15 | 16 | #endif // SRC_COMMANDS_CMD_HELP_H_ -------------------------------------------------------------------------------- /src/commands/cmd.matches.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "cmd.matches.h" 5 | #include "spdlog/common.h" 6 | #include "spdlog/spdlog.h" 7 | 8 | bool requestRecentMatches(DataObject& data, bool& verbose) 9 | { 10 | if (verbose) { 11 | spdlog::info("[ Start ] [ Thread ] MatchList"); 12 | } 13 | 14 | bool result = false; 15 | 16 | auto matchthread = std::thread([&data, verbose, &result]() { 17 | try { 18 | std::this_thread::sleep_for(std::chrono::milliseconds(CSGO_CLI_STEAM_MATCHLIST_DELAY)); 19 | 20 | // refresh match list 21 | CSGOMatchList matchList; 22 | 23 | if (verbose) { 24 | spdlog::info(" -> requesting MatchList"); 25 | } 26 | matchList.RefreshWait(); 27 | if (verbose) { 28 | spdlog::info(" -> got MatchList"); 29 | } 30 | 31 | result = true; 32 | 33 | if (verbose) { 34 | spdlog::info("[ Start ] processing MatchList"); 35 | } 36 | 37 | // empty match history 38 | if (matchList.Matches().size() == 0) { 39 | data.has_matches_played = false; 40 | } else { 41 | data.has_matches_played = true; 42 | data.num_matches_played = matchList.Matches().size(); 43 | 44 | int matches_num = 1; 45 | 46 | for (auto& match : matchList.Matches()) { 47 | 48 | if (verbose) { 49 | spdlog::info("[ Start ] processing Match #{}", matches_num); 50 | } 51 | 52 | CSGOMatchData parsedMatch; 53 | parsedMatch.matchid = match.matchid(); 54 | parsedMatch.matchtime = match.matchtime(); 55 | parsedMatch.matchtime_str = getDateTime(parsedMatch.matchtime); 56 | // replay info 57 | parsedMatch.server_ip = match.watchablematchinfo().server_ip(); 58 | parsedMatch.tv_port = match.watchablematchinfo().tv_port(); 59 | // map 60 | parsedMatch.map = match.watchablematchinfo().game_map(); 61 | parsedMatch.mapgroup = match.watchablematchinfo().game_mapgroup(); 62 | parsedMatch.game_type = match.watchablematchinfo().game_type(); // this is nowadays 0 63 | 64 | if (verbose) { 65 | spdlog::info("{}", match.DebugString()); 66 | } 67 | 68 | // iterate roundstats 69 | CMsgGCCStrike15_v2_MatchmakingServerRoundStats roundStats; 70 | for (int i = 0; i < match.roundstatsall().size(); ++i) { 71 | roundStats = match.roundstatsall(i); 72 | 73 | // if (verbose) { spdlog::debug("roundStats.DebugString /n {}", roundStats.DebugString()); } 74 | 75 | // last round = scoreboard 76 | // match.scoreboard = match.roundstatsall(roundstatsall().size()); 77 | 78 | // WARNING: the game_type is the map name 79 | if (parsedMatch.game_type == 0) { 80 | parsedMatch.game_type = roundStats.reservation().game_type(); 81 | } 82 | 83 | // ROUNDSTATS per player 84 | for (auto& account_id : roundStats.reservation().account_ids()) { 85 | 86 | CSGOMatchPlayerScore player; 87 | player.index = matchList.getPlayerIndex(account_id, roundStats); 88 | player.account_id = account_id; 89 | player.steam_id = CSteamID(player.account_id, k_EUniversePublic, k_EAccountTypeIndividual) 90 | .ConvertToUint64(); 91 | player.kills = roundStats.kills(player.index); 92 | player.assists = roundStats.assists(player.index); 93 | player.deaths = roundStats.deaths(player.index); 94 | // player.kdr = player.kills/player.deaths; 95 | player.mvps = roundStats.mvps(player.index); 96 | player.score = roundStats.scores(player.index); 97 | 98 | parsedMatch.scoreboard.push_back(player); 99 | 100 | if (verbose) { 101 | spdlog::info("[ End ] Match-Player"); 102 | } 103 | } 104 | 105 | if (verbose) { 106 | spdlog::debug("match.roundstatsall {}: {}\n", i, match.roundstatsall(i).DebugString()); 107 | } 108 | } 109 | 110 | // RESERVATION ID (from last roundstats item) 111 | parsedMatch.reservation_id = roundStats.reservationid(); 112 | parsedMatch.match_duration = roundStats.match_duration(); // seconds 113 | parsedMatch.match_duration_str = getDateTime(parsedMatch.match_duration, "%M:%Sm"); 114 | parsedMatch.replaylink = roundStats.map(); // http link to the bz2 archived replay file 115 | 116 | parsedMatch.sharecode = 117 | getShareCode(parsedMatch.matchid, parsedMatch.reservation_id, parsedMatch.tv_port); 118 | 119 | if (matchList.getOwnIndex(roundStats) >= 5) { 120 | parsedMatch.score_ally = roundStats.team_scores(1); 121 | parsedMatch.score_enemy = roundStats.team_scores(0); 122 | } else { 123 | parsedMatch.score_ally = roundStats.team_scores(0); 124 | parsedMatch.score_enemy = roundStats.team_scores(1); 125 | } 126 | 127 | parsedMatch.result = roundStats.match_result(); 128 | parsedMatch.result_str = matchList.getMatchResult(roundStats); 129 | 130 | data.matches.push_back(parsedMatch); 131 | 132 | if (verbose) { 133 | spdlog::info("[ End ] processing Match #{}", matches_num); 134 | } 135 | matches_num++; 136 | } 137 | } 138 | if (verbose) { 139 | spdlog::info("[ End ] processing MatchList"); 140 | } 141 | } catch (CSGO_CLI_TimeoutException) { 142 | printError("Warning", "Timeout on receiving MatchList."); 143 | result = false; 144 | } catch (ExceptionHandler& e) { 145 | printError("Fatal Error", e.what()); 146 | result = false; 147 | } 148 | 149 | if (verbose) { 150 | spdlog::info("[ End ] [ Thread ] MatchList"); 151 | } 152 | 153 | return 0; 154 | }); 155 | 156 | matchthread.join(); 157 | 158 | return result; 159 | } 160 | 161 | void printMatches(DataObject& data) 162 | { 163 | fmt::print("\n Hello {}!\n\n", data.playername); 164 | 165 | if (!data.has_matches_played) { 166 | fmt::print(" Your CS:GO match history is empty.\n"); 167 | return; 168 | } 169 | 170 | if (data.num_matches_played == 1) { 171 | fmt::print(" Here is your latest match:\n"); 172 | } else { 173 | fmt::print(" Here are your {} latest matches:\n\n", data.num_matches_played); 174 | } 175 | 176 | auto const printRow{[=](std::string const & s1, 177 | std::string const & s2, 178 | std::string const & s3, 179 | std::string const & s4, 180 | std::string const & s5, 181 | std::string const & s6) { 182 | return fmt::print("{0:^3} {1:<20} {2:^8} {3:^13} {4:^8} {5:^6} \n", s1, s2, s3, s4, s5, s6); 183 | }}; 184 | 185 | printRow("#", "Match Played", "Duration", "Map", "Score", "Result\n"); 186 | 187 | int i = 1; 188 | for (auto const & match : data.matches) { 189 | printRow( 190 | std::to_string(i), 191 | match.matchtime_str, 192 | match.match_duration_str, 193 | match.getGameType(), // match.getMapname(), 194 | match.getScore(), 195 | match.getMatchResult()); 196 | ++i; 197 | } 198 | 199 | // std::to_string(match.matchid), 200 | //"Replaylink:" match.replaylink, 201 | //"Match IP:" << match.server_ip, 202 | //"Match Port:" << match.tv_port, 203 | //"Match Reservation ID:" << match.reservation_id, 204 | //"Replay ShareCode:" << match.sharecode, 205 | //"Mapgroup:" << match.mapgroup, 206 | //"Gametype:" << match.gametype 207 | } -------------------------------------------------------------------------------- /src/commands/cmd.matches.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_COMMANDS_CMD_MATCHES_H_ 5 | #define SRC_COMMANDS_CMD_MATCHES_H_ 6 | 7 | #include "../csgo/CSGOMatchList.h" 8 | #include "../DataObject.h" 9 | #include "../DateTimeUtils.h" 10 | #include "../ErrorHandler.h" 11 | #include "../ExceptionHandler.h" 12 | #include "../ShareCode.h" 13 | #include "../VersionAndConstants.h" 14 | #include 15 | 16 | #include 17 | 18 | bool requestRecentMatches(DataObject& data, bool& verbose); 19 | void printMatches(DataObject& data); 20 | 21 | #endif // SRC_COMMANDS_CMD_MATCHES_H_ -------------------------------------------------------------------------------- /src/commands/cmd.scoreboard.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "cmd.scoreboard.h" 5 | 6 | void printScoreboard(DataObject& data) 7 | { 8 | if (!data.has_matches_played) { 9 | fmt::print("\n Your CS:GO match history is empty.\n"); 10 | return; 11 | } 12 | 13 | // ---------- Output Table 14 | 15 | //{0:^3} {1:<20} {2:^8} {3:^5} {4:<9} {5:<7} 16 | 17 | auto const printRow{[=](std::string const & s1, 18 | std::string const & s2, 19 | std::string const & s3, 20 | std::string const & s4, 21 | std::string const & s5, 22 | std::string const & s6, 23 | std::string const & s7, 24 | std::string const & s8, 25 | std::string const & s9, 26 | std::string const & s10, 27 | std::string const & s11) { 28 | return fmt::print( 29 | " {0} {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} \n", s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11); 30 | }}; 31 | 32 | fmt::print("\n Hello {}!\n", data.playername); 33 | fmt::print("\n Here is your scoreboard:\n\n"); 34 | 35 | printRow("Match Played", "Result", "Score", "K", "A", "D", "HS(%)", "K/D", "Rating", "MVP", "Score"); 36 | 37 | for (auto const & match : data.matches) { 38 | for (auto const & player : match.scoreboard) { 39 | if (player.account_id == data.account_id) { 40 | fmt::print("{}\n", match.matchtime_str); 41 | /*printRow( 42 | match.matchtime_str, 43 | match.result_str, 44 | match.getScore(), 45 | player.getKills(), 46 | player.getAssists(), 47 | player.getDeaths(), 48 | "hs", // player.getHSRatio(), 49 | player.getKillDeathRatio(), 50 | "rating", 51 | player.getMVPs(), 52 | player.getScore());*/ 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/commands/cmd.scoreboard.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_COMMANDS_CMD_SCOREBOARD_H_ 5 | #define SRC_COMMANDS_CMD_SCOREBOARD_H_ 6 | 7 | #include "../DataObject.h" 8 | #include 9 | #include 10 | 11 | void printScoreboard(DataObject& data); 12 | 13 | #endif // SRC_COMMANDS_CMD_SCOREBOARD_H_ -------------------------------------------------------------------------------- /src/commands/cmd.upload.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "cmd.upload.h" 5 | 6 | static inline void uploadShareCode(std::string& sharecode, ShareCodeCache* matchCache, ShareCodeUpload* codeUpload) 7 | { 8 | if (matchCache->find(sharecode)) { 9 | auto msg1 = fmt::format(fmt::fg(fmt::color::indian_red), "Skipped."); 10 | auto msg2 = fmt::format(fmt::fg(fmt::color::green), "The ShareCode \"{}\" was already uploaded.", sharecode); 11 | fmt::print(" {} {}\n", msg1, msg2); 12 | return; 13 | } 14 | 15 | std::string jsonResponse; 16 | 17 | fmt::print(" Uploading ShareCode: {}\n", WinCliColors::formatTerminalYellow(sharecode)); 18 | 19 | if (codeUpload->uploadShareCode(sharecode, jsonResponse) == 0) { 20 | int upload_status = codeUpload->processJsonResponse(jsonResponse); 21 | 22 | if (upload_status == 4 || upload_status == 5) { // queued (in-progress) or complete 23 | matchCache->insert(sharecode); 24 | } else if (upload_status <= 3) { 25 | printError("Error", "Could not parse the response (to the replay sharecode POST request)."); 26 | } 27 | 28 | } else { 29 | printError("Error", "Could not POST replay sharecode."); 30 | } 31 | } 32 | 33 | void uploadReplayShareCodes(DataObject& data, bool& verbose) 34 | { 35 | if (!data.has_matches_played) { 36 | WinCliColors::printRed(" No replay sharecodes to upload.\n"); 37 | return; 38 | } 39 | 40 | fmt::print( 41 | "\n Uploading Replay ShareCode{} to https://csgostats.gg/: \n\n", (data.num_matches_played == 1) ? "" : "s"); 42 | 43 | ShareCodeCache* matchCache = new ShareCodeCache(verbose); 44 | ShareCodeUpload* codeUpload = new ShareCodeUpload(verbose); 45 | 46 | for (auto& match : data.matches) { 47 | uploadShareCode(match.sharecode, matchCache, codeUpload); 48 | } 49 | } 50 | 51 | void uploadSingleShareCode(std::string& sharecode, bool& verbose) 52 | { 53 | WinCliColors::printTerminalYellow("\n Uploading Single Replay ShareCode to https://csgostats.gg/: \n\n"); 54 | 55 | ShareCodeCache* matchCache = new ShareCodeCache(verbose); 56 | ShareCodeUpload* codeUpload = new ShareCodeUpload(verbose); 57 | 58 | uploadShareCode(sharecode, matchCache, codeUpload); 59 | } -------------------------------------------------------------------------------- /src/commands/cmd.upload.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_COMMANDS_CMD_UPLOAD_H_ 5 | #define SRC_COMMANDS_CMD_UPLOAD_H_ 6 | 7 | #include "../csgostats/ShareCodeCache.h" 8 | #include "../csgostats/ShareCodeUpload.h" 9 | #include "../DataObject.h" 10 | #include "../ErrorHandler.h" 11 | 12 | #include 13 | 14 | static inline void uploadShareCode(std::string& sharecode, ShareCodeCache* matchCache, ShareCodeUpload* codeUpload); 15 | 16 | void uploadReplayShareCodes(DataObject& data, bool& verbose); 17 | void uploadSingleShareCode(std::string& sharecode, bool& verbose); 18 | 19 | #endif // SRC_COMMANDS_CMD_UPLOAD_H_ 20 | -------------------------------------------------------------------------------- /src/commands/cmd.user.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "cmd.user.h" 5 | #include "cstrike15_gcmessages.pb.h" 6 | 7 | bool requestPlayersProfile(DataObject& data, bool& verbose) 8 | { 9 | if (verbose) 10 | spdlog::info("[ Start ] [ Thread ] getUserInfo"); 11 | 12 | bool result = false; 13 | 14 | auto hellothread = std::thread([&data, verbose, &result]() { 15 | try { 16 | std::this_thread::sleep_for(std::chrono::milliseconds(CSGO_CLI_STEAM_HELLO_DELAY)); 17 | 18 | CSGOMMHello mmhello; 19 | if (verbose) 20 | spdlog::info(" Requesting: Hello"); 21 | mmhello.RefreshWait(); 22 | if (verbose) 23 | spdlog::info(" Got Hello"); 24 | 25 | result = true; 26 | 27 | if (verbose) { 28 | spdlog::debug("mmhello.data.DebugString {}", mmhello.data.DebugString()); 29 | } 30 | 31 | // player level 32 | data.player_level = mmhello.data.player_level(); 33 | data.player_cur_xp = mmhello.data.player_cur_xp(); 34 | data.player_xp_bonus_flags = mmhello.data.player_xp_bonus_flags(); 35 | 36 | // medals 37 | /*if (mmhello.data.has_medals()) { 38 | data.medals_arms = mmhello.data.medals().medal_arms(); 39 | data.medals_combat = mmhello.data.medals().medal_combat(); 40 | data.medals_global = mmhello.data.medals().medal_global(); 41 | data.medals_team = mmhello.data.medals().medal_team(); 42 | data.medals_weapon = mmhello.data.medals().medal_weapon(); 43 | }*/ 44 | // vac status 45 | data.vac_banned = mmhello.data.vac_banned(); 46 | data.penalty_seconds = mmhello.data.penalty_seconds(); 47 | data.penalty_reason = mmhello.data.penalty_reason(); 48 | 49 | // ranks 50 | if (mmhello.data.has_ranking()) { 51 | spdlog::debug("mmhello.data.rankings() (MatchMaking) {}", mmhello.data.ranking().DebugString()); 52 | DataObject::RankingInfo ri; 53 | ri.id = mmhello.data.ranking().rank_id(); 54 | ri.type = mmhello.data.ranking().rank_type_id(); 55 | ri.wins = mmhello.data.ranking().wins(); 56 | ri.change = mmhello.data.ranking().rank_change(); 57 | data.rankings.push_back(ri); 58 | } 59 | // commendations 60 | if (mmhello.data.has_commendation()) { 61 | data.cmd_friendly = mmhello.data.commendation().cmd_friendly(); 62 | data.cmd_teaching = mmhello.data.commendation().cmd_teaching(); 63 | data.cmd_leader = mmhello.data.commendation().cmd_leader(); 64 | } 65 | } catch (CSGO_CLI_TimeoutException) { 66 | printError("Warning", "Timeout on receiving UserInfo."); 67 | result = false; 68 | } catch (ExceptionHandler& e) { 69 | printError("Fatal error", e.what()); 70 | result = false; 71 | } 72 | if (verbose) 73 | spdlog::info("[ End ] [ Thread ] getUserInfo"); 74 | return 0; 75 | }); 76 | 77 | hellothread.join(); 78 | 79 | return result; 80 | } 81 | 82 | bool requestPlayersRankInfo(DataObject& data, bool& verbose) 83 | { 84 | if (verbose) 85 | spdlog::info("[ Start ] [ Thread ] requestPlayersRankInfo"); 86 | 87 | bool result = false; 88 | 89 | auto rankUpdateThread = std::thread([&data, verbose, &result]() { 90 | try { 91 | std::this_thread::sleep_for(std::chrono::milliseconds(1000)); 92 | 93 | CSGORankUpdate rankUpdate; 94 | 95 | if (verbose) 96 | spdlog::info(" Requesting: rankUpdate for Wingman"); 97 | rankUpdate.RefreshWaitWingmanRank(); 98 | if (verbose) 99 | spdlog::info(" Got rankUpdate for Wingman"); 100 | 101 | std::this_thread::sleep_for(std::chrono::milliseconds(500)); 102 | 103 | if (verbose) 104 | spdlog::info(" Requesting: rankUpdate for DangerZone"); 105 | rankUpdate.RefreshWaitDangerZoneRank(); 106 | if (verbose) 107 | spdlog::info(" Got rankUpdate for DangerZone"); 108 | 109 | result = true; 110 | 111 | if (verbose) { 112 | spdlog::debug("rankUpdate.data[0] (Wingman) {}", rankUpdate.data[0].DebugString()); 113 | spdlog::debug("rankUpdate.data[1] (DangerZone) {}", rankUpdate.data[1].DebugString()); 114 | } 115 | 116 | DataObject::RankingInfo ri; 117 | 118 | // wingman 119 | PlayerRankingInfo wm_pri = rankUpdate.data[0].rankings().Get(0); 120 | ri.id = wm_pri.rank_id(); 121 | ri.type = wm_pri.rank_type_id(); 122 | ri.wins = wm_pri.wins(); 123 | ri.change = wm_pri.rank_change(); 124 | data.rankings.push_back(ri); 125 | 126 | ri = {}; // reset 127 | 128 | // dangerzone 129 | PlayerRankingInfo dz_pri = rankUpdate.data[1].rankings().Get(0); 130 | ri.id = dz_pri.rank_id(); 131 | ri.type = dz_pri.rank_type_id(); 132 | ri.wins = dz_pri.wins(); 133 | ri.change = dz_pri.rank_change(); 134 | data.rankings.push_back(ri); 135 | 136 | } catch (CSGO_CLI_TimeoutException) { 137 | printError("Warning", "Timeout on receiving RankUpdate."); 138 | result = false; 139 | } catch (ExceptionHandler& e) { 140 | printError("Fatal error", e.what()); 141 | result = false; 142 | } 143 | if (verbose) 144 | spdlog::info("[ End ] [ Thread ] rankUpdate"); 145 | return 0; 146 | }); 147 | 148 | rankUpdateThread.join(); 149 | 150 | return result; 151 | } 152 | 153 | void printPlayersProfile(DataObject& data) 154 | { 155 | // ---------- Format Output Strings 156 | 157 | std::string level = fmt::format( 158 | "{0} ({1}/40) (XP: {2}/5000 | {3:.2f}%)", 159 | data.getPlayerLevel(), 160 | data.player_level, 161 | data.getPlayerXp(), 162 | data.getPlayerXpPercentage()); 163 | 164 | std::string likes = 165 | fmt::format("{} x friendly, {} x teaching, {} x leader", data.cmd_friendly, data.cmd_teaching, data.cmd_leader); 166 | 167 | std::string penalty = fmt::format("{} ({} Minutes)", data.penalty_reason, (data.penalty_seconds / 60)); 168 | 169 | std::string clan = fmt::format("{} \"{}\"", data.clan_name, data.clan_tag); 170 | 171 | auto mm_ranks = data.rankings[0]; 172 | auto mm_rank_name = data.getRankName(mm_ranks.id); 173 | 174 | std::string matchmaking_rank = fmt::format("{} ({}/18) ({} wins)", mm_rank_name, mm_ranks.id, mm_ranks.wins); 175 | 176 | auto wm_ranks = data.rankings[1]; 177 | auto wm_rank_name = data.getRankName(wm_ranks.id); 178 | 179 | std::string wingman_rank = fmt::format("{} ({}/18) ({} wins)", wm_rank_name, wm_ranks.id, wm_ranks.wins); 180 | 181 | auto dz_ranks = data.rankings[2]; 182 | auto dz_rank_name = data.getRankName(dz_ranks.id); 183 | 184 | std::string dangerzone_rank = fmt::format("{} ({}/18) ({} wins)", dz_rank_name, dz_ranks.id, dz_ranks.wins); 185 | 186 | // @todo(jakoch): how to access medals data? 187 | // auto medals = fmt::format("{} x arms, {} x combat, {} x global, {} x team, {} x weapon", 188 | // data.medals_arms, data.medals_combat, data.medals_global, data.medals_team, data.medals_weapon); 189 | 190 | // ---------- Output Table 191 | 192 | auto const printAligned{[=](std::string const & a, std::string const & b = "") { 193 | return fmt::print(" {0:<18} {1}\n", a, b); 194 | }}; 195 | 196 | fmt::print("\n Hello {}!\n", data.playername); 197 | fmt::print("\n Here is your user profile:\n\n"); 198 | 199 | printAligned("[Steam]"); 200 | printAligned(" "); 201 | printAligned("Name:", data.playername); 202 | printAligned("Clan:", clan); 203 | printAligned("ID:", toSteamIDClassic(data.steam_id)); 204 | printAligned("ID32:", toSteamID32(data.steam_id)); 205 | printAligned("ID64:", std::to_string(data.steam_id)); 206 | printAligned("Player Level:", std::to_string(data.steam_player_level)); 207 | printAligned("VAC Status:", data.getVacStatus()); 208 | printAligned("Profile URL:", data.getSteamProfileUrl()); 209 | printAligned(" "); 210 | printAligned("[CS:GO]"); 211 | printAligned(" "); 212 | printAligned("MatchMaking Rank:", matchmaking_rank); 213 | printAligned("Wingman Rank:", wingman_rank); 214 | printAligned("DangerZone Rank:", dangerzone_rank); 215 | printAligned("Player Level:", level); 216 | printAligned("Likes:", likes); 217 | printAligned("Penalty:", penalty); 218 | printAligned("Overwatch:", data.getCanDoOverwatch()); 219 | // printAligned("Medals:", medals); 220 | } -------------------------------------------------------------------------------- /src/commands/cmd.user.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_COMMANDS_CMD_USER_H_ 5 | #define SRC_COMMANDS_CMD_USER_H_ 6 | 7 | #include "../csgo/CSGOMMHello.h" 8 | #include "../csgo/CSGORankUpdate.h" 9 | #include "../DataObject.h" 10 | #include "../ErrorHandler.h" 11 | #include "../ExceptionHandler.h" 12 | #include "../SteamId.h" 13 | #include "../VersionAndConstants.h" 14 | 15 | #include 16 | 17 | bool requestPlayersProfile(DataObject& data, bool& verbose); 18 | bool requestPlayersRankInfo(DataObject& data, bool& verbose); 19 | void printPlayersProfile(DataObject& data); 20 | 21 | #endif // SRC_COMMANDS_CMD_USER_H_ -------------------------------------------------------------------------------- /src/csgo/CSGOClient.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "CSGOClient.h" 5 | 6 | static uint32_t const ProtobufFlag = (1 << 31); 7 | CSGOClient* CSGOClient::m_instance = nullptr; 8 | 9 | CSGOClient::CSGOClient() : 10 | m_welcomeHandler(this, &CSGOClient::OnClientWelcome), 11 | m_availableCb(this, &CSGOClient::OnMessageAvailable), 12 | m_failedCb(this, &CSGOClient::OnMessageFailed) 13 | { 14 | m_gameCoordinator = reinterpret_cast(SteamClient()->GetISteamGenericInterface( 15 | SteamAPI_GetHSteamUser(), SteamAPI_GetHSteamPipe(), STEAMGAMECOORDINATOR_INTERFACE_VERSION)); 16 | 17 | RegisterHandler(k_EMsgGCClientWelcome, &m_welcomeHandler); 18 | 19 | CMsgClientHello hello; 20 | hello.set_client_session_need(1); 21 | 22 | if (SendGCMessage(k_EMsgGCClientHello, &hello) != k_EGCResultOK) { 23 | throw ExceptionHandler("failed to send GCClientHello"); 24 | } 25 | } 26 | 27 | EGCResults CSGOClient::SendGCMessage(uint32_t uMsgType, google::protobuf::Message* msg) 28 | { 29 | std::lock_guard lock(m_sendMutex); 30 | 31 | #if GOOGLE_PROTOBUF_VERSION > 3009002 32 | int body_size = google::protobuf::internal::ToIntSize(msg->ByteSizeLong()); 33 | #else 34 | int body_size = msg->ByteSize(); 35 | #endif 36 | 37 | auto size = body_size + 2 * sizeof(uint32_t); 38 | 39 | if (m_msgBuffer.size() < size) { 40 | m_msgBuffer.resize(size); 41 | } 42 | 43 | uMsgType |= ProtobufFlag; 44 | 45 | reinterpret_cast(m_msgBuffer.data())[0] = uMsgType; 46 | reinterpret_cast(m_msgBuffer.data())[1] = 0; 47 | 48 | msg->SerializeToArray(m_msgBuffer.data() + 2 * sizeof(uint32_t), m_msgBuffer.size() - 2 * sizeof(uint32_t)); 49 | 50 | return m_gameCoordinator->SendMessage(uMsgType, m_msgBuffer.data(), size); 51 | } 52 | 53 | void CSGOClient::OnMessageAvailable(GCMessageAvailable_t* msg) 54 | { 55 | std::lock_guard lock(m_recvMutex); 56 | std::lock_guard lock2(m_handlerMutex); 57 | 58 | if (m_recvBuffer.size() < msg->m_nMessageSize) { 59 | m_recvBuffer.resize(msg->m_nMessageSize); 60 | } 61 | 62 | uint32_t msgType; 63 | uint32_t msgSize; 64 | 65 | auto res = m_gameCoordinator->RetrieveMessage(&msgType, m_recvBuffer.data(), m_recvBuffer.size(), &msgSize); 66 | 67 | if (res == k_EGCResultOK) { 68 | if (msgType & ProtobufFlag) { 69 | auto handler = m_msgHandler.equal_range(msgType & (~ProtobufFlag)); 70 | 71 | for (auto it = handler.first; it != handler.second; ++it) { 72 | it->second->Handle(m_recvBuffer.data() + 2 * sizeof(uint32_t), msgSize - 2 * sizeof(uint32_t)); 73 | } 74 | } 75 | } 76 | } 77 | 78 | void CSGOClient::OnMessageFailed(GCMessageFailed_t* msg) 79 | { 80 | throw ExceptionHandler("Failed to deliver GC message"); 81 | } 82 | 83 | void CSGOClient::RegisterHandler(uint32 msgId, IGCMsgHandler* handler) 84 | { 85 | std::lock_guard lock(m_handlerMutex); 86 | m_msgHandler.insert({msgId, handler}); 87 | } 88 | 89 | void CSGOClient::RemoveHandler(uint32 msgId, IGCMsgHandler* handler) 90 | { 91 | std::lock_guard lock(m_handlerMutex); 92 | 93 | auto h = m_msgHandler.equal_range(msgId); 94 | 95 | for (auto it = h.first; it != h.second; ++it) { 96 | if (it->second == handler) { 97 | it = m_msgHandler.erase(it); 98 | return; 99 | } 100 | } 101 | } 102 | 103 | CSGOClient* CSGOClient::GetInstance() 104 | { 105 | if (!m_instance) { 106 | m_instance = new CSGOClient(); 107 | } 108 | 109 | return m_instance; 110 | } 111 | 112 | void CSGOClient::Destroy() 113 | { 114 | delete m_instance; 115 | m_instance = nullptr; 116 | } 117 | 118 | void CSGOClient::WaitForGameClientConnect() 119 | { 120 | if (m_connectedToGameClient) { 121 | return; 122 | } 123 | 124 | std::unique_lock lock(m_connectedMutex); 125 | // if this takes longer than 10 seconds we are already connected to the gc 126 | m_connectedCV.wait_for(lock, std::chrono::seconds(10)); 127 | m_connectedToGameClient = true; 128 | } 129 | 130 | void CSGOClient::OnClientWelcome(CMsgClientWelcome const & msg) 131 | { 132 | // printf("Received welcome CS:GO Game Coordinator version %s (Connected to %s).", 133 | // std::to_string(msg.version()).c_str(), msg.location().country().c_str()); 134 | 135 | m_connectedToGameClient = true; 136 | m_connectedCV.notify_all(); 137 | } 138 | -------------------------------------------------------------------------------- /src/csgo/CSGOClient.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_CSGO_CSGOCLIENT_H_ 5 | #define SRC_CSGO_CSGOCLIENT_H_ 6 | 7 | // Steamworks SDK 8 | #pragma warning(disable : 4996) 9 | #include 10 | #include 11 | 12 | // CSGO Protobuf's 13 | #include "cstrike15_gcmessages.pb.h" 14 | #include "engine_gcmessages.pb.h" 15 | #include "gcsdk_gcmessages.pb.h" 16 | #include "gcsystemmsgs.pb.h" 17 | 18 | #include "../ExceptionHandler.h" 19 | #include "../VersionAndConstants.h" 20 | #include "GCMsgHandler.h" 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | /** 29 | * A minimal CS:GO Game Client 30 | */ 31 | class CSGOClient 32 | { 33 | public: 34 | /** 35 | * Retrieves the instance to the current csgo client or creates a new one 36 | */ 37 | static CSGOClient* GetInstance(); 38 | 39 | /** 40 | * Destroys the csgo client 41 | */ 42 | static void Destroy(); 43 | 44 | /** 45 | * Sends a gc protobuf message 46 | */ 47 | EGCResults SendGCMessage(uint32 uMsgType, google::protobuf::Message* msg); 48 | 49 | /** 50 | * Registers a gc protobuf msg handler 51 | */ 52 | void RegisterHandler(uint32 msgId, IGCMsgHandler* handler); 53 | 54 | /** 55 | * Removes a gc protobuf msg handler 56 | */ 57 | void RemoveHandler(uint32 msgId, IGCMsgHandler* handler); 58 | 59 | /** 60 | * Blocks until we are connected to the GameClient 61 | */ 62 | void WaitForGameClientConnect(); 63 | 64 | private: 65 | /** 66 | * Sends client mm hello 67 | */ 68 | CSGOClient(); 69 | CSGOClient(CSGOClient const &) = delete; 70 | 71 | /** 72 | * Steam callback for gc messages 73 | */ 74 | void OnMessageAvailable(GCMessageAvailable_t* msg); 75 | 76 | /** 77 | * Steam callback for failed gc messages 78 | */ 79 | void OnMessageFailed(GCMessageFailed_t* msg); 80 | 81 | /** 82 | * Handles the gc welcome msg 83 | */ 84 | void OnClientWelcome(CMsgClientWelcome const & msg); 85 | 86 | private: 87 | static CSGOClient* m_instance; 88 | 89 | GCMsgHandler m_welcomeHandler; 90 | std::condition_variable m_connectedCV; 91 | std::mutex m_connectedMutex; 92 | bool m_connectedToGameClient = false; 93 | 94 | ISteamGameCoordinator* m_gameCoordinator; 95 | 96 | CCallback m_availableCb; 97 | CCallback m_failedCb; 98 | std::vector m_recvBuffer; 99 | std::vector m_msgBuffer; 100 | std::mutex m_sendMutex; 101 | std::mutex m_recvMutex; 102 | std::mutex m_handlerMutex; 103 | std::multimap m_msgHandler; 104 | }; 105 | 106 | #endif // SRC_CSGO_CSGOCLIENT_H_ 107 | -------------------------------------------------------------------------------- /src/csgo/CSGOMMHello.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "CSGOMMHello.h" 5 | #include "../ExceptionHandler.h" 6 | #include "../VersionAndConstants.h" 7 | 8 | #include 9 | 10 | CSGOMMHello::CSGOMMHello() : m_mmhelloHandler(this, &CSGOMMHello::OnMMHello) 11 | { 12 | CSGOClient::GetInstance()->RegisterHandler(k_EMsgGCCStrike15_v2_MatchmakingGC2ClientHello, &m_mmhelloHandler); 13 | } 14 | 15 | CSGOMMHello::~CSGOMMHello() 16 | { 17 | CSGOClient::GetInstance()->RemoveHandler(k_EMsgGCCStrike15_v2_MatchmakingGC2ClientHello, &m_mmhelloHandler); 18 | } 19 | 20 | void CSGOMMHello::OnMMHello(CMsgGCCStrike15_v2_MatchmakingGC2ClientHello const & msg) 21 | { 22 | std::unique_lock lock(m_mmhelloMutex); 23 | data = msg; 24 | m_updateComplete = true; 25 | lock.unlock(); 26 | m_updateCv.notify_all(); 27 | } 28 | 29 | void CSGOMMHello::Refresh() 30 | { 31 | CMsgGCCStrike15_v2_MatchmakingClient2GCHello request; 32 | if (CSGOClient::GetInstance()->SendGCMessage(k_EMsgGCCStrike15_v2_MatchmakingClient2GCHello, &request) != 33 | k_EGCResultOK) { 34 | throw ExceptionHandler("Failed to send EMsgGCCStrike15_v2_MatchmakingClient2GCHello"); 35 | } 36 | } 37 | 38 | void CSGOMMHello::RefreshWait() 39 | { 40 | m_updateComplete = false; 41 | Refresh(); 42 | std::unique_lock lock(m_mmhelloMutex); 43 | 44 | m_updateCv.wait_for(lock, std::chrono::milliseconds(CSGO_CLI_STEAM_CMSG_TIMEOUT)); 45 | 46 | if (!m_updateComplete) { 47 | throw CSGO_CLI_TimeoutException(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/csgo/CSGOMMHello.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_CSGO_CSGOMMHELLO_H_ 5 | #define SRC_CSGO_CSGOMMHELLO_H_ 6 | 7 | #include "CSGOClient.h" 8 | 9 | #include 10 | 11 | class CSGOMMHello 12 | { 13 | public: 14 | CSGOMMHello(); 15 | ~CSGOMMHello(); 16 | 17 | void Refresh(); 18 | void RefreshWait(); 19 | 20 | CMsgGCCStrike15_v2_MatchmakingGC2ClientHello data; 21 | 22 | private: 23 | void OnMMHello(CMsgGCCStrike15_v2_MatchmakingGC2ClientHello const & msg); 24 | 25 | private: 26 | bool m_updateComplete = false; 27 | std::condition_variable m_updateCv; 28 | mutable std::mutex m_mmhelloMutex; 29 | GCMsgHandler m_mmhelloHandler; 30 | }; 31 | 32 | #endif // SRC_CSGO_CSGOMMHELLO_H_ 33 | -------------------------------------------------------------------------------- /src/csgo/CSGOMatchData.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_CSGO_CSGOMATCHDATA_H_ 5 | #define SRC_CSGO_CSGOMATCHDATA_H_ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "CSGOMatchPlayerScore.h" 12 | 13 | #include 14 | #include 15 | 16 | struct CSGOMatchData 17 | { 18 | private: 19 | // TODO(jakoch): find out, if this is correct? 20 | // the idea is that "lastRound.reservation.game_type" is the "map_name" 21 | enum GameTypes 22 | { 23 | de_dust2 = 520, 24 | de_train = 1032, 25 | de_inferno = 4104, 26 | de_nuke = 8200, 27 | de_vertigo = 16392, 28 | cs_office = 65544, 29 | de_mirage = 32776, 30 | de_cache = 1048584, 31 | de_workout = 67108872, 32 | de_zoo = 33554440, 33 | cs_agency = 134217736, 34 | de_overpass = 268435464 35 | }; 36 | 37 | std::string getGameTypeStr(/*GameTypes game_types*/ int32 game_type) const 38 | { 39 | switch (game_type) { 40 | case GameTypes::de_train: 41 | return "de_train"; 42 | case GameTypes::de_dust2: 43 | return "de_dust2"; 44 | case GameTypes::de_inferno: 45 | return "de_inferno"; 46 | case GameTypes::de_nuke: 47 | return "de_nuke"; 48 | case GameTypes::de_vertigo: 49 | return "de_vertigo"; 50 | case GameTypes::cs_office: 51 | return "cs_office"; 52 | case GameTypes::de_mirage: 53 | return "de_mirage"; 54 | case GameTypes::de_cache: 55 | return "de_cache"; 56 | case GameTypes::de_zoo: 57 | return "de_zoo"; 58 | case GameTypes::cs_agency: 59 | return "cs_agency"; 60 | case GameTypes::de_overpass: 61 | return "de_overpass"; 62 | case GameTypes::de_workout: 63 | return "de_workout"; 64 | // omit default case to trigger compiler warning for missing cases 65 | } 66 | return std::to_string(static_cast(game_type)); 67 | }; 68 | 69 | public: 70 | uint64 matchid; 71 | 72 | time_t matchtime; 73 | std::string matchtime_str; 74 | 75 | time_t match_duration; 76 | std::string match_duration_str; 77 | 78 | uint32 server_ip; 79 | uint32 tv_port; 80 | uint64 reservation_id; 81 | 82 | std::string sharecode; 83 | std::string replaylink; /* roundstats.map */ 84 | 85 | std::string map; /* watchablematchinfo.game_map */ 86 | std::string mapgroup; /* watchablematchinfo.game_mapgroup */ 87 | uint32 game_type; /* roundstatsall.reservation.game_type | watchablematchinfo.game_type */ 88 | 89 | uint32 spectators; 90 | 91 | std::vector scoreboard; 92 | 93 | int result; 94 | std::string result_str; 95 | 96 | int score_ally; 97 | int score_enemy; 98 | 99 | std::string getScore() const 100 | { 101 | return fmt::format("{:02} : {:02}", score_ally, score_enemy); 102 | } 103 | 104 | // @todo(jakoch): how to get mapname? 105 | std::string getMapname() const 106 | { 107 | return (map.empty() ? "? " : map); 108 | } 109 | 110 | std::string getMatchResult() const 111 | { 112 | if (result_str == "LOSS") { 113 | return fmt::format(fmt::fg(fmt::color::red), "LOSS"); 114 | } else if (result_str == "WIN") { 115 | return fmt::format(fmt::fg(fmt::color::green), "WIN"); 116 | } else { // result_str == "TIE" 117 | return fmt::format(fmt::fg(fmt::color::yellow), "TIE"); 118 | } 119 | } 120 | 121 | std::string getGameType() const 122 | { 123 | return getGameTypeStr(game_type); 124 | } 125 | }; 126 | 127 | #endif // SRC_CSGO_CSGOMATCHDATA_H_ 128 | -------------------------------------------------------------------------------- /src/csgo/CSGOMatchList.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "CSGOMatchList.h" 5 | #include "../ExceptionHandler.h" 6 | #include "../VersionAndConstants.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | /* 13 | * MatchList 14 | * MatchListRequestRecentUserGames 15 | */ 16 | 17 | CSGOMatchList::CSGOMatchList() : m_matchListHandler(this, &CSGOMatchList::OnMatchList) 18 | { 19 | CSGOClient::GetInstance()->RegisterHandler(k_EMsgGCCStrike15_v2_MatchList, &m_matchListHandler); 20 | } 21 | 22 | CSGOMatchList::~CSGOMatchList() 23 | { 24 | CMsgGCCStrike15_v2_MatchList matchlist; 25 | 26 | for (auto& m : m_matches) { 27 | matchlist.add_matches()->CopyFrom(m); 28 | } 29 | 30 | CSGOClient::GetInstance()->RemoveHandler(k_EMsgGCCStrike15_v2_MatchList, &m_matchListHandler); 31 | } 32 | 33 | void CSGOMatchList::OnMatchList(CMsgGCCStrike15_v2_MatchList const & msg) 34 | { 35 | std::unique_lock lock(m_matchMutex); 36 | 37 | for (auto it = msg.matches().rbegin(); it != msg.matches().rend(); ++it) { 38 | m_matches.push_back(*it); 39 | } 40 | 41 | m_updateComplete = true; 42 | lock.unlock(); 43 | m_updateCv.notify_all(); 44 | } 45 | 46 | void CSGOMatchList::Refresh() 47 | { 48 | uint32 const accountId = SteamUser()->GetSteamID().GetAccountID(); 49 | 50 | CMsgGCCStrike15_v2_MatchListRequestRecentUserGames request; 51 | request.set_accountid(accountId); 52 | 53 | if (CSGOClient::GetInstance()->SendGCMessage(k_EMsgGCCStrike15_v2_MatchListRequestRecentUserGames, &request) != 54 | k_EGCResultOK) { 55 | throw ExceptionHandler("Failed to send EMsgGCCStrike15_v2_MatchListRequestRecentUserGames"); 56 | } 57 | } 58 | 59 | void CSGOMatchList::RefreshWait() 60 | { 61 | m_updateComplete = false; 62 | Refresh(); 63 | std::unique_lock lock(m_matchMutex); 64 | 65 | m_updateCv.wait_for(lock, std::chrono::milliseconds(CSGO_CLI_STEAM_CMSG_TIMEOUT)); 66 | 67 | if (!m_updateComplete) { 68 | throw CSGO_CLI_TimeoutException(); 69 | } 70 | } 71 | 72 | std::vector const & CSGOMatchList::Matches() const 73 | { 74 | std::lock_guard lock(m_matchMutex); 75 | return m_matches; 76 | } 77 | 78 | int CSGOMatchList::getOwnIndex(CMsgGCCStrike15_v2_MatchmakingServerRoundStats const & roundStats) const 79 | { 80 | uint32 const accountId = SteamUser()->GetSteamID().GetAccountID(); 81 | 82 | for (int i = 0; i < roundStats.reservation().account_ids().size(); ++i) { 83 | if (roundStats.reservation().account_ids(i) == accountId) { 84 | return i; 85 | } 86 | } 87 | 88 | throw ExceptionHandler("Unable to find own AccountID in matchinfo."); 89 | } 90 | 91 | int CSGOMatchList::getPlayerIndex( 92 | uint32 accountId, CMsgGCCStrike15_v2_MatchmakingServerRoundStats const & roundStats) const 93 | { 94 | for (int i = 0; i < roundStats.reservation().account_ids().size(); ++i) { 95 | if (roundStats.reservation().account_ids(i) == accountId) { 96 | return i; 97 | } 98 | } 99 | 100 | throw ExceptionHandler("Unable to find specified AccountID in matchinfo."); 101 | } 102 | 103 | std::string CSGOMatchList::getMatchResult(CMsgGCCStrike15_v2_MatchmakingServerRoundStats const & roundStats) const 104 | { 105 | int const num = getMatchResultNum(roundStats); 106 | if (num == 0) 107 | return "TIE"; 108 | if (num == 1) 109 | return "WIN"; 110 | return "LOSS"; 111 | } 112 | 113 | int CSGOMatchList::getMatchResultNum(CMsgGCCStrike15_v2_MatchmakingServerRoundStats const & roundStats) const 114 | { 115 | auto const ownIndex = getOwnIndex(roundStats); 116 | if (roundStats.match_result() == 0) { 117 | return 0; // tie 118 | } 119 | if (roundStats.match_result() == 1 && ownIndex <= 4) { 120 | return 1; // win 121 | } 122 | if (roundStats.match_result() == 2 && ownIndex >= 5) { 123 | return 1; // win 124 | } 125 | return 2; // loss 126 | } 127 | -------------------------------------------------------------------------------- /src/csgo/CSGOMatchList.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_CSGO_CSGOMATCHLIST_H_ 5 | #define SRC_CSGO_CSGOMATCHLIST_H_ 6 | 7 | #include "CSGOClient.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | /** 14 | * utility class to store and update the match list 15 | */ 16 | class CSGOMatchList 17 | { 18 | public: 19 | CSGOMatchList(); 20 | ~CSGOMatchList(); 21 | 22 | void Refresh(); 23 | void RefreshWait(); 24 | 25 | std::vector const & Matches() const; 26 | 27 | int getOwnIndex(CMsgGCCStrike15_v2_MatchmakingServerRoundStats const & roundStats) const; 28 | int getPlayerIndex(uint32 accountId, CMsgGCCStrike15_v2_MatchmakingServerRoundStats const & roundStats) const; 29 | std::string getMatchResult(CMsgGCCStrike15_v2_MatchmakingServerRoundStats const & roundStats) const; 30 | int getMatchResultNum(CMsgGCCStrike15_v2_MatchmakingServerRoundStats const & roundStats) const; 31 | 32 | CMsgGCCStrike15_v2_MatchList matchList; 33 | 34 | private: 35 | void OnMatchList(CMsgGCCStrike15_v2_MatchList const & msg); 36 | 37 | private: 38 | bool m_updateComplete = false; 39 | std::condition_variable m_updateCv; 40 | mutable std::mutex m_matchMutex; 41 | std::vector m_matches; 42 | GCMsgHandler m_matchListHandler; 43 | }; 44 | 45 | #endif // SRC_CSGO_CSGOMATCHLIST_H_ 46 | -------------------------------------------------------------------------------- /src/csgo/CSGOMatchPlayerScore.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "CSGOMatchPlayerScore.h" 5 | 6 | std::string CSGOMatchPlayerScore::getKillDeathRatio() const 7 | { 8 | return fmt::format("{}", kills / deaths); 9 | } 10 | 11 | /*std::string CSGOMatchPlayerScore::getHSRatio() const 12 | { 13 | return fmt::format("{} ({}%)", headshot, headshot_percentage = (headshots / kills) * 100)); 14 | }*/ 15 | 16 | std::string CSGOMatchPlayerScore::getKills() const 17 | { 18 | return std::to_string(kills); 19 | } 20 | std::string CSGOMatchPlayerScore::getAssists() const 21 | { 22 | return std::to_string(assists); 23 | } 24 | std::string CSGOMatchPlayerScore::getDeaths() const 25 | { 26 | return std::to_string(deaths); 27 | } 28 | std::string CSGOMatchPlayerScore::getMVPs() const 29 | { 30 | return std::to_string(mvps); 31 | } 32 | std::string CSGOMatchPlayerScore::getScore() const 33 | { 34 | return std::to_string(score); 35 | } 36 | -------------------------------------------------------------------------------- /src/csgo/CSGOMatchPlayerScore.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_CSGO_CSGOMATCHPLAYERSCORE_H_ 5 | #define SRC_CSGO_CSGOMATCHPLAYERSCORE_H_ 6 | 7 | #include "CSGOClient.h" 8 | #include 9 | #include 10 | 11 | /** 12 | * This struct represents the PlayerScoreboard. 13 | * 14 | * Usage: std::vector scoreboard; 15 | */ 16 | struct CSGOMatchPlayerScore 17 | { 18 | public: 19 | int index = 0; 20 | uint32 account_id = 0; 21 | uint64 steam_id = 0; 22 | int32 kills = 0; 23 | int32 assists = 0; 24 | int32 deaths = 0; 25 | int32 kdr = 0; 26 | int32 mvps = 0; 27 | int32 score = 0; 28 | 29 | std::string getKills() const; 30 | std::string getAssists() const; 31 | std::string getDeaths() const; 32 | std::string getKillDeathRatio() const; 33 | // std::string getHSRatio() const; 34 | std::string getMVPs() const; 35 | std::string getScore() const; 36 | }; 37 | 38 | #endif // SRC_CSGO_CSGOMATCHPLAYERSCORE_H_ 39 | -------------------------------------------------------------------------------- /src/csgo/CSGORankUpdate.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "CSGORankUpdate.h" 5 | #include "../ExceptionHandler.h" 6 | #include "../VersionAndConstants.h" 7 | 8 | #include 9 | 10 | CSGORankUpdate::CSGORankUpdate() : m_rankUpdateHandler(this, &CSGORankUpdate::OnRankUpdate) 11 | { 12 | CSGOClient::GetInstance()->RegisterHandler(k_EMsgGCCStrike15_v2_ClientGCRankUpdate, &m_rankUpdateHandler); 13 | } 14 | 15 | CSGORankUpdate::~CSGORankUpdate() 16 | { 17 | CSGOClient::GetInstance()->RemoveHandler(k_EMsgGCCStrike15_v2_ClientGCRankUpdate, &m_rankUpdateHandler); 18 | } 19 | 20 | void CSGORankUpdate::OnRankUpdate(CMsgGCCStrike15_v2_ClientGCRankUpdate const & msg) 21 | { 22 | std::unique_lock lock(m_rankUpdateMutex); 23 | data.push_back(msg); 24 | m_updateComplete = true; 25 | lock.unlock(); 26 | m_updateCv.notify_all(); 27 | } 28 | 29 | void CSGORankUpdate::GetWingmanRank() 30 | { 31 | CMsgGCCStrike15_v2_ClientGCRankUpdate request; 32 | request.add_rankings()->set_rank_type_id(7); 33 | 34 | if (CSGOClient::GetInstance()->SendGCMessage(k_EMsgGCCStrike15_v2_ClientGCRankUpdate, &request) != k_EGCResultOK) { 35 | throw ExceptionHandler("Failed to send EMsgGCCStrike15_v2_ClientGCRankUpdate"); 36 | } 37 | } 38 | 39 | void CSGORankUpdate::GetDangerZoneRank() 40 | { 41 | CMsgGCCStrike15_v2_ClientGCRankUpdate request; 42 | request.add_rankings()->set_rank_type_id(10); 43 | 44 | if (CSGOClient::GetInstance()->SendGCMessage(k_EMsgGCCStrike15_v2_ClientGCRankUpdate, &request) != k_EGCResultOK) { 45 | throw ExceptionHandler("Failed to send EMsgGCCStrike15_v2_ClientGCRankUpdate"); 46 | } 47 | } 48 | 49 | void CSGORankUpdate::RefreshWaitWingmanRank() 50 | { 51 | m_updateComplete = false; 52 | GetWingmanRank(); 53 | std::unique_lock lock(m_rankUpdateMutex); 54 | 55 | m_updateCv.wait_for(lock, std::chrono::milliseconds(CSGO_CLI_STEAM_CMSG_TIMEOUT + 10000)); 56 | 57 | if (!m_updateComplete) { 58 | throw CSGO_CLI_TimeoutException(); 59 | } 60 | } 61 | 62 | void CSGORankUpdate::RefreshWaitDangerZoneRank() 63 | { 64 | m_updateComplete = false; 65 | GetDangerZoneRank(); 66 | std::unique_lock lock(m_rankUpdateMutex); 67 | 68 | m_updateCv.wait_for(lock, std::chrono::milliseconds(CSGO_CLI_STEAM_CMSG_TIMEOUT + 10000)); 69 | 70 | if (!m_updateComplete) { 71 | throw CSGO_CLI_TimeoutException(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/csgo/CSGORankUpdate.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_CSGO_CSGORANKUPDATE_H_ 5 | #define SRC_CSGO_CSGORANKUPDATE_H_ 6 | 7 | #include "CSGOClient.h" 8 | 9 | #include 10 | #include 11 | 12 | class CSGORankUpdate 13 | { 14 | public: 15 | CSGORankUpdate(); 16 | ~CSGORankUpdate(); 17 | 18 | void GetWingmanRank(); 19 | void GetDangerZoneRank(); 20 | 21 | void RefreshWaitWingmanRank(); 22 | void RefreshWaitDangerZoneRank(); 23 | 24 | std::vector data; 25 | 26 | private: 27 | void OnRankUpdate(CMsgGCCStrike15_v2_ClientGCRankUpdate const & msg); 28 | 29 | private: 30 | bool m_updateComplete = false; 31 | std::condition_variable m_updateCv; 32 | mutable std::mutex m_rankUpdateMutex; 33 | GCMsgHandler m_rankUpdateHandler; 34 | }; 35 | 36 | #endif // SRC_CSGO_CSGORANKUPDATE_H_ -------------------------------------------------------------------------------- /src/csgo/GCMsgHandler.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_CSGO_GCMSGHANDLER_H_ 5 | #define SRC_CSGO_GCMSGHANDLER_H_ 6 | 7 | #include 8 | 9 | struct IGCMsgHandler 10 | { 11 | virtual void Handle(void* buf, size_t len) const = 0; 12 | }; 13 | 14 | template 15 | /** 16 | * Handler for Protobuf Messages 17 | */ 18 | class GCMsgHandler : public IGCMsgHandler 19 | { 20 | public: 21 | using CallbackThread = std::function; 22 | 23 | template 24 | /** 25 | * Construct from class handler 26 | */ 27 | GCMsgHandler(C* instance, void (C::*handler)(M const &)) 28 | { 29 | m_handler = std::bind(std::mem_fn(handler), instance, std::placeholders::_1); 30 | } 31 | 32 | template 33 | /** 34 | * Construct from functor 35 | */ 36 | explicit GCMsgHandler(F const & handler) : m_handler(handler) 37 | { 38 | } 39 | 40 | /** 41 | * Try parsing msg from buffer and call handler 42 | */ 43 | /*virtual*/ void Handle(void* buf, size_t len) const final 44 | { 45 | M msg; 46 | msg.ParseFromArray(buf, len); 47 | m_handler(msg); 48 | } 49 | 50 | private: 51 | CallbackThread m_handler; 52 | }; 53 | 54 | #endif // SRC_CSGO_GCMSGHANDLER_H_ 55 | -------------------------------------------------------------------------------- /src/csgostats/ShareCodeCache.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "ShareCodeCache.h" 5 | 6 | ShareCodeCache::ShareCodeCache(bool verboseMode) 7 | { 8 | // create file, if not exists 9 | if (!std::filesystem::exists(csvFile)) { 10 | matchDbFile.open(csvFile, std::ios::out | std::ios::app); 11 | matchDbFile.close(); 12 | } 13 | 14 | matchDbFile.open(csvFile, std::ios::in); 15 | sharecodeCache = read(matchDbFile); 16 | matchDbFile.close(); 17 | 18 | if (verboseMode) { 19 | // debug print sharecode cache 20 | printf(" Cached Sharecodes: %zd \n", sharecodeCache.size()); 21 | for (auto const & sharecode : sharecodeCache) { 22 | printf(" \"%s\" \n", sharecode.c_str()); 23 | } 24 | printf("\n"); 25 | } 26 | 27 | // clear cache 28 | if (sharecodeCache.size() >= 50) { 29 | std::ofstream ofs(csvFile, std::ios::out | std::ios::trunc); 30 | ofs.close(); 31 | } 32 | } 33 | 34 | bool ShareCodeCache::find(std::string sharecode) 35 | { 36 | for (auto const & sharecodeFromCache : sharecodeCache) { 37 | if (sharecode.compare(sharecodeFromCache.c_str()) == 0) { 38 | return true; 39 | } 40 | } 41 | return false; 42 | } 43 | 44 | bool ShareCodeCache::insert(std::string sharecode) 45 | { 46 | matchDbFile.open(csvFile, std::ios::out | std::ios::app); 47 | if (matchDbFile.good()) { 48 | matchDbFile << sharecode << std::endl; 49 | matchDbFile.close(); 50 | return true; 51 | } 52 | return false; 53 | } 54 | 55 | std::vector ShareCodeCache::read(std::istream& is) 56 | { 57 | std::vector tokens; 58 | std::string token; 59 | while (std::getline(is, token) && !token.empty()) { 60 | tokens.push_back(token); 61 | } 62 | return tokens; 63 | } 64 | 65 | ShareCodeCache::~ShareCodeCache() 66 | { 67 | matchDbFile.close(); 68 | } -------------------------------------------------------------------------------- /src/csgostats/ShareCodeCache.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_CSGOSTATS_SHARECODECACHE_H_ 5 | #define SRC_CSGOSTATS_SHARECODECACHE_H_ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | class ShareCodeCache 15 | { 16 | public: 17 | explicit ShareCodeCache(bool verboseMode); 18 | ~ShareCodeCache(); 19 | 20 | bool find(std::string sharecode); 21 | bool insert(std::string sharecode); 22 | 23 | private: 24 | std::string const csvFile = "sharecode.db"; 25 | std::fstream matchDbFile; 26 | 27 | std::vector sharecodeCache; 28 | 29 | std::vector read(std::istream& input); 30 | }; 31 | 32 | #endif // SRC_CSGOSTATS_SHARECODECACHE_H_ -------------------------------------------------------------------------------- /src/csgostats/ShareCodeUpload.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "ShareCodeUpload.h" 5 | #include "../VersionAndConstants.h" 6 | 7 | #include 8 | #include 9 | 10 | ShareCodeUpload::ShareCodeUpload(bool verboseMode) 11 | { 12 | verbose = verboseMode; 13 | 14 | curl = curl_easy_init(); 15 | 16 | host = curl_slist_append(NULL, "csgostats.gg:80:104.16.222.55"); 17 | host = curl_slist_append(host, "csgostats.gg:443:104.16.222.55"); 18 | } 19 | 20 | ShareCodeUpload::~ShareCodeUpload() 21 | { 22 | curl_slist_free_all(host); 23 | 24 | curl_easy_cleanup(curl); 25 | } 26 | 27 | size_t CurlWrite_CallbackFunc_StdString(void* contents, size_t size, size_t nmemb, std::string* s) 28 | { 29 | size_t newLength = size * nmemb; 30 | size_t oldLength = s->size(); 31 | 32 | try { 33 | s->resize(oldLength + newLength); 34 | } catch (std::bad_alloc& e) { 35 | // cast to void (formerly self-assign) to avoid unused/unreferenced variable e 36 | static_cast(e); 37 | // handle memory problem 38 | return 0; 39 | } 40 | 41 | std::copy(reinterpret_cast(contents), reinterpret_cast(contents) + newLength, s->begin() + oldLength); 42 | 43 | return size * nmemb; 44 | } 45 | 46 | /* 47 | POST the CSGO Demo Sharecode to csgostats.gg 48 | */ 49 | int ShareCodeUpload::uploadShareCode(std::string shareCode, std::string& responseContent) 50 | { 51 | CURLcode res; 52 | 53 | char errorBuffer[CURL_ERROR_SIZE]; 54 | 55 | // set the error buffer as empty before performing a request 56 | errorBuffer[0] = 0; 57 | 58 | // provide a buffer for storing errors 59 | curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errorBuffer); 60 | 61 | curl_easy_setopt(curl, CURLOPT_RESOLVE, host); 62 | 63 | // 1. URL that is about to receive our POST data 64 | curl_easy_setopt(curl, CURLOPT_URL, "https://csgostats.gg/match/upload/ajax"); 65 | 66 | // 2. build data to POST 67 | std::string postData("sharecode="); 68 | postData.append(shareCode); // this is the raw sharecode, not an URL. escaping not needed. 69 | postData.append("&index=0"); 70 | 71 | // 3. set data to POST 72 | char const * data = postData.c_str(); 73 | // curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(data)); 74 | curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(strlen(data))); 75 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data); 76 | 77 | // prepare user-agent identifier 78 | char ua_ident[100]; 79 | printf(ua_ident, "User-Agent: Mozilla/5.0 (compatible; %s)", CSGO_CLI_USERAGENT_ID); 80 | 81 | // 4. set headers 82 | struct curl_slist* headers = NULL; 83 | headers = curl_slist_append(headers, "Accept: application/json, text/javascript, */*; q=0.01"); 84 | headers = curl_slist_append(headers, "Accept-Language: en-US;q=0.8,en;q=0.7"); 85 | headers = curl_slist_append(headers, ua_ident); 86 | headers = curl_slist_append(headers, "X-Requested-With: XMLHttpRequest"); 87 | headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded; charset=UTF-8"); 88 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); 89 | 90 | // 5. follow+timeout 91 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 92 | curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); 93 | 94 | // 6. SSL 95 | curl_easy_setopt(curl, CURLOPT_CAINFO, "curl-ca-bundle.crt"); 96 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); // only for https 97 | 98 | // 7. setup method to handle the response data 99 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWrite_CallbackFunc_StdString); 100 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseContent); 101 | 102 | // 8. enable verbose mode 103 | if (verbose) { 104 | curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); 105 | } 106 | 107 | // perform the request 108 | res = curl_easy_perform(curl); 109 | 110 | // show error infos 111 | if (res != CURLE_OK) { 112 | // if the request did not complete correctly, show the error information 113 | size_t len = strlen(errorBuffer); 114 | fprintf(stderr, "\nlibcurl: (%d) ", res); 115 | if (len) { 116 | fprintf(stderr, "%s%s", errorBuffer, ((errorBuffer[len - 1] != '\n') ? "\n" : "")); 117 | } else { 118 | // if no detailed error information was written to errorBuffer, 119 | // show the more generic information from curl_easy_strerror instead 120 | fprintf(stderr, "%s\n", curl_easy_strerror(res)); 121 | } 122 | 123 | // cleanup 124 | curl_easy_cleanup(curl); 125 | // free the custom headers 126 | curl_slist_free_all(headers); 127 | 128 | // when CURL is NOT OK, return false 129 | return 1; 130 | } 131 | 132 | // output response 133 | if (verbose) { 134 | std::cout << "\n\n[LOG] [UploadShareCode] [POST Request] ResponseContent:\n"; 135 | std::cout << "---\n" << responseContent << "\n---\n"; 136 | } 137 | 138 | curl_easy_reset(curl); 139 | 140 | // when CURL is OK, return 0 141 | return 0; 142 | } 143 | 144 | /** 145 | * return codes: 146 | * 0. should never happen :) "and you may ask yourself: how did i get here?" 147 | * 1. Error: response empty or response content HTML, instead of JSON 148 | * 2. Error: JSON parsing failed 149 | * 3. Server-Side: Error 150 | * 4. Server-Side: Match queued / processing demo file 151 | * 5. Server-Side: complete 152 | */ 153 | int ShareCodeUpload::processJsonResponse(std::string& jsonResponse) 154 | { 155 | // response empty? 156 | if (jsonResponse.empty()) { 157 | printError("Error", "The response content is empty.\n"); 158 | return 1; 159 | } 160 | 161 | // make sure response is JSON and not HTML 162 | if (jsonResponse.rfind("", 0) == 0 || jsonResponse.rfind("", 0) == 0) { 163 | // if HTML, check if we hit the Cloudflare Captcha page 164 | if (jsonResponse.find("Cloudflare") != std::string::npos) { 165 | printError("Error", "The response content is not JSON, but HTML (Cloudflare Captcha!).\n"); 166 | return 1; 167 | } 168 | printError("Error", "The response content is not JSON, but HTML.\n"); 169 | return 1; 170 | } 171 | 172 | // parse response as json, catch parsing errors 173 | nlohmann::json json; 174 | try { 175 | json = nlohmann::json::parse(jsonResponse); 176 | } catch (nlohmann::json::parse_error& e) { 177 | auto const msg = fmt::format( 178 | "Message: {}\n Exception Id: {}\n Byte position of parsing error: {}\n", e.what(), e.id, e.byte); 179 | printError("JsonParsingError", msg.c_str()); 180 | return 2; 181 | } 182 | 183 | // ensure that the keys "status" and "data" are present 184 | if (!json.contains("status") && !json.contains("data")) { 185 | fmt::print("Response: {}", jsonResponse); 186 | printError("Error", "Json Response does not contain the keys \"status\" and \"data\"."); 187 | return 2; 188 | } 189 | auto const status = json["status"].get(); 190 | auto const data = json["data"]; 191 | 192 | /* 193 | csgostats.gg has 4 json responses to a sharecode POST request: error, queued, retrying, complete. 194 | */ 195 | 196 | if (status == "error") { 197 | std::string const msg = data["msg"].get(); 198 | 199 | auto const result = fmt::format(" Result: {} -> {}. \n", status, msg); 200 | WinCliColors::printRed(result); 201 | 202 | return 3; 203 | } 204 | 205 | if (status == "queued" || status == "retrying") { 206 | std::string const msg = data["msg"].get(); 207 | 208 | // msg contains HTML crap, let's cut that out 209 | std::string msgHtml = msg; 210 | std::string newMsg(" "); 211 | 212 | // get the "in queue #X" part (start of value (char 0) to char "<"span) 213 | std::string const queuedString = msgHtml.substr(0, msgHtml.find("<")); 214 | newMsg.append(queuedString); 215 | 216 | // get the "time remaining part (start of value (char "~" + 1) to end) 217 | std::string const timeString = msgHtml.substr(msgHtml.find("~") + 1, -1); 218 | newMsg.append(timeString); 219 | 220 | std::string const url = data["url"].get(); 221 | 222 | auto const result = fmt::format(" Result: {} -> {} | {} \n", status, url, newMsg); 223 | WinCliColors::printDarkOrange(result); 224 | 225 | return 4; 226 | } 227 | 228 | if (status == "complete") { 229 | std::string const url = data["url"].get(); 230 | 231 | auto const result = fmt::format(" Result: {} -> {} \n", status, url); 232 | WinCliColors::printGreen(result); 233 | 234 | return 5; 235 | } 236 | 237 | return 0; 238 | } -------------------------------------------------------------------------------- /src/csgostats/ShareCodeUpload.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_CSGOSTATS_SHARECODEUPLOAD_H_ 5 | #define SRC_CSGOSTATS_SHARECODEUPLOAD_H_ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "../ErrorHandler.h" 12 | #include "../platform/windows/WinCliColors.h" 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | class ShareCodeUpload 22 | { 23 | public: 24 | explicit ShareCodeUpload(bool verboseMode); 25 | ~ShareCodeUpload(); 26 | 27 | int uploadShareCode(std::string shareCode, std::string& responseContent); 28 | int processJsonResponse(std::string& jsonResponse); 29 | 30 | // int testProcessJsonResponse(); 31 | 32 | private: 33 | CURL* curl = nullptr; 34 | bool verbose = false; 35 | 36 | struct curl_slist* host = NULL; 37 | 38 | CURL* initCurlConnection(); 39 | }; 40 | 41 | #endif // SRC_CSGOSTATS_SHARECODEUPLOAD_H_ 42 | -------------------------------------------------------------------------------- /src/main/main.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "../ErrorHandler.h" 10 | #include "../ExceptionHandler.h" 11 | #include "../platform/windows/WinCliColors.h" 12 | #include "../VersionAndConstants.h" 13 | 14 | #include "../commands/cmd.globalstats.h" 15 | #include "../commands/cmd.help.h" 16 | #include "../commands/cmd.matches.h" 17 | #include "../commands/cmd.scoreboard.h" 18 | #include "../commands/cmd.upload.h" 19 | #include "../commands/cmd.user.h" 20 | 21 | #ifdef _WIN32 22 | #include 23 | #include 24 | #endif 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | // Includes needed for _setmode() (+io.h) 32 | #include 33 | 34 | void initSteamAPI(bool& verbose) 35 | { 36 | if (verbose) 37 | spdlog::info("[ Start ] STEAM_INIT"); 38 | 39 | if (SteamAPI_RestartAppIfNecessary(k_uAppIdInvalid)) { 40 | exit(1); 41 | } 42 | 43 | #ifdef _WIN32 44 | int savedStderr; 45 | if (!verbose) { 46 | savedStderr = _dup(_fileno(stderr)); 47 | freopen("NUL", "w", stderr); 48 | } 49 | #endif 50 | 51 | if (!SteamAPI_Init()) { 52 | printError("Fatal Error", "Steam not running. SteamAPI_Init() failed.\nPlease run Steam."); 53 | exit(1); 54 | } 55 | 56 | #ifdef _WIN32 57 | if (!verbose) { 58 | fflush(stderr); 59 | _dup2(savedStderr, _fileno(stderr)); 60 | _close(savedStderr); 61 | clearerr(stderr); 62 | } 63 | #endif 64 | 65 | if (!SteamUser()->BLoggedOn()) { 66 | printError("Fatal Error", "Steam user not logged in. SteamUser()->BLoggedOn() returned false.\nPlease log in."); 67 | exit(1); 68 | } 69 | 70 | // TODO(my_username): setPersonaState(Invisible) 7 71 | 72 | if (verbose) { 73 | spdlog::info("[ End ] STEAM_INIT"); 74 | } 75 | } 76 | 77 | std::thread createCallbackThread(bool& running, bool& verbose) 78 | { 79 | if (verbose) { 80 | spdlog::info("[ Start ] CallbackThread & Steam_RunCallbacks"); 81 | } 82 | 83 | auto CallbackThread = std::thread([&running]() { 84 | while (running) { 85 | try { 86 | std::this_thread::sleep_for(std::chrono::milliseconds(CSGO_CLI_STEAM_CALLBACK_INTERVAL)); 87 | SteamAPI_RunCallbacks(); 88 | } catch (ExceptionHandler& e) { 89 | printError("Fatal Error", e.what()); 90 | exit(1); 91 | } 92 | } 93 | }); 94 | 95 | if (verbose) { 96 | spdlog::info("[ End ] CallbackThread & Steam_RunCallbacks"); 97 | } 98 | 99 | return CallbackThread; 100 | } 101 | 102 | void initGameClientConnection(DataObject& data, bool& verbose) 103 | { 104 | if (verbose) { 105 | spdlog::info("[ Start ] Trying to establish a GameClient Connection"); 106 | } 107 | 108 | bool result = false; 109 | try { 110 | // make sure we are connected to the GameClient 111 | if (verbose) { 112 | spdlog::info(" -> Requesting: GameClient Connection"); 113 | } 114 | CSGOClient::GetInstance()->WaitForGameClientConnect(); 115 | if (verbose) { 116 | spdlog::info(" -> Successful: GameClient connected!"); 117 | } 118 | result = true; 119 | 120 | data.account_id = SteamUser()->GetSteamID().GetAccountID(); 121 | data.steam_id = SteamUser()->GetSteamID().ConvertToUint64(); 122 | data.steam_player_level = SteamUser()->GetPlayerSteamLevel(); 123 | // this is a "const char*" UTF data narrowing to std::string 124 | data.playername = reinterpret_cast(SteamFriends()->GetPersonaName()); 125 | 126 | CSteamID clan_id = SteamFriends()->GetClanByIndex(0); 127 | data.clan_name = SteamFriends()->GetClanName(clan_id); 128 | data.clan_tag = SteamFriends()->GetClanTag(clan_id); 129 | } catch (ExceptionHandler& e) { 130 | printError("Fatal error", e.what()); 131 | result = false; 132 | } 133 | 134 | if (!result) { 135 | printError("Fatal error", "GameClient could not connect."); 136 | exit(1); 137 | } 138 | 139 | if (verbose) { 140 | spdlog::info("[ End ] Trying to establish a GameClient Connection"); 141 | } 142 | } 143 | 144 | void exitIfGameIsRunning() 145 | { 146 | #ifdef _WIN32 147 | HWND test = FindWindowW(0, L"Counter-Strike: Global Offensive"); 148 | if (test != NULL) { 149 | printError("Warning", "\nCS:GO is currently running.\nPlease close the game, before running this program."); 150 | exit(1); 151 | } 152 | #endif 153 | } 154 | 155 | int main(int argc, char** argv) 156 | { 157 | SetConsoleOutputCP(CP_UTF8); 158 | WinCliColors::enableConsoleColor(true); 159 | 160 | bool paramVerbose = false; 161 | bool paramPrintUser = false; 162 | bool paramPrintMatches = false; 163 | bool paramPrintScoreboard = false; 164 | bool paramUploadShareCodes = false; 165 | bool paramUploadShareCode = false; 166 | bool paramPrintGlobalStats = false; 167 | std::string shareCode; 168 | 169 | // default action 170 | if (argc <= 1) { 171 | printHelp(); 172 | return 0; 173 | } 174 | 175 | for (int i = 1; i < argc; i = i + 1) { 176 | std::string option = argv[i]; 177 | if (option == "-h" || option == "--h" || option == "-help" || option == "/?") { 178 | printHelp(); 179 | return 0; 180 | } else if (option == "-V" || option == "--V" || option == "-version") { 181 | fmt::print( 182 | "{} version {}\n", 183 | WinCliColors::formatLightGreen(CSGO_CLI_BINARYNAME), 184 | WinCliColors::formatYellow(CSGO_CLI_VERSION)); 185 | return 0; 186 | } else if (option == "-v" || option == "--v" || option == "-verbose") { 187 | paramVerbose = true; 188 | } else if (option == "-vv" || option == "--vv") { 189 | paramVerbose = true; 190 | spdlog::set_level(spdlog::level::debug); 191 | } else if (option == "-globalstats") { 192 | paramPrintGlobalStats = true; 193 | } else if (option == "-matches") { 194 | paramPrintMatches = true; 195 | } else if (option == "-scoreboard") { 196 | paramPrintScoreboard = true; 197 | } else if (option == "-user") { 198 | paramPrintUser = true; 199 | } else if (option == "-upload") { 200 | paramPrintMatches = true; 201 | paramUploadShareCodes = true; 202 | } else if (option == "-sharecode" || option == "-s") { 203 | paramUploadShareCode = true; 204 | shareCode = argv[i + 1]; 205 | i++; 206 | } else if (option != "") { 207 | printError("ERROR (invalid argument)", option.c_str()); 208 | fmt::print("Please check: '{} -help'\n", CSGO_CLI_BINARYNAME); 209 | return 1; 210 | } 211 | } 212 | 213 | if (paramVerbose && !paramPrintUser && !paramPrintGlobalStats && !paramPrintMatches && !paramPrintScoreboard && 214 | !paramUploadShareCode && !paramUploadShareCodes) { 215 | printError("ERROR", "You are using (-v|-verbose) without any other command."); 216 | fmt::print("Please check: '{} -help'\n", CSGO_CLI_BINARYNAME); 217 | return 1; 218 | } 219 | 220 | // HANDLE UPLOADING OF SINGLE SHARECODE (no need to connect to STEAM_API) 221 | 222 | if (paramUploadShareCode) { 223 | uploadSingleShareCode(shareCode, paramVerbose); 224 | return 0; 225 | } 226 | 227 | // CONNECT TO STEAM_API 228 | 229 | exitIfGameIsRunning(); 230 | 231 | initSteamAPI(paramVerbose); 232 | 233 | bool running = true; 234 | 235 | std::thread CallbackThread = createCallbackThread(running, paramVerbose); 236 | 237 | DataObject data; 238 | 239 | initGameClientConnection(data, paramVerbose); 240 | 241 | // GET DATA 242 | 243 | if (paramPrintUser) { 244 | if (!requestPlayersProfile(data, paramVerbose)) { 245 | printError("Error", "Steam did not respond in time. Could not print -user."); 246 | exit(1); 247 | } 248 | if (!requestPlayersRankInfo(data, paramVerbose)) { 249 | printError("Error", "Steam did not respond in time. Could not print -user."); 250 | exit(1); 251 | } 252 | } 253 | 254 | if (paramPrintGlobalStats) { 255 | if (!requestGlobalStats(data, paramVerbose)) { 256 | printError("Error", "Steam did not respond in time. Could not print -globalstats."); 257 | exit(1); 258 | } 259 | } 260 | 261 | if (paramPrintMatches || paramPrintScoreboard || paramUploadShareCodes) { 262 | if (!requestRecentMatches(data, paramVerbose)) { 263 | printError("Error", "Steam did not respond in time."); 264 | exit(1); 265 | } 266 | } 267 | 268 | // OUTPUT 269 | 270 | if (paramPrintUser) { 271 | printPlayersProfile(data); 272 | } 273 | 274 | if (paramPrintGlobalStats) { 275 | printGlobalStats(data); 276 | } 277 | 278 | if (paramPrintMatches) { 279 | printMatches(data); 280 | } 281 | 282 | if (paramPrintScoreboard) { 283 | printScoreboard(data); 284 | } 285 | 286 | if (paramUploadShareCodes) { 287 | uploadReplayShareCodes(data, paramVerbose); 288 | } 289 | 290 | // SHUTDOWN 291 | 292 | running = false; 293 | CallbackThread.join(); 294 | CSGOClient::Destroy(); 295 | SteamAPI_Shutdown(); 296 | 297 | return EXIT_SUCCESS; 298 | } 299 | -------------------------------------------------------------------------------- /src/platform/windows/WinCliColors.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "WinCliColors.h" 5 | 6 | // console color heck by mlocati: https://gist.github.com/mlocati/21a9233ac83f7d3d7837535bc109b3b7 7 | typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); 8 | 9 | namespace WinCliColors 10 | { 11 | bool consoleHasColorSupport() 12 | { 13 | const DWORD MINV_MAJOR = 10, MINV_MINOR = 0, MINV_BUILD = 10586; 14 | 15 | HMODULE hMod = GetModuleHandle(TEXT("ntdll.dll")); 16 | 17 | if (!hMod) { 18 | return false; 19 | } 20 | 21 | RtlGetVersionPtr rlGetVersion = (RtlGetVersionPtr)GetProcAddress(hMod, "RtlGetVersion"); 22 | 23 | if (rlGetVersion == NULL) { 24 | return false; 25 | } 26 | 27 | RTL_OSVERSIONINFOW version_info = {0}; 28 | version_info.dwOSVersionInfoSize = sizeof(version_info); 29 | 30 | if (rlGetVersion(&version_info) != 0) { 31 | return false; 32 | } 33 | 34 | if (version_info.dwMajorVersion > MINV_MAJOR || 35 | (version_info.dwMajorVersion == MINV_MAJOR && 36 | (version_info.dwMinorVersion > MINV_MINOR || 37 | (version_info.dwMinorVersion == MINV_MINOR && version_info.dwBuildNumber >= MINV_BUILD)))) { 38 | return true; 39 | } 40 | 41 | return false; 42 | } 43 | 44 | bool enableConsoleColor(bool enabled) 45 | { 46 | if (!consoleHasColorSupport()) { 47 | return false; 48 | } 49 | 50 | HANDLE hStdOut; 51 | hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); 52 | 53 | if (INVALID_HANDLE_VALUE == hStdOut) { 54 | return false; 55 | } 56 | 57 | DWORD mode; 58 | if (!GetConsoleMode(hStdOut, &mode)) { 59 | return false; 60 | } 61 | 62 | if (((mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) ? 1 : 0) == (enabled ? 1 : 0)) { 63 | return true; 64 | } 65 | 66 | if (enabled) { 67 | mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; 68 | } else { 69 | mode &= ~ENABLE_VIRTUAL_TERMINAL_PROCESSING; 70 | } 71 | 72 | if (SetConsoleMode(hStdOut, mode)) { 73 | return true; 74 | } 75 | 76 | return false; 77 | } 78 | 79 | // ---------------------------------------------------------------------------- 80 | 81 | void printRed(std::string message) 82 | { 83 | return fmt::print(fmt::fg(fmt::color::red), "{}", message); 84 | } 85 | void printDarkOrange(std::string message) 86 | { 87 | return fmt::print(fmt::fg(fmt::color::dark_orange), "{}", message); 88 | } 89 | void printYellow(std::string message) 90 | { 91 | return fmt::print(fmt::fg(fmt::color::yellow), "{}", message); 92 | } 93 | void printGreen(std::string message) 94 | { 95 | return fmt::print(fmt::fg(fmt::color::green), "{}", message); 96 | } 97 | void printTerminalYellow(std::string message) 98 | { 99 | return fmt::print(fmt::fg(fmt::terminal_color::yellow), "{}", message); 100 | } 101 | 102 | // ---------------------------------------------------------------------------- 103 | 104 | std::string formatRed(std::string message) 105 | { 106 | return fmt::format(fmt::fg(fmt::color::red), message); 107 | } 108 | std::string formatDarkOrange(std::string message) 109 | { 110 | return fmt::format(fmt::fg(fmt::color::dark_orange), message); 111 | } 112 | std::string formatYellow(std::string message) 113 | { 114 | return fmt::format(fmt::fg(fmt::color::yellow), message); 115 | } 116 | std::string formatGreen(std::string message) 117 | { 118 | return fmt::format(fmt::fg(fmt::color::green), message); 119 | } 120 | std::string formatLightGreen(std::string message) 121 | { 122 | return fmt::format(fmt::fg(fmt::color::light_green), message); 123 | } 124 | std::string formatTerminalYellow(std::string message) 125 | { 126 | return fmt::format(fmt::fg(fmt::terminal_color::yellow), message); 127 | } 128 | 129 | } // namespace WinCliColors -------------------------------------------------------------------------------- /src/platform/windows/WinCliColors.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2018-present Jens A. Koch 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #ifndef SRC_PLATFORM_WINDOWS_WINCLICOLORS_H_ 5 | #define SRC_PLATFORM_WINDOWS_WINCLICOLORS_H_ 6 | 7 | #include 8 | #include 9 | 10 | #include "Windows.h" 11 | 12 | #include 13 | 14 | namespace WinCliColors 15 | { 16 | bool consoleHasColorSupport(); 17 | bool enableConsoleColor(bool enabled); 18 | 19 | void printRed(std::string message); 20 | void printDarkOrange(std::string message); 21 | void printYellow(std::string message); 22 | void printGreen(std::string message); 23 | void printTerminalYellow(std::string message); 24 | 25 | std::string formatRed(std::string message); 26 | std::string formatDarkOrange(std::string message); 27 | std::string formatYellow(std::string message); 28 | std::string formatGreen(std::string message); 29 | std::string formatLightGreen(std::string message); 30 | std::string formatTerminalYellow(std::string message); 31 | 32 | } // namespace WinCliColors 33 | 34 | #endif // SRC_PLATFORM_WINDOWS_WINCLICOLORS_H_ -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------- 2 | # Setup Catch Test Library 3 | #------------------------------------------------------------------- 4 | 5 | set(Catch2_DIR "${VCPKG_DIR}/share/Catch2") 6 | find_package(Catch2 CONFIG REQUIRED) 7 | 8 | include(Catch) 9 | 10 | #------------------------------------------------------------------- 11 | # Setup CMake's CTest 12 | #------------------------------------------------------------------- 13 | 14 | # remove superflous additionaly added build targets (continous, nightly, etc.) 15 | set_property(GLOBAL PROPERTY CTEST_TARGETS_ADDED 1) 16 | 17 | set(CTEST_OUTPUT_ON_FAILURE TRUE) 18 | 19 | include(CTest) 20 | enable_testing() 21 | 22 | #------------------------------------------------------------------- 23 | # Setup Test Suite (app: test_suite) 24 | #------------------------------------------------------------------- 25 | 26 | set(TEST_SOURCES 27 | #main.tests.cpp 28 | ShareCodeUpload.tests.cpp 29 | ) 30 | 31 | add_executable(test_suite ${TEST_SOURCES}) 32 | 33 | target_compile_features(test_suite PUBLIC cxx_std_20) 34 | 35 | target_include_directories(test_suite 36 | PUBLIC 37 | ${PROJECT_SOURCE_DIR}/src 38 | ) 39 | 40 | target_link_libraries(test_suite 41 | PRIVATE 42 | #Catch2::Catch2 43 | Catch2::Catch2WithMain 44 | csgo_cli_lib 45 | ) 46 | 47 | show_build_target_properties(test_suite) 48 | 49 | #------------------------------------------------------------------- 50 | # Setup Catch's Test Discovery 51 | #------------------------------------------------------------------- 52 | 53 | if(DEFINED ENV{GITHUB_ACTIONS} AND "$ENV{GITHUB_ACTIONS}" STREQUAL "true") 54 | # if tests are running on CI server, create an xml test report 55 | catch_discover_tests(test_suite 56 | REPORTER junit 57 | OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR} 58 | OUTPUT_SUFFIX test_results_${VCPKG_TARGET_TRIPLET}_${CMAKE_BUILD_TYPE}.junit.xml 59 | ) 60 | else() 61 | # if tests are running on local machine, print test results to console 62 | catch_discover_tests(test_suite REPORTER console) 63 | endif() 64 | -------------------------------------------------------------------------------- /tests/ShareCodeUpload.tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../src/csgostats/ShareCodeUpload.h" 5 | 6 | #include 7 | 8 | TEST_CASE("[ShareCodeUpload] Response to POST request can be parsed:", "[testProcessJsonResponse]") { 9 | 10 | ShareCodeUpload *shareCodeUpload = new ShareCodeUpload(false); 11 | 12 | SECTION("if response is empty, return 1") { 13 | std::string response = ""; 14 | int r = shareCodeUpload->processJsonResponse(response); 15 | REQUIRE(r == 1); 16 | } 17 | 18 | SECTION("if response content is not JSON, but HTML, return 1") { 19 | std::string response = ""; 20 | int r = shareCodeUpload->processJsonResponse(response); 21 | REQUIRE(r == 1); 22 | } 23 | 24 | SECTION("if response content is not JSON, but HTML and a Cloudflare Captcha, return 1") { 25 | std::string response = " ... Cloudflare"; 26 | int r = shareCodeUpload->processJsonResponse(response); 27 | REQUIRE(r == 1); 28 | } 29 | 30 | /*SECTION("if response content is JSON, but syntax - invalid JSON, return 2") { 31 | auto response = R"( 32 | { 33 | "status: 34 | } 35 | )"_json; 36 | int r = shareCodeUpload->processJsonResponse(response.dump()); 37 | REQUIRE(r == 2); 38 | }*/ 39 | 40 | SECTION("if response content is JSON, with status error, return 3") { 41 | 42 | auto response = R"( 43 | { 44 | "status": "error", 45 | "data" : { 46 | "msg": "Failed to add to the queue, please verify sharecode", 47 | "index" : null, 48 | "sharecode" : null 49 | }, 50 | "error" : 1 51 | } 52 | )"_json; 53 | 54 | std::string response_string = response.dump(); 55 | int r = shareCodeUpload->processJsonResponse(response_string); 56 | REQUIRE(r == 3); 57 | } 58 | 59 | SECTION("if response content is JSON, with status queued, return 4") { 60 | auto response = R"( 61 | { 62 | "status": "queued", 63 | "data" : { 64 | "msg": "in Queue #9 <\/span> ~ Time Remaining 4m 50s", 65 | "queue_id" : 1827459, 66 | "queue_pos" : 9, 67 | "in_queue" : 1, 68 | "demo_id" : 0, 69 | "url" : "https:\/\/csgostats.gg\/match\/processing\/1827459\/", 70 | "demo_url" : null, 71 | "start" : false, 72 | "index" : 0, 73 | "sharecode" : "CSGO-R7CCX-WRquC-xFxQO-hvFHt-uMOcF" 74 | }, 75 | "error" : 0 76 | } 77 | )"_json; 78 | 79 | std::string response_string = response.dump(); 80 | int r = shareCodeUpload->processJsonResponse(response_string); 81 | REQUIRE(r == 4); 82 | } 83 | 84 | SECTION("if response content is JSON, with status retrying, return 4") 85 | { 86 | auto response = R"( 87 | { 88 | "status" : "retrying", 89 | "data" : { 90 | "msg" : "Failed to parse demo file. Demo may be corrupt
Retrying, in Queue #1099 <\/span> ~ Time Remaining 7h 39m", 91 | "queue_id" : 70007, 92 | "queue_pos" : 1099, 93 | "in_queue" : 1, 94 | "demo_id" : 0, 95 | "url" : "https:\/\/csgostats.gg\/match\/processing\/70007", 96 | "demo_url" : "http:\/\/replay187.valve.net\/730\/003106056003414655216_0510340674.dem.bz2", 97 | "start" : false, 98 | "index" : "0", 99 | "sharecode" : "CSGO-U6MWi-hYFWJ-opPwD-JciHm-qOijD" 100 | }, 101 | "error" : 2 102 | } 103 | )"_json; 104 | 105 | std::string response_string = response.dump(); 106 | int r = shareCodeUpload->processJsonResponse(response_string); 107 | REQUIRE(r == 4); 108 | } 109 | 110 | SECTION("if response content is JSON, with status complete, return 5") { 111 | auto response = R"( 112 | { 113 | "status": "complete", 114 | "data" : { 115 | "msg": "Complete - View<\/a>", 116 | "index" : 0, 117 | "sharecode" : "CSGO-c5vYt-KEmjy-j6FYV-tz8Rt-JrzUO", 118 | "queue_id" : 1765382, 119 | "demo_id" : 1731725, 120 | "url" : "https:\/\/csgostats.gg\/match\/1731725" 121 | }, 122 | "error" : 0 123 | } 124 | )"_json; 125 | 126 | std::string response_string = response.dump(); 127 | int r = shareCodeUpload->processJsonResponse(response_string); 128 | REQUIRE(r == 5); 129 | } 130 | 131 | }; 132 | -------------------------------------------------------------------------------- /tests/main.tests.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_RUNNER 2 | #include 3 | #include 4 | 5 | #include "../src/platform/windows/WinCliColors.h" 6 | 7 | int main( int argc, char* argv[] ) 8 | { 9 | SetConsoleOutputCP(CP_UTF8); 10 | WinCliColors::enableConsoleColor(true); 11 | std::cout << "csgo_cli TestSuite\n" << std::endl; 12 | 13 | Catch::Session session; // There must be exactly one instance 14 | 15 | // writing to session.configData() here sets defaults 16 | // this is the preferred way to set them 17 | 18 | int returnCode = session.applyCommandLine( argc, argv ); 19 | if( returnCode != 0 ) // Indicates a command line error 20 | return returnCode; 21 | 22 | // writing to session.configData() or session.Config() here 23 | // overrides command line args 24 | // only do this if you know you need to 25 | 26 | int numFailed = session.run(); 27 | 28 | // numFailed is clamped to 255 as some unixes only use the lower 8 bits. 29 | // This clamping has already been applied, so just return it here 30 | // You can also do any post run clean-up here 31 | return numFailed; 32 | } -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csgo-cli", 3 | "version": "1.4.0", 4 | "maintainers": "Jens A. Koch ", 5 | "description": "csgo-cli shows your CS:GO user account, stats and latest matches. It also uploads demo sharecodes to csgostats.gg. :sunglasses:", 6 | "homepage": "https://github.com/jakoch/csgo-cli", 7 | "dependencies": [ 8 | "catch2", 9 | { 10 | "name": "curl", 11 | "default-features": false, 12 | "features": [ 13 | "http2", 14 | "ssl", 15 | "winssl" 16 | ] 17 | }, 18 | "fmt", 19 | "nlohmann-json", 20 | "protobuf", 21 | "spdlog", 22 | "zlib" 23 | ] 24 | } 25 | --------------------------------------------------------------------------------