├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question---suggestion---other.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── mirror.yml │ └── protobuf.yml ├── .gitignore ├── .gitmodules ├── .golangci.yml ├── LICENSE.md ├── README.md ├── docs ├── game-events.md ├── out │ ├── parallel-processing │ │ └── parallel-processing.png │ ├── parsing-loop │ │ └── parsing-loop.png │ └── processing-loop │ │ └── processing-loop.png ├── parallel-processing.puml ├── parsing-loop.puml └── processing-loop.puml ├── examples ├── README.md ├── _assets │ ├── metadata │ │ ├── ar_baggage.txt │ │ ├── ar_shoots.txt │ │ ├── cs_italy.txt │ │ ├── cs_office.txt │ │ ├── de_ancient.txt │ │ ├── de_anubis.txt │ │ ├── de_dust.txt │ │ ├── de_dust2.txt │ │ ├── de_inferno.txt │ │ ├── de_inferno_s2.txt │ │ ├── de_mirage.txt │ │ ├── de_nuke.txt │ │ ├── de_overpass.txt │ │ ├── de_overpass_2v2.txt │ │ ├── de_train.txt │ │ └── de_vertigo.txt │ └── radar │ │ ├── ar_baggage_lower_radar_psd.png │ │ ├── ar_baggage_radar_psd.png │ │ ├── ar_shoots_radar_psd.png │ │ ├── cs_italy_radar_psd.png │ │ ├── cs_office_radar_psd.png │ │ ├── de_ancient_radar_psd.png │ │ ├── de_anubis_radar_psd.png │ │ ├── de_dust2_radar_psd.png │ │ ├── de_inferno_radar_psd.png │ │ ├── de_mirage_radar_psd.png │ │ ├── de_nuke_lower_radar_psd.png │ │ ├── de_nuke_radar_psd.png │ │ ├── de_overpass_radar_psd.png │ │ ├── de_train_lower_radar_psd.png │ │ ├── de_train_radar_psd.png │ │ ├── de_vertigo_lower_radar_psd.png │ │ ├── de_vertigo_radar_psd.png │ │ ├── default_png.vtex_c.png │ │ ├── workshop_preview_radar_psd.png │ │ └── workshop_preview_radar_tga.png ├── common.go ├── encrypted-net-messages │ ├── README.md │ ├── enc_net_msg.go │ └── enc_net_msg_test.go ├── entities │ ├── README.md │ ├── entities.go │ └── entities_test.go ├── heatmap │ ├── README.md │ ├── heatmap.go │ ├── heatmap.jpg │ └── heatmap_test.go ├── map_metadata.go ├── map_metadata_test.go ├── mocking │ ├── README.md │ ├── collect_kills.go │ └── mocking_test.go ├── nade-trajectories │ ├── README.md │ ├── nade_trajectories.go │ ├── nade_trajectories.jpg │ └── nade_trajectories_test.go ├── net-messages │ ├── README.md │ ├── netmessages.go │ └── netmessages_test.go ├── print-events │ ├── README.md │ ├── print_events.go │ └── print_events_test.go ├── voice-capture │ └── README.md └── web-assembly │ └── README.md ├── go.mod ├── go.sum ├── internal └── bitread │ └── bitread.go ├── pkg └── demoinfocs │ ├── common │ ├── common.go │ ├── common_test.go │ ├── entity_util.go │ ├── entity_util_test.go │ ├── equipment.go │ ├── equipment_test.go │ ├── gamerules.go │ ├── hostage.go │ ├── hostage_test.go │ ├── inferno.go │ ├── inferno_test.go │ ├── player.go │ └── player_test.go │ ├── constants │ └── constants.go │ ├── datatables.go │ ├── datatables_test.go │ ├── debug_off.go │ ├── debug_on.go │ ├── demoinfocs_test.go │ ├── doc.go │ ├── event-list-dump │ ├── 13990.bin │ ├── 13992.bin │ ├── 14023.bin │ └── 14070.bin │ ├── events │ ├── events.go │ └── events_test.go │ ├── examples_test.go │ ├── fake │ ├── game_rules.go │ ├── game_state.go │ ├── parser.go │ ├── parser_test.go │ └── participants.go │ ├── game_events.go │ ├── game_events_test.go │ ├── game_rules_interface.go │ ├── game_state.go │ ├── game_state_interface.go │ ├── game_state_test.go │ ├── matchinfo.go │ ├── msg │ ├── cstrike15_gcmessages.pb.go │ ├── cstrike15_usermessages.pb.go │ ├── doc.go │ ├── engine_gcmessages.pb.go │ ├── generate.sh │ ├── msg.go │ ├── netmessages.pb.go │ ├── proto │ │ ├── cstrike15_gcmessages.proto │ │ ├── cstrike15_usermessages.proto │ │ ├── engine_gcmessages.proto │ │ ├── netmessages.proto │ │ └── steammessages.proto │ └── steammessages.pb.go │ ├── msgs2 │ ├── cs_gameevents.pb.go │ ├── cstrike15_gcmessages.pb.go │ ├── cstrike15_usermessages.pb.go │ ├── demo.pb.go │ ├── doc.go │ ├── engine_gcmessages.pb.go │ ├── gameevents.pb.go │ ├── gcsdk_gcmessages.pb.go │ ├── generate.sh │ ├── msg.go │ ├── netmessages.pb.go │ ├── network_connection.pb.go │ ├── networkbasetypes.pb.go │ ├── proto │ │ └── s2 │ │ │ ├── base_gcmessages.proto │ │ │ ├── base_gcmessages_csgo.proto │ │ │ ├── c_peer2peer_netmessages.proto │ │ │ ├── clientmessages.proto │ │ │ ├── connectionless_netmessages.proto │ │ │ ├── cs_gameevents.proto │ │ │ ├── cs_usercmd.proto │ │ │ ├── cstrike15_gcmessages.proto │ │ │ ├── cstrike15_usermessages.proto │ │ │ ├── demo.proto │ │ │ ├── econ_gcmessages.proto │ │ │ ├── engine_gcmessages.proto │ │ │ ├── enums_clientserver.proto │ │ │ ├── fatdemo.proto │ │ │ ├── gameevents.proto │ │ │ ├── gcsdk_gcmessages.proto │ │ │ ├── gcsystemmsgs.proto │ │ │ ├── netmessages.proto │ │ │ ├── network_connection.proto │ │ │ ├── networkbasetypes.proto │ │ │ ├── networksystem_protomessages.proto │ │ │ ├── steamdatagram_messages_auth.proto │ │ │ ├── steamdatagram_messages_sdr.proto │ │ │ ├── steammessages.proto │ │ │ ├── steammessages_base.proto │ │ │ ├── steammessages_cloud.steamworkssdk.proto │ │ │ ├── steammessages_gamenetworkingui.proto │ │ │ ├── steammessages_helprequest.steamworkssdk.proto │ │ │ ├── steammessages_oauth.steamworkssdk.proto │ │ │ ├── steammessages_player.steamworkssdk.proto │ │ │ ├── steammessages_publishedfile.steamworkssdk.proto │ │ │ ├── steammessages_unified_base.steamworkssdk.proto │ │ │ ├── steamnetworkingsockets_messages.proto │ │ │ ├── steamnetworkingsockets_messages_certs.proto │ │ │ ├── steamnetworkingsockets_messages_udp.proto │ │ │ ├── te.proto │ │ │ ├── uifontfile_format.proto │ │ │ ├── usercmd.proto │ │ │ ├── usermessages.proto │ │ │ └── valveextensions.proto │ ├── steammessages.pb.go │ ├── te.pb.go │ └── usermessages.pb.go │ ├── net_messages.go │ ├── parser.go │ ├── parser_interface.go │ ├── parser_test.go │ ├── parsing.go │ ├── participants_interface.go │ ├── s2_commands.go │ ├── sendtables │ ├── entity.go │ ├── entity_interface.go │ ├── entity_test.go │ ├── fake │ │ ├── doc.go │ │ ├── entity.go │ │ └── property.go │ ├── propdecoder.go │ ├── propdecoder_test.go │ ├── property_interface.go │ ├── sendtables.go │ ├── sendtables_test.go │ ├── serverclass_interface.go │ └── st_parser.go │ ├── sendtables2 │ ├── class.go │ ├── entity.go │ ├── field.go │ ├── field_decoder.go │ ├── field_patch.go │ ├── field_path.go │ ├── field_state.go │ ├── field_type.go │ ├── huffman.go │ ├── panicf.go │ ├── parser.go │ ├── quantizedfloat.go │ ├── reader.go │ └── serializer.go │ ├── stringtables.go │ ├── user_messages.go │ └── user_messages_test.go ├── scripts ├── .gitignore ├── build.sh ├── check-interfaces-generated.sh ├── coverage.sh ├── download-test-data.sh ├── generate-interfaces.sh ├── git-hooks │ ├── link-git-hooks.sh │ ├── pre-commit.sh │ └── pre-push.sh ├── lint-changes.sh ├── profile.sh ├── race-tests.sh ├── regression-tests.sh └── unit-tests.sh └── test ├── .gitignore └── default.golden /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Linux shell scripts 5 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Download link to an affected demo: HLTV, ESEA, Matchmaking share link or other 12 | 13 | Code: 14 | ```go 15 | func x() { 16 | } 17 | ``` 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Library version** 23 | vX.X.X 24 | 25 | **Additional context** 26 | Add any other context about the problem here - like your OS if that could be relevant. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Higher goal** 8 | Describe what you're trying to do on a high level. i.e. 'I want to create a heatmap of x but can't get that data' instead of 'I want a new function A() to provide me with data x'. 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question---suggestion---other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question / Suggestion / Other 3 | about: Anything that isn't exactly a bug or feature 4 | 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | 9 | env: 10 | REVIEWDOG_REPORTER: github-check 11 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: 1.20.x 18 | 19 | - name: Install Reviewdog 20 | uses: reviewdog/action-setup@v1 21 | with: 22 | reviewdog_version: v0.11.0 23 | 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | 27 | - name: Download Go Deps 28 | run: go mod download 29 | 30 | - name: Install Tools 31 | run: | 32 | # install 7zip for decompressing test demos 33 | sudo apt-get install -y p7zip-full 34 | 35 | # Install interface generator 36 | go install github.com/vburenin/ifacemaker@v1.2.1 37 | 38 | # Fetch refs for linter 39 | git config remote.origin.fetch +refs/heads/*:refs/remotes/origin/* 40 | git fetch 41 | 42 | # Install golangci-lint 43 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.52.2 44 | 45 | - name: Build 46 | run: scripts/build.sh 47 | 48 | - name: Check Generated Code 49 | run: scripts/check-interfaces-generated.sh 50 | 51 | - name: Lint Changed Code 52 | run: scripts/lint-changes.sh 53 | continue-on-error: true 54 | 55 | - name: Race Tests 56 | run: scripts/race-tests.sh 57 | 58 | # Note: We run ALL tests again to get full coverage 59 | # Race tests are too slow and skip the regression set 60 | - name: All Tests + Coverage 61 | run: | 62 | scripts/coverage.sh 63 | bash <(curl -s https://codecov.io/bash) 64 | 65 | test-csda: # Run tests of CS Demo Analyzer 66 | runs-on: ubuntu-latest 67 | 68 | steps: 69 | - name: Checkout demoinfocs-golang repo 70 | uses: actions/checkout@v4 71 | with: 72 | path: demoinfocs-golang 73 | 74 | - name: Checkout CSDA repo 75 | uses: actions/checkout@v4 76 | with: 77 | repository: akiver/cs-demo-analyzer 78 | path: cs-demo-analyzer 79 | ref: main 80 | 81 | - name: Install Go 82 | uses: actions/setup-go@v5 83 | with: 84 | go-version-file: cs-demo-analyzer/go.mod 85 | cache-dependency-path: cs-demo-analyzer/go.sum 86 | 87 | - name: Update CSDA mod 88 | run: | 89 | cd cs-demo-analyzer 90 | go mod edit -replace github.com/markus-wa/demoinfocs-golang/v4=../demoinfocs-golang 91 | go mod tidy 92 | 93 | - name: Download demos cache file 94 | run: | 95 | cd cs-demo-analyzer 96 | curl -L -o demos.txt https://gitlab.com/akiver/cs-demos/-/raw/main/demos.txt 97 | 98 | - name: Restore demos cache 99 | uses: actions/cache@v4 100 | id: demos-cache 101 | with: 102 | path: cs-demo-analyzer/cs-demos 103 | key: demos-${{ hashFiles('cs-demo-analyzer/demos.txt') }} 104 | 105 | - name: Download demos 106 | if: steps.demos-cache.outputs.cache-hit != 'true' 107 | run: | 108 | cd cs-demo-analyzer 109 | ./download-demos.sh 110 | 111 | - name: Test 112 | run: | 113 | cd cs-demo-analyzer 114 | go test ./tests 115 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master, v3] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master, v3] 9 | schedule: 10 | - cron: '0 1 * * 4' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v1 27 | 28 | - name: Autobuild 29 | uses: github/codeql-action/autobuild@v1 30 | 31 | - name: Perform CodeQL Analysis 32 | uses: github/codeql-action/analyze@v1 33 | -------------------------------------------------------------------------------- /.github/workflows/mirror.yml: -------------------------------------------------------------------------------- 1 | name: Mirror 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build: 9 | name: Mirror to GitLab 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Mirror to GitLab 13 | uses: wearerequired/git-mirror-action@v1.2.0 14 | env: 15 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 16 | with: 17 | source-repo: 'https://github.com/markus-wa/demoinfocs-golang.git' 18 | destination-repo: 'git@gitlab.com:markus-wa/demoinfocs-golang.git' 19 | -------------------------------------------------------------------------------- /.github/workflows/protobuf.yml: -------------------------------------------------------------------------------- 1 | name: Update Protobufs 2 | on: 3 | workflow_dispatch: {} 4 | schedule: 5 | - cron: '0 0 * * *' 6 | jobs: 7 | protobuf: 8 | name: Update Protobufs 9 | runs-on: ubuntu-latest 10 | env: 11 | GameTracking_dir: /tmp/GameTracking-CSGO 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Download latest Protobufs 15 | run: | 16 | git clone --depth=1 https://github.com/SteamDatabase/GameTracking-CSGO.git $GameTracking_dir 17 | cp $GameTracking_dir/Protobufs/{cstrike15_*.proto,engine_gcmessages.proto,netmessages.proto,steammessages.proto,gcsdk_gcmessages.proto,networkbasetypes.proto,network_connection.proto} pkg/demoinfocs/msg/proto 18 | 19 | if [[ ! `git status --porcelain` ]]; then 20 | echo "PROTOBUFS_CHANGED=false" >> $GITHUB_ENV 21 | exit 0 22 | else 23 | echo "PROTOBUFS_CHANGED=true" >> $GITHUB_ENV 24 | fi 25 | 26 | - name: Install Go 27 | if: env.PROTOBUFS_CHANGED == 'true' 28 | uses: actions/setup-go@v2 29 | with: 30 | go-version: 1.21.x 31 | 32 | - name: Install Protobuf tools 33 | if: env.PROTOBUFS_CHANGED == 'true' 34 | run: | 35 | sudo apt-get update 36 | sudo apt-get install unzip 37 | 38 | wget -O /tmp/protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v21.3/protoc-21.3-linux-x86_64.zip 39 | unzip /tmp/protoc.zip -d /tmp/protoc 40 | 41 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 42 | 43 | - name: Generate Go code 44 | if: env.PROTOBUFS_CHANGED == 'true' 45 | run: | 46 | cd pkg/demoinfocs/msg 47 | 48 | export PATH=/tmp/protoc/bin:$PATH 49 | ./generate.sh 50 | 51 | - name: Commit changes 52 | if: env.PROTOBUFS_CHANGED == 'true' 53 | run: | 54 | commit_sha=$(pushd $GameTracking_dir > /dev/null && git rev-parse HEAD && popd > /dev/null) 55 | echo "COMMIT_SHA=$commit_sha" >> $GITHUB_ENV 56 | 57 | - name: Create Pull Request 58 | id: pr 59 | if: env.PROTOBUFS_CHANGED == 'true' 60 | uses: peter-evans/create-pull-request@v3 61 | with: 62 | token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 63 | title: "protobuf: updated to ${{ env.COMMIT_SHA }}" 64 | base: master 65 | branch: update-protobufs 66 | author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 67 | commit-message: | 68 | protobuf: updated to ${{ env.COMMIT_SHA }} 69 | 70 | see https://github.com/SteamDatabase/GameTracking-CSGO 71 | body: | 72 | see https://github.com/SteamDatabase/GameTracking-CSGO 73 | 74 | - name: Print PR outputs 75 | if: env.PROTOBUFS_CHANGED == 'true' 76 | run: | 77 | echo "Pull Request Number - ${{ env.PULL_REQUEST_NUMBER }}" 78 | echo "Pull Request Number - ${{ steps.pr.outputs.pull-request-number }}" 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Test binary 2 | demoinfocs-golang.test 3 | demoinfocs-golang.test.exe 4 | .vscode/ 5 | .idea/ 6 | *.log 7 | log.* 8 | coverage.* 9 | *.out 10 | demoinfocs.test -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "cs-demos"] 2 | path = test/cs-demos 3 | url = https://gitlab.com/markus-wa/cs-demos-2 4 | branch = master 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 5m 3 | build-tags: 4 | - debugdemoinfocs 5 | skip-dirs: 6 | - msg 7 | skip-files: 8 | - parser_interface.go 9 | - game_state_interface.go 10 | allow-parallel-runners: true 11 | 12 | linters: 13 | disable-all: true 14 | enable: 15 | - bodyclose 16 | - dogsled 17 | - dupl 18 | - exportloopref 19 | - exhaustive 20 | - funlen 21 | - goconst 22 | - gocritic 23 | - gocyclo 24 | - gofmt 25 | - goimports 26 | - goprintffuncname 27 | - gosec 28 | - gosimple 29 | - govet 30 | - ineffassign 31 | - misspell 32 | - nakedret 33 | - noctx 34 | - nolintlint 35 | - rowserrcheck 36 | - staticcheck 37 | - stylecheck 38 | - unconvert 39 | - unparam 40 | - unused 41 | - whitespace 42 | - asciicheck 43 | - gocognit 44 | - godox 45 | - nestif 46 | - prealloc 47 | - revive 48 | - wsl 49 | 50 | issues: 51 | exclude-rules: 52 | # Exclude some linters from running on tests files. 53 | - path: _test\.go 54 | linters: 55 | - wsl 56 | - funlen 57 | 58 | linters-settings: 59 | gocritic: 60 | disabled-checks: 61 | - ifElseChain 62 | gci: 63 | local-prefixes: github.com/markus-wa/demoinfocs-golang/v3 64 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2024 Markus Walther 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/out/parallel-processing/parallel-processing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/docs/out/parallel-processing/parallel-processing.png -------------------------------------------------------------------------------- /docs/out/parsing-loop/parsing-loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/docs/out/parsing-loop/parsing-loop.png -------------------------------------------------------------------------------- /docs/out/processing-loop/processing-loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/docs/out/processing-loop/processing-loop.png -------------------------------------------------------------------------------- /docs/parallel-processing.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | participant Consumer 3 | participant Parser 4 | participant Queue 5 | 6 | Consumer ++ 7 | Consumer -> Parser ++: ParseToEnd 8 | par 9 | loop parsing loop 10 | Parser -> Parser ++: parseFrame 11 | Parser -> Queue ++: enqueue net-msg 12 | Parser -- 13 | end 14 | Parser -- 15 | 16 | else 17 | 18 | loop processing loop 19 | Queue --> Parser --: receive net-msg 20 | Parser ++ 21 | Parser -> Parser ++: process 22 | Parser -> Consumer ++: call EventHandler 23 | Consumer --> Parser -- 24 | Parser -- 25 | end 26 | end 27 | Parser -> Consumer -- 28 | Consumer -- 29 | 30 | @enduml -------------------------------------------------------------------------------- /docs/parsing-loop.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | participant Consumer 3 | participant Parser 4 | participant BitReader 5 | participant Message 6 | participant Queue 7 | 8 | Consumer -> Parser ++: ParseToEnd 9 | 10 | loop while more messages to read 11 | Parser -> Parser ++: parseFrame 12 | 13 | Parser -> BitReader ++: read 14 | BitReader --> Parser -- 15 | 16 | Parser -> Message ++: decode 17 | Message --> Parser -- 18 | 19 | Parser -> Queue: enqueue net-msg 20 | Parser -- 21 | end 22 | 23 | Parser -> Parser: wait for processing loop 24 | 25 | Parser --> Consumer -- 26 | 27 | @enduml -------------------------------------------------------------------------------- /docs/processing-loop.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | participant Consumer 3 | participant Queue 4 | participant NetMessageDispatcher 5 | participant Parser 6 | participant GameState 7 | participant EventDispatcher 8 | participant Consumer 9 | 10 | loop while messages in queue 11 | Queue -> NetMessageDispatcher ++: receive net-msg 12 | 13 | NetMessageDispatcher -> Parser ++: handle 14 | 15 | Parser -> GameState ++: update 16 | GameState --> Parser -- 17 | 18 | Parser -> EventDispatcher ++: dispatch 19 | EventDispatcher -> Consumer ++: handle 20 | Consumer --> EventDispatcher -- 21 | EventDispatcher --> Parser -- 22 | 23 | Parser --> NetMessageDispatcher -- 24 | 25 | NetMessageDispatcher -> Consumer ++: handle 26 | Consumer --> NetMessageDispatcher -- 27 | 28 | NetMessageDispatcher -- 29 | end 30 | 31 | @enduml -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Here you can find a overview of examples on how to use demoinfocs-golang. 4 | 5 | :information_source: Example code may not be production ready - specifically error handling and such is done in a simplified way and should not be used in critical systems as-is. 6 | 7 | |Example|Description 8 | |-|-| 9 | |[heatmap](heatmap)|Creating a heatmap from positions where players fired shots from| 10 | |[nade-trajectories](nade-trajectories)|Map overview with grenade trajectories| 11 | |[voice-capture](voice-capture)|Capture voice data from players| 12 | |[entities](entities)|Using unhandled data from entities (`Parser.ServerClasses()`)| 13 | |[net-messages](net-messages)|Parsing and handling custom net-messages| 14 | |[encrypted-net-messages](encrypted-net-messages)|Parsing and handling encrypted net-messages (e.g. text chat in MM demos)| 15 | |[print-events](print-events)|Printing kills, scores & chat messages| 16 | |[mocking](mocking)|Using the `fake` package to write unit tests for your code| 17 | |[web-assembly](web-assembly)|Using the library from JavaScript (browser/node) with [WebAssembly](https://webassembly.org/)| 18 | |[more examples](https://github.com/markus-wa/demoinfocs-golang/wiki/Additional-Examples-(Gists))|A collection of unpolished GitHub Gists based on past requests| 19 | -------------------------------------------------------------------------------- /examples/_assets/metadata/ar_baggage.txt: -------------------------------------------------------------------------------- 1 | "ar_baggage" 2 | { 3 | "material" "overviews/ar_baggage" 4 | "pos_x" "-1316" 5 | "pos_y" "1288" 6 | "scale" "2.539062" 7 | "rotate" "1" 8 | "zoom" "1.300000" 9 | 10 | "verticalsections" 11 | { 12 | "default" // use the primary radar image 13 | { 14 | "AltitudeMax" "10000" 15 | "AltitudeMin" "-5" 16 | } 17 | "lower" // i.e. de_nuke_lower_radar.dds 18 | { 19 | "AltitudeMax" "-5" 20 | "AltitudeMin" "-10000" 21 | } 22 | } 23 | 24 | 25 | "CTSpawn_x" "0.510000" 26 | "CTSpawn_y" "0.820000" 27 | "TSpawn_x" "0.510000" 28 | "TSpawn_y" "0.290000" 29 | } 30 | -------------------------------------------------------------------------------- /examples/_assets/metadata/ar_shoots.txt: -------------------------------------------------------------------------------- 1 | "ar_shoots" 2 | { 3 | "material" "overviews/ar_shoots" 4 | "pos_x" "-1368" 5 | "pos_y" "1952" 6 | "scale" "2.687500" 7 | "CTSpawn_x" "0.520000" 8 | "CTSpawn_y" "0.270000" 9 | "TSpawn_x" "0.520000" 10 | "TSpawn_y" "0.710000" 11 | } 12 | -------------------------------------------------------------------------------- /examples/_assets/metadata/cs_italy.txt: -------------------------------------------------------------------------------- 1 | // HLTV overview description file for cs_italy.bsp 2 | 3 | "cs_italy" 4 | { 5 | "material" "overviews/cs_italy" // texture file 6 | "pos_x" "-2647" // upper left world coordinate 7 | "pos_y" "2592" 8 | "scale" "4.6" 9 | "rotate" "1" 10 | "zoom" "1.5" 11 | 12 | // loading screen icons and positions 13 | "CTSpawn_x" "0.41" 14 | "CTSpawn_y" "0.91" 15 | "TSpawn_x" "0.6" 16 | "TSpawn_y" "0.1" 17 | 18 | "Hostage1_x" "0.43" 19 | "Hostage1_y" "0.29" 20 | "Hostage2_x" "0.48" 21 | "Hostage2_y" "0.24" 22 | "Hostage3_x" "0.64" 23 | "Hostage3_y" "0.03" 24 | "Hostage4_x" "0.72" 25 | "Hostage4_y" "0.05" 26 | // "Hostage5_x" "0.8" 27 | // "Hostage5_y" "0.3" 28 | // "Hostage6_x" "0.6" 29 | // "Hostage6_y" "0.9" 30 | } 31 | 32 | -------------------------------------------------------------------------------- /examples/_assets/metadata/cs_office.txt: -------------------------------------------------------------------------------- 1 | // HLTV overview description file for cs_office.bsp 2 | 3 | "cs_office" 4 | { 5 | "material" "overviews/cs_office" // texture file 6 | "pos_x" "-1838" // upper left world coordinate 7 | "pos_y" "1858" 8 | "scale" "4.1" 9 | 10 | // loading screen icons and positions 11 | "CTSpawn_x" "0.16" 12 | "CTSpawn_y" "0.89" 13 | "TSpawn_x" "0.78" 14 | "TSpawn_y" "0.30" 15 | 16 | "Hostage1_x" "0.84" 17 | "Hostage1_y" "0.27" 18 | "Hostage2_x" "0.84" 19 | "Hostage2_y" "0.48" 20 | "Hostage3_x" "0.91" 21 | "Hostage3_y" "0.48" 22 | "Hostage4_x" "0.77" 23 | "Hostage4_y" "0.48" 24 | "Hostage5_x" "0.77" 25 | "Hostage5_y" "0.55" 26 | // "Hostage6_x" "0.6" 27 | // "Hostage6_y" "0.9" 28 | } 29 | 30 | -------------------------------------------------------------------------------- /examples/_assets/metadata/de_ancient.txt: -------------------------------------------------------------------------------- 1 | // HLTV overview description file for de_ancient.bsp 2 | 3 | "de_ancient" 4 | { 5 | "material" "overviews/de_ancient" // texture file 6 | "pos_x" "-2953" // upper left world coordinate 7 | "pos_y" "2164" 8 | "scale" "5" 9 | "rotate" "0" 10 | "zoom" "0" 11 | 12 | // loading screen icons and positions 13 | "CTSpawn_x" "0.51" 14 | "CTSpawn_y" "0.17" 15 | "TSpawn_x" "0.485" 16 | "TSpawn_y" "0.87" 17 | 18 | "bombA_x" "0.31" 19 | "bombA_y" "0.25" 20 | "bombB_x" "0.80" 21 | "bombB_y" "0.40" 22 | } 23 | 24 | -------------------------------------------------------------------------------- /examples/_assets/metadata/de_anubis.txt: -------------------------------------------------------------------------------- 1 | // TAVR - AUTO RADAR. v 2.5.0a 2 | "de_anubis" 3 | { 4 | "CTSpawn_x" "0.610000" 5 | "CTSpawn_y" "0.220000" 6 | "TSpawn_x" "0.580000" 7 | "TSpawn_y" "0.930000" 8 | "material" "overviews/de_anubis" 9 | "pos_x" "-2796.000000" 10 | "pos_y" "3328.000000" 11 | "scale" "5.220000" 12 | } 13 | -------------------------------------------------------------------------------- /examples/_assets/metadata/de_dust.txt: -------------------------------------------------------------------------------- 1 | // HLTV overview description file for de_dust.bsp 2 | 3 | "de_dust" 4 | { 5 | "material" "overviews/de_dust" // texture file 6 | "pos_x" "-2850" // upper left world coordinate 7 | "pos_y" "4073" 8 | "scale" "6" 9 | "rotate" "1" 10 | "zoom" "1.3" 11 | 12 | // loading screen icons and positions 13 | "CTSpawn_x" "0.47" 14 | "CTSpawn_y" "0.92" 15 | "TSpawn_x" "0.47" 16 | "TSpawn_y" "0.10" 17 | 18 | "bombA_x" "0.80" 19 | "bombA_y" "0.55" 20 | "bombB_x" "0.53" 21 | "bombB_y" "0.74" 22 | } 23 | 24 | -------------------------------------------------------------------------------- /examples/_assets/metadata/de_dust2.txt: -------------------------------------------------------------------------------- 1 | // HLTV overview description file for de_dust2_v2.bsp 2 | 3 | "de_dust2" 4 | { 5 | "material" "overviews/de_dust2_v2" // texture file 6 | "pos_x" "-2476" // upper left world coordinate 7 | "pos_y" "3239" 8 | "scale" "4.4" 9 | "rotate" "1" 10 | "zoom" "1.1" 11 | 12 | "inset_left" "0.0" 13 | "inset_top" "0.0" 14 | "inset_right" "0.0" 15 | "inset_bottom" "0.0" 16 | 17 | // loading screen icons and positions 18 | "CTSpawn_x" "0.62" 19 | "CTSpawn_y" "0.21" 20 | "TSpawn_x" "0.39" 21 | "TSpawn_y" "0.91" 22 | 23 | "bombA_x" "0.80" 24 | "bombA_y" "0.16" 25 | "bombB_x" "0.21" 26 | "bombB_y" "0.12" 27 | } 28 | -------------------------------------------------------------------------------- /examples/_assets/metadata/de_inferno.txt: -------------------------------------------------------------------------------- 1 | // HLTV overview description file for de_inferno.bsp 2 | 3 | "de_inferno" 4 | { 5 | "material" "overviews/de_inferno" // texture file 6 | "pos_x" "-2087" // upper left world coordinate 7 | "pos_y" "3870" 8 | "scale" "4.9" 9 | 10 | // loading screen icons and positions 11 | "CTSpawn_x" "0.9" 12 | "CTSpawn_y" "0.35" 13 | "TSpawn_x" "0.1" 14 | "TSpawn_y" "0.67" 15 | 16 | "bombA_x" "0.81" 17 | "bombA_y" "0.69" 18 | "bombB_x" "0.49" 19 | "bombB_y" "0.22" 20 | } 21 | -------------------------------------------------------------------------------- /examples/_assets/metadata/de_inferno_s2.txt: -------------------------------------------------------------------------------- 1 | // HLTV overview description file for de_inferno.bsp 2 | 3 | "de_inferno" 4 | { 5 | "material" "overviews/de_inferno" // texture file 6 | "pos_x" "-2087" // upper left world coordinate 7 | "pos_y" "3870" 8 | "scale" "4.9" 9 | 10 | // loading screen icons and positions 11 | "CTSpawn_x" "0.9" 12 | "CTSpawn_y" "0.35" 13 | "TSpawn_x" "0.1" 14 | "TSpawn_y" "0.67" 15 | 16 | "bombA_x" "0.81" 17 | "bombA_y" "0.69" 18 | "bombB_x" "0.49" 19 | "bombB_y" "0.22" 20 | } 21 | -------------------------------------------------------------------------------- /examples/_assets/metadata/de_mirage.txt: -------------------------------------------------------------------------------- 1 | "de_mirage" 2 | { 3 | "material" "overviews/de_mirage" // texture file 4 | "pos_x" "-3230" // X coordinate, 5 | "pos_y" "1713" // Y coordinate, 6 | "scale" "5.00" // and used scale used when taking the screenshot 7 | "rotate" "0" // map was rotated by 90 degress in image editor 8 | "zoom" "0" // optimal zoom factor if map is shown in full size 9 | 10 | // loading screen icons and positions 11 | "CTSpawn_x" "0.28" 12 | "CTSpawn_y" "0.70" 13 | "TSpawn_x" "0.87" 14 | "TSpawn_y" "0.36" 15 | 16 | "bombA_x" "0.54" 17 | "bombA_y" "0.76" 18 | "bombB_x" "0.23" 19 | "bombB_y" "0.28" 20 | 21 | "inset_left" "0.135" 22 | "inset_top" "0.08" 23 | "inset_right" "0.105" 24 | "inset_bottom" "0.08" 25 | } -------------------------------------------------------------------------------- /examples/_assets/metadata/de_nuke.txt: -------------------------------------------------------------------------------- 1 | // HLTV overview description file for de_nuke.bsp 2 | 3 | "de_nuke" 4 | { 5 | "material" "overviews/de_nuke" // texture file 6 | "pos_x" "-3453" // upper left world coordinate 7 | "pos_y" "2887" 8 | "scale" "7" 9 | 10 | "verticalsections" 11 | { 12 | "default" // use the primary radar image 13 | { 14 | "AltitudeMax" "10000" 15 | "AltitudeMin" "-495" 16 | } 17 | "lower" // i.e. de_nuke_lower_radar.dds 18 | { 19 | "AltitudeMax" "-495" 20 | "AltitudeMin" "-10000" 21 | } 22 | } 23 | 24 | // loading screen icons and positions 25 | "CTSpawn_x" "0.82" 26 | "CTSpawn_y" "0.45" 27 | "TSpawn_x" "0.19" 28 | "TSpawn_y" "0.54" 29 | 30 | "bombA_x" "0.58" 31 | "bombA_y" "0.48" 32 | "bombB_x" "0.58" 33 | "bombB_y" "0.58" 34 | 35 | "inset_left" "0.33" 36 | "inset_top" "0.2" 37 | "inset_right" "0.2" 38 | "inset_bottom" "0.2" 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /examples/_assets/metadata/de_overpass.txt: -------------------------------------------------------------------------------- 1 | // HLTV overview description file for de_overpass.bsp 2 | 3 | "de_overpass" 4 | { 5 | "material" "overviews/de_overpass" // texture file 6 | "pos_x" "-4831" // upper left world coordinate 7 | "pos_y" "1781" 8 | "scale" "5.2" 9 | "rotate" "0" 10 | "zoom" "0" 11 | 12 | // loading screen icons and positions 13 | "CTSpawn_x" "0.49" 14 | "CTSpawn_y" "0.2" 15 | "TSpawn_x" "0.66" 16 | "TSpawn_y" "0.93" 17 | 18 | "bombA_x" "0.55" 19 | "bombA_y" "0.23" 20 | "bombB_x" "0.7" 21 | "bombB_y" "0.31" 22 | } -------------------------------------------------------------------------------- /examples/_assets/metadata/de_overpass_2v2.txt: -------------------------------------------------------------------------------- 1 | // HLTV overview description file for de_overpass.bsp 2 | 3 | "de_overpass" 4 | { 5 | "material" "overviews/de_overpass" // texture file 6 | "pos_x" "-4831" // upper left world coordinate 7 | "pos_y" "1781" 8 | "scale" "5.2" 9 | "rotate" "0" 10 | "zoom" "0" 11 | 12 | // loading screen icons and positions 13 | "CTSpawn_x" "0.49" 14 | "CTSpawn_y" "0.2" 15 | "TSpawn_x" "0.66" 16 | "TSpawn_y" "0.93" 17 | 18 | "bombA_x" "0.55" 19 | "bombA_y" "0.23" 20 | "bombB_x" "0.7" 21 | "bombB_y" "0.31" 22 | } -------------------------------------------------------------------------------- /examples/_assets/metadata/de_train.txt: -------------------------------------------------------------------------------- 1 | "de_train" 2 | { 3 | "material" "overviews/de_train" 4 | "pos_x" "-2308" 5 | "pos_y" "2078" 6 | "scale" "4.082077" 7 | "verticalsections" 8 | { 9 | "default" // use the primary radar image 10 | { 11 | "AltitudeMax" "20000" 12 | "AltitudeMin" "-50" 13 | } 14 | "lower" // i.e. de_nuke_lower_radar.dds 15 | { 16 | "AltitudeMax" "-50" 17 | "AltitudeMin" "-5000" 18 | } 19 | } 20 | "CTSpawn_x" "0.860000" 21 | "CTSpawn_y" "0.770000" 22 | "TSpawn_x" "0.120000" 23 | "TSpawn_y" "0.250000" 24 | "bombA_x" "0.630000" 25 | "bombA_y" "0.490000" 26 | "bombB_x" "0.520000" 27 | "bombB_y" "0.760000" 28 | } 29 | -------------------------------------------------------------------------------- /examples/_assets/metadata/de_vertigo.txt: -------------------------------------------------------------------------------- 1 | // HLTV overview description file for de_vertigo.bsp 2 | 3 | "de_vertigo" 4 | { 5 | "material" "overviews/de_vertigo_radar" // texture file 6 | "pos_x" "-3168" // upper left world coordinate 7 | "pos_y" "1762" 8 | "scale" "4.0" 9 | 10 | "verticalsections" 11 | { 12 | "default" // use the primary radar image 13 | { 14 | "AltitudeMax" "20000" 15 | "AltitudeMin" "11700" 16 | } 17 | "lower" // i.e. de_nuke_lower_radar.dds 18 | { 19 | "AltitudeMax" "11700" 20 | "AltitudeMin" "-10000" 21 | } 22 | } 23 | 24 | // loading screen icons and positions 25 | "CTSpawn_x" "0.54" 26 | "CTSpawn_y" "0.25" 27 | "TSpawn_x" "0.20" 28 | "TSpawn_y" "0.75" 29 | 30 | "bombA_x" "0.705" 31 | "bombA_y" "0.585" 32 | "bombB_x" "0.222" 33 | "bombB_y" "0.223" 34 | 35 | "inset_left" "0.1" 36 | "inset_top" "0.1" 37 | "inset_right" "0.2" 38 | "inset_bottom" "0.15" 39 | 40 | } 41 | -------------------------------------------------------------------------------- /examples/_assets/radar/ar_baggage_lower_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/ar_baggage_lower_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/ar_baggage_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/ar_baggage_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/ar_shoots_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/ar_shoots_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/cs_italy_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/cs_italy_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/cs_office_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/cs_office_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_ancient_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_ancient_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_anubis_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_anubis_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_dust2_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_dust2_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_inferno_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_inferno_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_mirage_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_mirage_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_nuke_lower_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_nuke_lower_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_nuke_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_nuke_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_overpass_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_overpass_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_train_lower_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_train_lower_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_train_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_train_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_vertigo_lower_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_vertigo_lower_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/de_vertigo_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/de_vertigo_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/default_png.vtex_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/default_png.vtex_c.png -------------------------------------------------------------------------------- /examples/_assets/radar/workshop_preview_radar_psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/workshop_preview_radar_psd.png -------------------------------------------------------------------------------- /examples/_assets/radar/workshop_preview_radar_tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/_assets/radar/workshop_preview_radar_tga.png -------------------------------------------------------------------------------- /examples/common.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | // DemoPathFromArgs returns the value of the -demo command line flag. 11 | // Panics if an error occurs. 12 | func DemoPathFromArgs() string { 13 | fl := new(flag.FlagSet) 14 | 15 | demPathPtr := fl.String("demo", "", "Demo file `path`") 16 | 17 | err := fl.Parse(os.Args[1:]) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | demPath := *demPathPtr 23 | 24 | return demPath 25 | } 26 | 27 | func checkError(err error) { 28 | if err != nil { 29 | panic(err) 30 | } 31 | } 32 | 33 | // RedirectStdout redirects standard output to dev null. 34 | // Panics if an error occurs. 35 | func RedirectStdout(f func()) { 36 | // Redirect stdout, the resulting image is written to this 37 | old := os.Stdout 38 | 39 | r, w, err := os.Pipe() 40 | checkError(err) 41 | 42 | os.Stdout = w 43 | 44 | // Discard the output in a separate goroutine so writing to stdout can't block indefinitely 45 | go func() { 46 | for err := error(nil); err == nil; _, err = io.Copy(ioutil.Discard, r) { 47 | } 48 | }() 49 | 50 | f() 51 | 52 | os.Stdout = old 53 | } 54 | -------------------------------------------------------------------------------- /examples/encrypted-net-messages/README.md: -------------------------------------------------------------------------------- 1 | # Parsing & handling encrypted net-messages 2 | 3 | See also [net-messages](../net-messages) for regular net-messages. 4 | 5 | This example shows how to have the parser deal with encrypted net-messages. 6 | 7 | For Valve MM games, the decryption key can be obtained from `.dem.info` files using `MatchInfoDecryptionKey()`. 8 | The key then needs to be passed to `ParserConfig.NetMessageDecryptionKey`. 9 | 10 | ## Run 11 | 12 | go run enc_net_nsg.go -demo path/to/demo.dem -info path/to/demo.dem.info 13 | 14 | This prints chat messages from the passed demo (assuming the `.dem.info` file contains the correct decryption key). 15 | -------------------------------------------------------------------------------- /examples/encrypted-net-messages/enc_net_msg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | 9 | dem "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 10 | "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 11 | ) 12 | 13 | func checkErr(err error) { 14 | if err != nil { 15 | panic(err) 16 | } 17 | } 18 | 19 | func main() { 20 | fl := new(flag.FlagSet) 21 | 22 | demPathPtr := fl.String("demo", "", "Demo file `path`") 23 | infoPathPtr := fl.String("info", "", "Info file `path`") 24 | 25 | err := fl.Parse(os.Args[1:]) 26 | checkErr(err) 27 | 28 | demPath := *demPathPtr 29 | infoPath := *infoPathPtr 30 | 31 | infoF, err := os.Open(infoPath) 32 | checkErr(err) 33 | 34 | b, err := ioutil.ReadAll(infoF) 35 | checkErr(err) 36 | 37 | k, err := dem.MatchInfoDecryptionKey(b) 38 | checkErr(err) 39 | 40 | f, err := os.Open(demPath) 41 | checkErr(err) 42 | 43 | defer f.Close() 44 | 45 | cfg := dem.DefaultParserConfig 46 | cfg.NetMessageDecryptionKey = k 47 | 48 | p := dem.NewParserWithConfig(f, cfg) 49 | 50 | p.RegisterEventHandler(func(warn events.ParserWarn) { 51 | log.Println("WARNING:", warn.Message) 52 | }) 53 | 54 | p.RegisterEventHandler(func(message events.ChatMessage) { 55 | log.Println(message) 56 | }) 57 | 58 | err = p.ParseToEnd() 59 | checkErr(err) 60 | } 61 | -------------------------------------------------------------------------------- /examples/encrypted-net-messages/enc_net_msg_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | // Just make sure the example runs 9 | func TestEncryptedNetMessages(t *testing.T) { 10 | if testing.Short() { 11 | t.Skip("skipping test") 12 | } 13 | 14 | os.Args = []string{"cmd", "-demo", "../../test/cs-demos/match730_003528806449641685104_1453182610_271.dem", "-info", "../../test/cs-demos/match730_003528806449641685104_1453182610_271.dem.info"} 15 | 16 | main() 17 | } 18 | 19 | // Make sure it doesn't error / crash 20 | func TestEncryptedNetMessages_BadKey(t *testing.T) { 21 | if testing.Short() { 22 | t.Skip("skipping test") 23 | } 24 | 25 | os.Args = []string{"cmd", "-demo", "../../test/cs-demos/match730_003528806449641685104_1453182610_271.dem", "-info", "../../test/cs-demos/match730_003449478367177343081_1946274414_112.dem.info"} 26 | 27 | main() 28 | } 29 | -------------------------------------------------------------------------------- /examples/entities/entities.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | _ "image/jpeg" 6 | "os" 7 | 8 | ex "github.com/markus-wa/demoinfocs-golang/v4/examples" 9 | demoinfocs "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 10 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 11 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 12 | ) 13 | 14 | // Run like this: go run entities.go -demo /path/to/demo.dem 15 | func main() { 16 | f, err := os.Open(ex.DemoPathFromArgs()) 17 | checkError(err) 18 | defer f.Close() 19 | 20 | p := demoinfocs.NewParser(f) 21 | defer p.Close() 22 | 23 | p.RegisterEventHandler(func(events.DataTablesParsed) { 24 | p.ServerClasses().FindByName("CWeaponAWP").OnEntityCreated(func(ent st.Entity) { 25 | ent.Property("m_hOwnerEntity").OnUpdate(func(val st.PropertyValue) { 26 | x := p.GameState().Participants().FindByHandle64(val.S2UInt64()) 27 | if x != nil { 28 | var prev string 29 | prevHandle := ent.Property("m_hPrevOwner").Value().S2UInt64() 30 | prevPlayer := p.GameState().Participants().FindByHandle64(prevHandle) 31 | if prevPlayer != nil { 32 | if prevHandle != val.S2UInt64() { 33 | prev = prevPlayer.Name + "'s" 34 | } else { 35 | prev = "his dropped" 36 | } 37 | } else { 38 | prev = "a brand new" 39 | } 40 | fmt.Printf("%s picked up %s AWP (#%d)\n", x.Name, prev, ent.ID()) 41 | } 42 | }) 43 | }) 44 | }) 45 | 46 | err = p.ParseToEnd() 47 | checkError(err) 48 | } 49 | 50 | func checkError(err error) { 51 | if err != nil { 52 | panic(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/entities/entities_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | // Just make sure the example runs 9 | func TestEntities(t *testing.T) { 10 | if testing.Short() { 11 | t.Skip("skipping test") 12 | } 13 | 14 | os.Args = []string{"cmd", "-demo", "../../test/cs-demos/s2/s2.dem"} 15 | 16 | main() 17 | } 18 | -------------------------------------------------------------------------------- /examples/heatmap/README.md: -------------------------------------------------------------------------------- 1 | # Creating a heatmap 2 | 3 | This example shows how to create a heatmap from positions where players fired their weapons from. 4 | 5 | :information_source: Uses radar images from `../_assets/radar` directory. 6 | 7 | See `heatmap.go` for the source code. 8 | 9 | ## Running the example 10 | 11 | `go run heatmap.go -demo /path/to/demo > out.jpg` 12 | 13 | This will create a JPEG of a radar overview with dots on all the locations where shots were fired from. 14 | 15 | ![Resulting heatmap](https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/master/examples/heatmap/heatmap.jpg) 16 | -------------------------------------------------------------------------------- /examples/heatmap/heatmap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | "image/jpeg" 7 | "os" 8 | 9 | r2 "github.com/golang/geo/r2" 10 | heatmap "github.com/markus-wa/go-heatmap/v2" 11 | schemes "github.com/markus-wa/go-heatmap/v2/schemes" 12 | 13 | ex "github.com/markus-wa/demoinfocs-golang/v4/examples" 14 | demoinfocs "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 15 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 16 | msg "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2" 17 | ) 18 | 19 | const ( 20 | dotSize = 15 21 | opacity = 128 22 | jpegQuality = 90 23 | ) 24 | 25 | // Run like this: go run heatmap.go -demo /path/to/demo.dem > out.jpg 26 | func main() { 27 | // 28 | // Parsing 29 | // 30 | 31 | f, err := os.Open(ex.DemoPathFromArgs()) 32 | checkError(err) 33 | 34 | defer f.Close() 35 | 36 | p := demoinfocs.NewParser(f) 37 | defer p.Close() 38 | 39 | var ( 40 | mapMetadata ex.Map 41 | mapRadarImg image.Image 42 | ) 43 | 44 | p.RegisterNetMessageHandler(func(msg *msg.CSVCMsg_ServerInfo) { 45 | // Get metadata for the map that the game was played on for coordinate translations 46 | mapMetadata = ex.GetMapMetadata(msg.GetMapName()) 47 | 48 | // Load map overview image 49 | mapRadarImg = ex.GetMapRadar(msg.GetMapName()) 50 | }) 51 | 52 | // Register handler for WeaponFire, triggered every time a shot is fired 53 | var points []r2.Point 54 | 55 | p.RegisterEventHandler(func(e events.WeaponFire) { 56 | // Translate positions from in-game coordinates to radar overview image pixels 57 | x, y := mapMetadata.TranslateScale(e.Shooter.Position().X, e.Shooter.Position().Y) 58 | 59 | points = append(points, r2.Point{X: x, Y: y}) 60 | }) 61 | 62 | // Parse the whole demo 63 | err = p.ParseToEnd() 64 | checkError(err) 65 | 66 | // 67 | // Preparation of heatmap data 68 | // 69 | 70 | // Find bounding rectangle for points to get around the normalization done by the heatmap library 71 | r2Bounds := r2.RectFromPoints(points...) 72 | padding := float64(dotSize) / 2.0 // Calculating padding amount to avoid shrinkage by the heatmap library 73 | bounds := image.Rectangle{ 74 | Min: image.Point{X: int(r2Bounds.X.Lo - padding), Y: int(r2Bounds.Y.Lo - padding)}, 75 | Max: image.Point{X: int(r2Bounds.X.Hi + padding), Y: int(r2Bounds.Y.Hi + padding)}, 76 | } 77 | 78 | // Transform r2.Points into heatmap.DataPoints 79 | data := make([]heatmap.DataPoint, 0, len(points)) 80 | 81 | for _, p := range points[1:] { 82 | // Invert Y since go-heatmap expects data to be ordered from bottom to top 83 | data = append(data, heatmap.P(p.X, p.Y*-1)) 84 | } 85 | 86 | // 87 | // Drawing the image 88 | // 89 | 90 | // Create output canvas and use map overview image as base 91 | img := image.NewRGBA(mapRadarImg.Bounds()) 92 | draw.Draw(img, mapRadarImg.Bounds(), mapRadarImg, image.Point{}, draw.Over) 93 | 94 | // Generate and draw heatmap overlay on top of the overview 95 | imgHeatmap := heatmap.Heatmap(image.Rect(0, 0, bounds.Dx(), bounds.Dy()), data, dotSize, opacity, schemes.AlphaFire) 96 | draw.Draw(img, bounds, imgHeatmap, image.Point{}, draw.Over) 97 | 98 | // Write to stdout 99 | err = jpeg.Encode(os.Stdout, img, &jpeg.Options{Quality: jpegQuality}) 100 | checkError(err) 101 | } 102 | 103 | func checkError(err error) { 104 | if err != nil { 105 | panic(err) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /examples/heatmap/heatmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/heatmap/heatmap.jpg -------------------------------------------------------------------------------- /examples/heatmap/heatmap_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/markus-wa/demoinfocs-golang/v4/examples" 8 | ) 9 | 10 | // Just make sure the example runs 11 | func TestHeatmap(t *testing.T) { 12 | if testing.Short() { 13 | t.Skip("skipping test") 14 | } 15 | 16 | os.Args = []string{"cmd", "-demo", "../../test/cs-demos/s2/s2.dem"} 17 | 18 | examples.RedirectStdout(func() { 19 | main() 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /examples/map_metadata.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | "fmt" 7 | "image" 8 | 9 | "github.com/andygrunwald/vdf" 10 | ) 11 | 12 | // Map represents a CS:GO map. It contains information required to translate 13 | // in-game world coordinates to coordinates relative to (0, 0) on the provided map-overviews (radar images). 14 | type Map struct { 15 | PosX float64 `json:"pos_x,string"` 16 | PosY float64 `json:"pos_y,string"` 17 | Scale float64 `json:"scale,string"` 18 | } 19 | 20 | // Translate translates in-game world-relative coordinates to (0, 0) relative coordinates. 21 | func (m Map) Translate(x, y float64) (float64, float64) { 22 | return x - m.PosX, m.PosY - y 23 | } 24 | 25 | // TranslateScale translates and scales in-game world-relative coordinates to (0, 0) relative coordinates. 26 | // The outputs are pixel coordinates for the radar images found in the maps folder. 27 | func (m Map) TranslateScale(x, y float64) (float64, float64) { 28 | x, y = m.Translate(x, y) 29 | return x / m.Scale, y / m.Scale 30 | } 31 | 32 | //go:embed _assets/* 33 | var fs embed.FS 34 | 35 | // GetMapMetadata fetches metadata for a specific map version from 36 | // `https://radar-overviews.csgo.saiko.tech///info.json`. 37 | // Panics if any error occurs. 38 | func GetMapMetadata(name string) Map { 39 | f, err := fs.Open(fmt.Sprintf("_assets/metadata/%s.txt", name)) 40 | checkError(err) 41 | 42 | defer f.Close() 43 | 44 | m, err := vdf.NewParser(f).Parse() 45 | checkError(err) 46 | 47 | b, err := json.Marshal(m) 48 | checkError(err) 49 | 50 | var data map[string]Map 51 | 52 | err = json.Unmarshal(b, &data) 53 | checkError(err) 54 | 55 | mapInfo, ok := data[name] 56 | if !ok { 57 | panic(fmt.Sprintf("failed to get map info.json entry for %q", name)) 58 | } 59 | 60 | return mapInfo 61 | } 62 | 63 | // GetMapRadar fetches the radar image for a specific map version from 64 | // `https://radar-overviews.csgo.saiko.tech///radar.png`. 65 | // Panics if any error occurs. 66 | func GetMapRadar(name string) image.Image { 67 | f, err := fs.Open(fmt.Sprintf("_assets/radar/%s_radar_psd.png", name)) 68 | checkError(err) 69 | 70 | defer f.Close() 71 | 72 | img, _, err := image.Decode(f) 73 | checkError(err) 74 | 75 | return img 76 | } 77 | -------------------------------------------------------------------------------- /examples/map_metadata_test.go: -------------------------------------------------------------------------------- 1 | package examples_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/markus-wa/demoinfocs-golang/v4/examples" 9 | ) 10 | 11 | func TestGetMapMetadata(t *testing.T) { 12 | meta := examples.GetMapMetadata("de_dust2") 13 | 14 | assert.Equal(t, examples.Map{ 15 | PosX: -2476, 16 | PosY: 3239, 17 | Scale: 4.4, 18 | }, meta) 19 | } 20 | -------------------------------------------------------------------------------- /examples/mocking/README.md: -------------------------------------------------------------------------------- 1 | # Mocking the parser 2 | 3 | This example shows you how to use the provided [`fake` package](https://godoc.org/github.com/markus-wa/demoinfocs-golang/fake) to mock `demoinfocs.Parser` and other parts of the library. 4 | That way you will be able to write useful unit tests for your application. 5 | 6 | ## System under test 7 | 8 | First, let's have a look at the API of our code, the 'system under test': 9 | 10 | ```go 11 | import ( 12 | dem "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 13 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 14 | ) 15 | 16 | func collectKills(parser dem.Parser) (kills []events.Kill, err error) { 17 | ... 18 | } 19 | ``` 20 | 21 | We deliberately ignore the implementation so we don't make assumptions about the code since it might change in the future. 22 | 23 | As you can see `collectKills` takes an `Parser` as input and returns a slice of `events.Kill` and potentially an error. 24 | 25 | ## Positive test case 26 | 27 | Now let's have a look at our first test. Here we want to ensure that all kills are collected and that the order of the collected events is correct. 28 | 29 | ```go 30 | import ( 31 | "errors" 32 | "testing" 33 | 34 | assert "github.com/stretchr/testify/assert" 35 | 36 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 37 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 38 | fake "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/fake" 39 | ) 40 | 41 | func TestCollectKills(t *testing.T) { 42 | parser := fake.NewParser() 43 | kill1 := kill(common.EqAK47) 44 | kill2 := kill(common.EqScout) 45 | kill3 := kill(common.EqAUG) 46 | parser.MockEvents(kill1) // First frame 47 | parser.MockEvents(kill2, kill3) // Second frame 48 | 49 | parser.On("ParseToEnd").Return(nil) // Return no error 50 | 51 | actual, err := collectKills(parser) 52 | 53 | assert.Nil(t, err) 54 | expected := []events.Kill{kill1, kill2, kill3} 55 | assert.Equal(t, expected, actual) 56 | } 57 | 58 | func kill(wep common.EquipmentElement) events.Kill { 59 | eq := common.NewEquipment(wep) 60 | return events.Kill{ 61 | Killer: new(common.Player), 62 | Weapon: &eq, 63 | Victim: new(common.Player), 64 | } 65 | } 66 | ``` 67 | 68 | As you can see we first create a mocked parser with `fake.NewParser()`. 69 | 70 | Then we create two `Kill` events and add them into the `Parser.Events` map. 71 | The map index indicates at which frame the events will be sent out, in our case that's during the first and second frame, as we just iterate over the slice indices. 72 | 73 | Note: Especially when used together with `Parser.NetMessages` it can be useful to set these indices manually to ensure the events and net-messages are sent at the right moment. 74 | 75 | ## Negative test case 76 | 77 | Last but not least we want to do another test that ensures any error the parser encounters is returned to the callee and not suppressed by our function. 78 | 79 | ```go 80 | import ( 81 | "errors" 82 | "testing" 83 | 84 | assert "github.com/stretchr/testify/assert" 85 | 86 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 87 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 88 | fake "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/fake" 89 | ) 90 | 91 | func TestCollectKillsError(t *testing.T) { 92 | parser := fake.NewParser() 93 | expectedErr := errors.New("Test error") 94 | parser.On("ParseToEnd").Return(expectedErr) 95 | 96 | kills, actualErr := collectKills(parser) 97 | 98 | assert.Equal(t, expectedErr, actualErr) 99 | assert.Nil(t, kills) 100 | } 101 | ``` 102 | 103 | This test simply tells the mock to return the specified error and asserts that our function returns it to us. 104 | It also makes sure that kills is nil, and not an empty slice. 105 | -------------------------------------------------------------------------------- /examples/mocking/collect_kills.go: -------------------------------------------------------------------------------- 1 | package mocking 2 | 3 | import ( 4 | demoinfocs "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 5 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 6 | ) 7 | 8 | func collectKills(parser demoinfocs.Parser) (kills []events.Kill, err error) { 9 | parser.RegisterEventHandler(func(kill events.Kill) { 10 | kills = append(kills, kill) 11 | }) 12 | err = parser.ParseToEnd() 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /examples/mocking/mocking_test.go: -------------------------------------------------------------------------------- 1 | package mocking 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | assert "github.com/stretchr/testify/assert" 8 | 9 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 10 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 11 | fake "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/fake" 12 | ) 13 | 14 | func TestCollectKills(t *testing.T) { 15 | parser := fake.NewParser() 16 | kill1 := kill(common.EqAK47) 17 | kill2 := kill(common.EqScout) 18 | kill3 := kill(common.EqAUG) 19 | parser.MockEvents(kill1) // First frame 20 | parser.MockEvents(kill2, kill3) // Second frame 21 | 22 | parser.On("ParseToEnd").Return(nil) // Return no error 23 | 24 | actual, err := collectKills(parser) 25 | 26 | assert.Nil(t, err) 27 | expected := []events.Kill{kill1, kill2, kill3} 28 | assert.Equal(t, expected, actual) 29 | } 30 | 31 | func kill(wep common.EquipmentType) events.Kill { 32 | eq := common.NewEquipment(wep) 33 | return events.Kill{ 34 | Killer: new(common.Player), 35 | Weapon: eq, 36 | Victim: new(common.Player), 37 | } 38 | } 39 | 40 | func TestCollectKillsError(t *testing.T) { 41 | parser := fake.NewParser() 42 | expectedErr := errors.New("Test error") 43 | parser.On("ParseToEnd").Return(expectedErr) 44 | 45 | kills, actualErr := collectKills(parser) 46 | 47 | assert.Nil(t, kills) 48 | assert.Equal(t, expectedErr, actualErr) 49 | } 50 | -------------------------------------------------------------------------------- /examples/nade-trajectories/README.md: -------------------------------------------------------------------------------- 1 | # Nade trajectory overview 2 | 3 | This example shows how to create a overview of grenade trajectories of a match. 4 | 5 | :information_source: Uses radar images from `../_assets/radar` directory. 6 | 7 | ## Running the example 8 | 9 | `go run nade_trajectories.go -demo /path/to/demo > out.jpg` 10 | 11 | This will create a JPEG with grenade trajectories of the first five rounds. The reason it doesn't do more trajectories is because the image would look quite cluttered otherwise. 12 | 13 | ![Resulting map overview with trajectories](https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/master/examples/nade-trajectories/nade_trajectories.jpg) 14 | -------------------------------------------------------------------------------- /examples/nade-trajectories/nade_trajectories.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/examples/nade-trajectories/nade_trajectories.jpg -------------------------------------------------------------------------------- /examples/nade-trajectories/nade_trajectories_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | ex "github.com/markus-wa/demoinfocs-golang/v4/examples" 8 | ) 9 | 10 | // Just make sure the example runs 11 | func TestBouncyNades(t *testing.T) { 12 | if testing.Short() { 13 | t.Skip("skipping test") 14 | } 15 | 16 | os.Args = []string{"cmd", "-demo", "../../test/cs-demos/s2/s2.dem"} 17 | 18 | ex.RedirectStdout(func() { 19 | main() 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /examples/net-messages/netmessages.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "google.golang.org/protobuf/proto" 8 | 9 | ex "github.com/markus-wa/demoinfocs-golang/v4/examples" 10 | demoinfocs "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 11 | msg "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2" 12 | ) 13 | 14 | // Run like this: go run netmessages.go -demo /path/to/demo.dem > out.png 15 | func main() { 16 | f, err := os.Open(ex.DemoPathFromArgs()) 17 | checkError(err) 18 | defer f.Close() 19 | 20 | // Configure parsing of BSPDecal net-message 21 | cfg := demoinfocs.DefaultParserConfig 22 | cfg.AdditionalNetMessageCreators = map[int]demoinfocs.NetMessageCreator{ 23 | int(msg.SVC_Messages_svc_BSPDecal): func() proto.Message { 24 | return new(msg.CSVCMsg_BSPDecal) 25 | }, 26 | } 27 | 28 | p := demoinfocs.NewParserWithConfig(f, cfg) 29 | defer p.Close() 30 | 31 | // Register handler for BSPDecal messages 32 | p.RegisterNetMessageHandler(func(m *msg.CSVCMsg_BSPDecal) { 33 | fmt.Printf("bullet decal at x=%f y=%f z=%f\n", m.Pos.GetX(), m.Pos.GetY(), m.Pos.GetZ()) 34 | }) 35 | 36 | // Parse to end 37 | err = p.ParseToEnd() 38 | checkError(err) 39 | } 40 | 41 | func checkError(err error) { 42 | if err != nil { 43 | panic(err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/net-messages/netmessages_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | // Just make sure the example runs 9 | func TestNetMessages(t *testing.T) { 10 | if testing.Short() { 11 | t.Skip("skipping test") 12 | } 13 | 14 | os.Args = []string{"cmd", "-demo", "../../test/cs-demos/s2/s2.dem"} 15 | 16 | main() 17 | } 18 | -------------------------------------------------------------------------------- /examples/print-events/print_events.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | ex "github.com/markus-wa/demoinfocs-golang/v4/examples" 8 | demoinfocs "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 9 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 10 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 11 | ) 12 | 13 | // Run like this: go run print_events.go -demo /path/to/demo.dem 14 | func main() { 15 | f, err := os.Open(ex.DemoPathFromArgs()) 16 | checkError(err) 17 | 18 | defer f.Close() 19 | 20 | p := demoinfocs.NewParser(f) 21 | defer p.Close() 22 | 23 | // Parse header 24 | header, err := p.ParseHeader() 25 | checkError(err) 26 | fmt.Println("Map:", header.MapName) 27 | 28 | // Register handler on kill events 29 | p.RegisterEventHandler(func(e events.Kill) { 30 | var hs string 31 | if e.IsHeadshot { 32 | hs = " (HS)" 33 | } 34 | var wallBang string 35 | if e.PenetratedObjects > 0 { 36 | wallBang = " (WB)" 37 | } 38 | fmt.Printf("%s <%v%s%s> %s\n", formatPlayer(e.Killer), e.Weapon, hs, wallBang, formatPlayer(e.Victim)) 39 | }) 40 | 41 | // Register handler on round end to figure out who won 42 | p.RegisterEventHandler(func(e events.RoundEnd) { 43 | gs := p.GameState() 44 | switch e.Winner { 45 | case common.TeamTerrorists: 46 | // Winner's score + 1 because it hasn't actually been updated yet 47 | fmt.Printf("Round finished: winnerSide=T ; score=%d:%d\n", gs.TeamTerrorists().Score(), gs.TeamCounterTerrorists().Score()) 48 | case common.TeamCounterTerrorists: 49 | fmt.Printf("Round finished: winnerSide=CT ; score=%d:%d\n", gs.TeamCounterTerrorists().Score(), gs.TeamTerrorists().Score()) 50 | default: 51 | // Probably match medic or something similar 52 | fmt.Println("Round finished: No winner (tie)") 53 | } 54 | }) 55 | 56 | // Register handler for chat messages to print them 57 | p.RegisterEventHandler(func(e events.ChatMessage) { 58 | fmt.Printf("Chat - %s says: %s\n", formatPlayer(e.Sender), e.Text) 59 | }) 60 | 61 | p.RegisterEventHandler(func(e events.RankUpdate) { 62 | fmt.Printf("Rank Update: %d went from rank %d to rank %d, change: %f\n", e.SteamID32, e.RankOld, e.RankNew, e.RankChange) 63 | }) 64 | 65 | // Parse to end 66 | err = p.ParseToEnd() 67 | checkError(err) 68 | } 69 | 70 | func formatPlayer(p *common.Player) string { 71 | if p == nil { 72 | return "?" 73 | } 74 | 75 | switch p.Team { 76 | case common.TeamTerrorists: 77 | return "[T]" + p.Name 78 | case common.TeamCounterTerrorists: 79 | return "[CT]" + p.Name 80 | } 81 | 82 | return p.Name 83 | } 84 | 85 | func checkError(err error) { 86 | if err != nil { 87 | panic(err) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/print-events/print_events_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // Just make sure the example runs 11 | func TestScores(t *testing.T) { 12 | if testing.Short() { 13 | t.Skip("skipping test") 14 | } 15 | 16 | os.Args = []string{"cmd", "-demo", "../../test/cs-demos/s2/s2.dem"} 17 | 18 | main() 19 | } 20 | 21 | // formatPlayer should be null-safe - there are some demos with killer / victim = nil 22 | func TestFormatPlayerNil(t *testing.T) { 23 | assert.Equal(t, "?", formatPlayer(nil)) 24 | } 25 | -------------------------------------------------------------------------------- /examples/voice-capture/README.md: -------------------------------------------------------------------------------- 1 | # Capturing Voice Data 2 | 3 | ## CS2 4 | 5 | See https://github.com/DandrewsDev/CS2VoiceData 6 | 7 | ## CS:GO 8 | 9 | This example shows how to use the library to capture voice audio from demos. 10 | 11 | Since the build process is somewhat different from the library itself (due to usage of CGO), the example has it's own repository which you can find here: 12 | https://github.com/saiko-tech/csgo-demo-voice-capture-example 13 | -------------------------------------------------------------------------------- /examples/web-assembly/README.md: -------------------------------------------------------------------------------- 1 | # WebAssembly (WASM) 2 | 3 | This example shows how to use the library with [WebAssembly](https://webassembly.org/). 4 | 5 | Since the build process is somewhat different from the library itself, the example has it's own repository which you can find here: 6 | https://github.com/markus-wa/demoinfocs-wasm 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/markus-wa/demoinfocs-golang/v4 2 | 3 | require ( 4 | github.com/andygrunwald/vdf v1.1.0 5 | github.com/golang/geo v0.0.0-20230421003525-6adc56603217 6 | github.com/golang/snappy v0.0.4 7 | github.com/llgcode/draw2d v0.0.0-20230723155556-e595d7c7e75e 8 | github.com/markus-wa/go-heatmap/v2 v2.0.0 9 | github.com/markus-wa/go-unassert v0.1.3 10 | github.com/markus-wa/gobitread v0.2.4 11 | github.com/markus-wa/godispatch v1.4.1 12 | github.com/markus-wa/ice-cipher-go v0.0.0-20230901094113-348096939ba7 13 | github.com/markus-wa/quickhull-go/v2 v2.2.0 14 | github.com/oklog/ulid/v2 v2.1.0 15 | github.com/pkg/errors v0.9.1 16 | github.com/samber/lo v1.47.0 17 | github.com/stretchr/testify v1.10.0 18 | golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 19 | google.golang.org/protobuf v1.36.4 20 | ) 21 | 22 | require ( 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | github.com/stretchr/objx v0.5.2 // indirect 27 | golang.org/x/image v0.18.0 // indirect 28 | golang.org/x/text v0.16.0 // indirect 29 | gopkg.in/yaml.v3 v3.0.1 // indirect 30 | ) 31 | 32 | go 1.21 33 | -------------------------------------------------------------------------------- /internal/bitread/bitread.go: -------------------------------------------------------------------------------- 1 | // Package bitread provides a wrapper for github.com/markus-wa/gobitread with CS:GO demo parsing specific helpers. 2 | // 3 | // Intended for internal use only. 4 | package bitread 5 | 6 | import ( 7 | "io" 8 | "math" 9 | "sync" 10 | 11 | bitread "github.com/markus-wa/gobitread" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | const ( 16 | smallBuffer = 512 17 | largeBuffer = 1024 * 128 18 | maxVarInt32Bytes = 5 19 | maxVarintBytes = 10 20 | ) 21 | 22 | // BitReader wraps github.com/markus-wa/gobitread.BitReader and provides additional functionality specific to CS:GO demos. 23 | type BitReader struct { 24 | bitread.BitReader 25 | buffer *[]byte 26 | } 27 | 28 | // ReadString reads a variable length string. 29 | func (r *BitReader) ReadString() string { 30 | // Valve also uses this sooo 31 | const valveMaxStringLength = 4096 32 | return r.readStringLimited(valveMaxStringLength, false) 33 | } 34 | 35 | func (r *BitReader) readStringLimited(limit int, endOnNewLine bool) string { 36 | const minStringBufferLength = 256 37 | result := make([]byte, 0, minStringBufferLength) 38 | 39 | for i := 0; i < limit; i++ { 40 | b := r.ReadSingleByte() 41 | if b == 0 || (endOnNewLine && b == '\n') { 42 | break 43 | } 44 | 45 | result = append(result, b) 46 | } 47 | 48 | return string(result) 49 | } 50 | 51 | // ReadFloat reads a 32-bit float. Wraps ReadInt(). 52 | func (r *BitReader) ReadFloat() float32 { 53 | return math.Float32frombits(uint32(r.ReadInt(32))) 54 | } 55 | 56 | // ReadVarInt32 reads a variable size unsigned int (max 32-bit). 57 | func (r *BitReader) ReadVarInt32() uint32 { 58 | var ( 59 | res uint32 60 | b uint32 = 0x80 61 | ) 62 | 63 | for count := uint(0); b&0x80 != 0 && count != maxVarInt32Bytes; count++ { 64 | b = uint32(r.ReadSingleByte()) 65 | res |= (b & 0x7f) << (7 * count) 66 | } 67 | 68 | return res 69 | } 70 | 71 | // ReadVarInt64 reads a variable size unsigned int (max 64-bit). 72 | func (r *BitReader) ReadVarInt64() uint64 { 73 | var ( 74 | res uint64 75 | b uint64 = 0x80 76 | ) 77 | 78 | for count := uint(0); b&0x80 != 0 && count != maxVarintBytes; count++ { 79 | b = uint64(r.ReadSingleByte()) 80 | res |= (b & 0x7f) << (7 * count) 81 | } 82 | 83 | return res 84 | } 85 | 86 | // ReadSignedVarInt32 reads a variable size signed int (max 32-bit). 87 | func (r *BitReader) ReadSignedVarInt32() int32 { 88 | res := r.ReadVarInt32() 89 | return int32((res >> 1) ^ -(res & 1)) 90 | } 91 | 92 | // ReadSignedVarInt64 reads a variable size signed int (max 64-bit). 93 | func (r *BitReader) ReadSignedVarInt64() int64 { 94 | res := r.ReadVarInt64() 95 | return int64((res >> 1) ^ -(res & 1)) 96 | } 97 | 98 | // ReadUBitInt reads some kind of variable size uint. 99 | // Honestly, not quite sure how it works. 100 | func (r *BitReader) ReadUBitInt() uint { 101 | res := r.ReadInt(6) 102 | switch res & (16 | 32) { 103 | case 16: 104 | res = (res & 15) | (r.ReadInt(4) << 4) 105 | case 32: 106 | res = (res & 15) | (r.ReadInt(8) << 4) 107 | case 48: 108 | res = (res & 15) | (r.ReadInt(32-4) << 4) 109 | } 110 | 111 | return res 112 | } 113 | 114 | var bitReaderPool = sync.Pool{ 115 | New: func() any { 116 | return new(BitReader) 117 | }, 118 | } 119 | 120 | // Pool puts the BitReader into a pool for future use. 121 | // Pooling BitReaders improves performance by minimizing the amount newly allocated readers. 122 | func (r *BitReader) Pool() error { 123 | err := r.Close() 124 | if err != nil { 125 | return errors.Wrap(err, "failed to close BitReader before pooling") 126 | } 127 | 128 | if len(*r.buffer) == smallBuffer { 129 | smallBufferPool.Put(r.buffer) 130 | } 131 | 132 | r.buffer = nil 133 | 134 | bitReaderPool.Put(r) 135 | 136 | return nil 137 | } 138 | 139 | func newBitReader(underlying io.Reader, buffer *[]byte) *BitReader { 140 | br := bitReaderPool.Get().(*BitReader) 141 | br.buffer = buffer 142 | br.OpenWithBuffer(underlying, *buffer) 143 | 144 | return br 145 | } 146 | 147 | var smallBufferPool = sync.Pool{ 148 | New: func() any { 149 | b := make([]byte, smallBuffer) 150 | return &b 151 | }, 152 | } 153 | 154 | // NewSmallBitReader returns a BitReader with a small buffer, suitable for short streams. 155 | func NewSmallBitReader(underlying io.Reader) *BitReader { 156 | return newBitReader(underlying, smallBufferPool.Get().(*[]byte)) 157 | } 158 | 159 | // NewLargeBitReader returns a BitReader with a large buffer, suitable for long streams (main demo file). 160 | func NewLargeBitReader(underlying io.Reader) *BitReader { 161 | b := make([]byte, largeBuffer) 162 | return newBitReader(underlying, &b) 163 | } 164 | -------------------------------------------------------------------------------- /pkg/demoinfocs/common/entity_util.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 4 | 5 | func getInt(entity st.Entity, propName string) int { 6 | if entity == nil { 7 | return 0 8 | } 9 | 10 | return entity.PropertyValueMust(propName).Int() 11 | } 12 | 13 | func getUInt64(entity st.Entity, propName string) uint64 { 14 | if entity == nil { 15 | return 0 16 | } 17 | 18 | return entity.PropertyValueMust(propName).S2UInt64() 19 | } 20 | 21 | func getFloat(entity st.Entity, propName string) float32 { 22 | if entity == nil { 23 | return 0 24 | } 25 | 26 | return entity.PropertyValueMust(propName).Float() 27 | } 28 | 29 | func getString(entity st.Entity, propName string) string { 30 | if entity == nil { 31 | return "" 32 | } 33 | 34 | return entity.PropertyValueMust(propName).String() 35 | } 36 | 37 | func getBool(entity st.Entity, propName string) bool { 38 | if entity == nil { 39 | return false 40 | } 41 | 42 | return entity.PropertyValueMust(propName).BoolVal() 43 | } 44 | -------------------------------------------------------------------------------- /pkg/demoinfocs/common/entity_util_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetFloat_Nil(t *testing.T) { 10 | assert.Zero(t, getFloat(nil, "test")) 11 | } 12 | 13 | func TestGetInt_Nil(t *testing.T) { 14 | assert.Zero(t, getInt(nil, "test")) 15 | } 16 | 17 | func TestGetString_Nil(t *testing.T) { 18 | assert.Empty(t, getString(nil, "test")) 19 | } 20 | 21 | func TestGetBool_Nil(t *testing.T) { 22 | assert.Empty(t, getBool(nil, "test")) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/demoinfocs/common/gamerules.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // GamePhase represents a phase in CS:GO 4 | type GamePhase int 5 | 6 | // The following game rules have been found at https://github.com/pmrowla/hl2sdk-csgo/blob/master/game/shared/teamplayroundbased_gamerules.h#L37. 7 | // It seems that the naming used in the source engine is _not_ what is used in-game. 8 | // The original names of the enum fields are added as comments to each field. 9 | const ( 10 | // GamePhaseInit is the default value of the game phase 11 | GamePhaseInit GamePhase = 0 // enum name: Init 12 | 13 | // GamePhasePregame 14 | GamePhasePregame GamePhase = 1 // enum name: Pregame 15 | 16 | // GamePhaseStartGamePhase is set whenever a new game phase is started. 17 | // A game phase can be the normal match, i.e. first to 16 rounds, or an overtime match, 18 | // i.e. first to 4 rounds. It is set for _all_ overtimes played, i.e. for a match 19 | // with 3 overtimes, GamePhaseStartGamePhase is set 1 time for the normal 20 | // match and 1 time for each overtime played, for a total of 4 times. 21 | GamePhaseStartGamePhase GamePhase = 2 // enum name: StartGame 22 | 23 | // GamePhaseTeamSideSwitch is set whenever a team side switch happened, 24 | // i.e. both during normal game and overtime play. 25 | GamePhaseTeamSideSwitch GamePhase = 3 // enum name: PreRound 26 | 27 | // GamePhaseGameHalfEnded is set whenever a game phase has ended. 28 | // A game phase can be the normal match, i.e. first to 16 rounds, or an overtime match, 29 | // i.e. first to 4 rounds. It is set once for all overtimes played, i.e. for a match 30 | // with 3 overtimes, GamePhaseGameHalfEnded is set 1 time for the normal 31 | // match and 1 time for each overtime played, for a total of 4 times. 32 | GamePhaseGameHalfEnded GamePhase = 4 // enum name: TeamWin 33 | 34 | // GamePhaseGameEnded is set when the full game has ended. 35 | // This existence of this event is not reliable: it has been observed that a demo ends 36 | // before this event is set 37 | GamePhaseGameEnded GamePhase = 5 // enum name: Restart 38 | 39 | // GamePhaseStaleMate has not been observed so far 40 | GamePhaseStaleMate GamePhase = 6 // enum name: StaleMate 41 | 42 | // GamePhaseGameOver has not been observed so far 43 | GamePhaseGameOver GamePhase = 7 // enum name: GameOver 44 | ) 45 | 46 | // gamePhaseToString maps a GamePhase to a user friendly string 47 | var gamePhaseToString = map[GamePhase]string{ 48 | GamePhaseInit: "Init", 49 | GamePhasePregame: "Pregame", 50 | GamePhaseStartGamePhase: "Start game phase", 51 | GamePhaseTeamSideSwitch: "Team side switch", 52 | GamePhaseGameHalfEnded: "Game half ended", 53 | GamePhaseGameEnded: "Game ended", 54 | GamePhaseStaleMate: "StaleMate", 55 | GamePhaseGameOver: "GameOver", 56 | } 57 | 58 | func (r GamePhase) String() string { 59 | return gamePhaseToString[r] 60 | } 61 | -------------------------------------------------------------------------------- /pkg/demoinfocs/common/hostage.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/golang/geo/r3" 5 | 6 | "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/constants" 7 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 8 | ) 9 | 10 | // HostageState is the type for the various HostageStateXYZ constants. 11 | type HostageState byte 12 | 13 | // HostageState constants give information about hostages state. 14 | // e.g. being untied, picked up, rescued etc. 15 | const ( 16 | HostageStateIdle HostageState = 0 17 | HostageStateBeingUntied HostageState = 1 18 | HostageStateGettingPickedUp HostageState = 2 19 | HostageStateBeingCarried HostageState = 3 20 | HostageStateFollowingPlayer HostageState = 4 21 | HostageStateGettingDropped HostageState = 5 22 | HostageStateRescued HostageState = 6 23 | HostageStateDead HostageState = 7 24 | ) 25 | 26 | // Hostage represents a hostage. 27 | type Hostage struct { 28 | Entity st.Entity 29 | demoInfoProvider demoInfoProvider 30 | } 31 | 32 | // Position returns the current position of the hostage. 33 | func (hostage *Hostage) Position() r3.Vector { 34 | if hostage.Entity == nil { 35 | return r3.Vector{} 36 | } 37 | 38 | return hostage.Entity.Position() 39 | } 40 | 41 | // State returns the current hostage's state. 42 | // e.g. being untied, picked up, rescued etc. 43 | // See HostageState for all possible values. 44 | func (hostage *Hostage) State() HostageState { 45 | return HostageState(getInt(hostage.Entity, "m_nHostageState")) 46 | } 47 | 48 | // Health returns the hostage's health points. 49 | // ! On Valve MM matches hostages are invulnerable, it will always return 100 unless "mp_hostages_takedamage" is set to 1 50 | func (hostage *Hostage) Health() int { 51 | return getInt(hostage.Entity, "m_iHealth") 52 | } 53 | 54 | // Leader returns the possible player leading the hostage. 55 | // Returns nil if the hostage is not following a player. 56 | func (hostage *Hostage) Leader() *Player { 57 | if hostage.demoInfoProvider.IsSource2() { 58 | leaderHandle := getUInt64(hostage.Entity, "m_leader") 59 | if leaderHandle != constants.InvalidEntityHandleSource2 { 60 | return hostage.demoInfoProvider.FindPlayerByPawnHandle(leaderHandle) 61 | } 62 | 63 | return hostage.demoInfoProvider.FindPlayerByPawnHandle(getUInt64(hostage.Entity, "m_hHostageGrabber")) 64 | } 65 | 66 | return hostage.demoInfoProvider.FindPlayerByHandle(uint64(getInt(hostage.Entity, "m_leader"))) 67 | } 68 | 69 | // NewHostage creates a hostage. 70 | func NewHostage(demoInfoProvider demoInfoProvider, entity st.Entity) *Hostage { 71 | return &Hostage{ 72 | demoInfoProvider: demoInfoProvider, 73 | Entity: entity, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/demoinfocs/common/hostage_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/constants" 9 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 10 | ) 11 | 12 | func TestHostage_Leader(t *testing.T) { 13 | player := new(Player) 14 | player.EntityID = 10 15 | provider := demoInfoProviderMock{ 16 | playersByHandle: map[uint64]*Player{10: player}, 17 | } 18 | hostage := hostageWithProperty("m_leader", st.PropertyValue{IntVal: 10}, provider) 19 | 20 | assert.Equal(t, player, hostage.Leader()) 21 | } 22 | 23 | func TestHostage_LeaderWithInvalidHandleS2(t *testing.T) { 24 | player := new(Player) 25 | player.EntityID = 10 26 | provider := demoInfoProviderMock{ 27 | playersByHandle: map[uint64]*Player{10: player}, 28 | isSource2: true, 29 | } 30 | hostage := hostageWithProperties([]fakeProp{ 31 | { 32 | propName: "m_leader", 33 | value: st.PropertyValue{Any: uint64(constants.InvalidEntityHandleSource2)}, 34 | isNil: false, 35 | }, 36 | { 37 | propName: "m_hHostageGrabber", 38 | value: st.PropertyValue{Any: uint64(10)}, 39 | isNil: false, 40 | }, 41 | }, provider) 42 | 43 | assert.Equal(t, player, hostage.Leader()) 44 | } 45 | 46 | func TestHostage_State(t *testing.T) { 47 | hostage := hostageWithProperty("m_nHostageState", st.PropertyValue{IntVal: int(HostageStateFollowingPlayer)}, demoInfoProviderMock{}) 48 | 49 | assert.Equal(t, HostageStateFollowingPlayer, hostage.State()) 50 | } 51 | 52 | func TestHostage_Health(t *testing.T) { 53 | hostage := hostageWithProperty("m_iHealth", st.PropertyValue{IntVal: 40}, demoInfoProviderMock{}) 54 | 55 | assert.Equal(t, 40, hostage.Health()) 56 | } 57 | 58 | func hostageWithProperty(propName string, value st.PropertyValue, provider demoInfoProviderMock) *Hostage { 59 | return &Hostage{Entity: entityWithProperty(propName, value), demoInfoProvider: provider} 60 | } 61 | 62 | func hostageWithProperties(properties []fakeProp, provider demoInfoProviderMock) *Hostage { 63 | return &Hostage{Entity: entityWithProperties(properties), demoInfoProvider: provider} 64 | } 65 | -------------------------------------------------------------------------------- /pkg/demoinfocs/common/inferno_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/geo/r2" 7 | "github.com/golang/geo/r3" 8 | "github.com/stretchr/testify/assert" 9 | 10 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 11 | stfake "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables/fake" 12 | ) 13 | 14 | func TestInferno_UniqueID(t *testing.T) { 15 | entity := new(stfake.Entity) 16 | assert.NotEqual(t, NewInferno(nil, entity, nil).UniqueID(), NewInferno(nil, entity, nil).UniqueID(), "UniqueIDs of different infernos should be different") 17 | } 18 | 19 | func TestFires_Active(t *testing.T) { 20 | inf := Fires{ 21 | s: []Fire{ 22 | { 23 | IsBurning: false, 24 | Vector: r3.Vector{X: 1, Y: 2, Z: 3}, 25 | }, 26 | }, 27 | } 28 | 29 | assert.Empty(t, inf.Active().s, "Inferno should have no active fires") 30 | 31 | activeFires := []Fire{ 32 | { 33 | IsBurning: true, 34 | Vector: r3.Vector{X: 4, Y: 5, Z: 6}, 35 | }, 36 | { 37 | IsBurning: true, 38 | Vector: r3.Vector{X: 7, Y: 8, Z: 9}, 39 | }, 40 | } 41 | inf.s = append(inf.s, activeFires...) 42 | 43 | assert.Equal(t, activeFires, inf.Active().s, "Active inferno should contain active fires") 44 | } 45 | 46 | func TestInferno_ConvexHull2D(t *testing.T) { 47 | // Construct a Inferno that looks roughly like this. 48 | // D should be inside the 2D Convex Hull but a corner of the 3D Convex Hull 49 | // 50 | // C 51 | // / \ 52 | // / D \ 53 | // / \ 54 | // A - - - - - - - B 55 | // 56 | inf := Fires{ 57 | s: []Fire{ 58 | { 59 | Vector: r3.Vector{X: 1, Y: 2, Z: 3}, 60 | }, 61 | { 62 | Vector: r3.Vector{X: 4, Y: 7, Z: 6}, 63 | }, 64 | { 65 | Vector: r3.Vector{X: 7, Y: 2, Z: 9}, 66 | }, 67 | { 68 | Vector: r3.Vector{X: 4, Y: 4, Z: 12}, // This fire is inside the 2D hull 69 | }, 70 | }, 71 | } 72 | 73 | expectedHull := []r2.Point{ 74 | {X: 1, Y: 2}, 75 | {X: 4, Y: 7}, 76 | {X: 7, Y: 2}, 77 | } 78 | 79 | assert.ElementsMatch(t, expectedHull, inf.ConvexHull2D(), "ConvexHull2D should be as expected") 80 | 81 | // 3D-hull should be different 82 | assert.NotEqual(t, len(expectedHull), len(inf.ConvexHull3D().Vertices), "3D hull should contain the vertex 'D'") 83 | } 84 | 85 | // Just check that all fires are passed to quickhull.ConvexHull() 86 | func TestInferno_ConvexHull3D(t *testing.T) { 87 | inf := Fires{ 88 | s: []Fire{ 89 | { 90 | Vector: r3.Vector{X: 1, Y: 2, Z: 3}, 91 | }, 92 | { 93 | Vector: r3.Vector{X: 4, Y: 7, Z: 6}, 94 | }, 95 | { 96 | Vector: r3.Vector{X: 7, Y: 2, Z: 9}, 97 | }, 98 | { 99 | Vector: r3.Vector{X: 4, Y: 4, Z: 12}, 100 | }, 101 | }, 102 | } 103 | 104 | expectedHull := []r3.Vector{ 105 | {X: 1, Y: 2, Z: 3}, 106 | {X: 4, Y: 7, Z: 6}, 107 | {X: 7, Y: 2, Z: 9}, 108 | {X: 4, Y: 4, Z: 12}, 109 | } 110 | 111 | assert.ElementsMatch(t, expectedHull, inf.ConvexHull3D().Vertices, "ConvexHull3D should contain all fire locations") 112 | } 113 | 114 | func TestInferno_Thrower(t *testing.T) { 115 | entity := entityWithProperty("m_hOwnerEntity", st.PropertyValue{IntVal: 1}) 116 | 117 | player := new(Player) 118 | provider := demoInfoProviderMock{ 119 | playersByHandle: map[uint64]*Player{1: player}, 120 | } 121 | 122 | assert.Equal(t, player, NewInferno(provider, entity, nil).Thrower()) 123 | } 124 | 125 | func TestInferno_List(t *testing.T) { 126 | expected := []Fire{ 127 | { 128 | Vector: r3.Vector{X: 1, Y: 2, Z: 3}, 129 | }, 130 | { 131 | Vector: r3.Vector{X: 4, Y: 7, Z: 6}, 132 | }, 133 | { 134 | Vector: r3.Vector{X: 7, Y: 2, Z: 9}, 135 | }, 136 | { 137 | Vector: r3.Vector{X: 4, Y: 4, Z: 12}, 138 | }, 139 | } 140 | fires := Fires{ 141 | s: expected, 142 | } 143 | 144 | got := fires.List() 145 | assert.ElementsMatch(t, expected, got, "List() should return the fires contained in Fires") 146 | } 147 | -------------------------------------------------------------------------------- /pkg/demoinfocs/constants/constants.go: -------------------------------------------------------------------------------- 1 | // Package constants contains constants that are used internally across the demoinfocs library. 2 | package constants 3 | 4 | // Various constants that are used internally. 5 | const ( 6 | EntityHandleSerialNumberBits = 10 7 | 8 | MaxEdictBits = 11 9 | EntityHandleIndexMask = (1 << MaxEdictBits) - 1 10 | EntityHandleBits = MaxEdictBits + EntityHandleSerialNumberBits 11 | InvalidEntityHandle = (1 << EntityHandleBits) - 1 12 | 13 | MaxEdictBitsSource2 = 14 14 | EntityHandleIndexMaskSource2 = (1 << MaxEdictBitsSource2) - 1 15 | EntityHandleBitsSource2 = MaxEdictBitsSource2 + EntityHandleSerialNumberBits 16 | InvalidEntityHandleSource2 = (1 << EntityHandleBitsSource2) - 1 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/demoinfocs/datatables_test.go: -------------------------------------------------------------------------------- 1 | package demoinfocs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/geo/r3" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | 10 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 11 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 12 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 13 | stfake "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables/fake" 14 | ) 15 | 16 | type DevNullReader struct { 17 | } 18 | 19 | func (DevNullReader) Read(p []byte) (n int, err error) { 20 | return len(p), nil 21 | } 22 | 23 | func TestParser_BindNewPlayer_Issue98(t *testing.T) { 24 | p := newParser() 25 | 26 | p.rawPlayers = map[int]*common.PlayerInfo{ 27 | 0: { 28 | UserID: 1, 29 | Name: "Zim", 30 | GUID: "BOT", 31 | }, 32 | 1: { 33 | UserID: 2, 34 | Name: "The Suspect", 35 | GUID: "123", 36 | }, 37 | } 38 | 39 | bot := fakePlayerEntity(1) 40 | p.bindNewPlayerS1(bot) 41 | bot.Destroy() 42 | 43 | player := fakePlayerEntity(2) 44 | p.bindNewPlayerS1(player) 45 | 46 | assert.Len(t, p.GameState().Participants().Connected(), 1) 47 | } 48 | 49 | func TestParser_BindNewPlayer_Issue98_Reconnect(t *testing.T) { 50 | p := newParser() 51 | 52 | p.rawPlayers = map[int]*common.PlayerInfo{ 53 | 0: { 54 | UserID: 2, 55 | Name: "The Suspect", 56 | GUID: "123", 57 | XUID: 1, 58 | }, 59 | } 60 | 61 | player := fakePlayerEntity(1) 62 | p.bindNewPlayerS1(player) 63 | player.Destroy() 64 | 65 | p.RegisterEventHandler(func(events.PlayerConnect) { 66 | t.Error("expected no more PlayerConnect events but got one") 67 | }) 68 | p.bindNewPlayerS1(player) 69 | 70 | assert.Len(t, p.GameState().Participants().All(), 1) 71 | } 72 | 73 | func TestParser_BindNewPlayer_PlayerSpotted_Under32(t *testing.T) { 74 | testPlayerSpotted(t, "m_bSpottedByMask.000") 75 | } 76 | 77 | func TestParser_BindNewPlayer_PlayerSpotted_Over32(t *testing.T) { 78 | testPlayerSpotted(t, "m_bSpottedByMask.001") 79 | } 80 | 81 | func testPlayerSpotted(t *testing.T, propName string) { 82 | p := newParser() 83 | 84 | p.rawPlayers = map[int]*common.PlayerInfo{ 85 | 0: { 86 | UserID: 2, 87 | Name: "Spotter", 88 | GUID: "123", 89 | XUID: 1, 90 | }, 91 | } 92 | 93 | // TODO: Player interface so we don't have to mock all this 94 | spotted := new(stfake.Entity) 95 | spottedByProp0 := new(stfake.Property) 96 | 97 | var spottedByUpdateHandler st.PropertyUpdateHandler 98 | spottedByProp0.On("OnUpdate", mock.Anything).Run(func(args mock.Arguments) { 99 | spottedByUpdateHandler = args.Get(0).(st.PropertyUpdateHandler) 100 | }) 101 | 102 | spotted.On("Property", propName).Return(spottedByProp0) 103 | configurePlayerEntityMock(1, spotted) 104 | p.bindNewPlayerS1(spotted) 105 | 106 | var actual events.PlayerSpottersChanged 107 | p.RegisterEventHandler(func(e events.PlayerSpottersChanged) { 108 | actual = e 109 | }) 110 | 111 | spottedByUpdateHandler(st.PropertyValue{IntVal: 1}) 112 | 113 | expected := events.PlayerSpottersChanged{ 114 | Spotted: p.gameState.playersByEntityID[1], 115 | } 116 | assert.NotNil(t, expected.Spotted) 117 | assert.Equal(t, expected, actual) 118 | } 119 | 120 | func newParser() *parser { 121 | p := NewParser(new(DevNullReader)).(*parser) 122 | p.header = &common.DemoHeader{} 123 | 124 | return p 125 | } 126 | 127 | func fakePlayerEntity(id int) *stfake.Entity { 128 | entity := new(stfake.Entity) 129 | configurePlayerEntityMock(id, entity) 130 | 131 | return entity 132 | } 133 | 134 | func TestParser_GetClosestBombsiteFromPosition(t *testing.T) { 135 | p := newParser() 136 | p.bombsiteA = bombsite{ 137 | center: r3.Vector{X: 2, Y: 3, Z: 1}, 138 | } 139 | p.bombsiteB = bombsite{ 140 | center: r3.Vector{X: 4, Y: 5, Z: 7}, 141 | } 142 | 143 | site := p.getClosestBombsiteFromPosition(r3.Vector{X: -2, Y: 2, Z: 2}) 144 | 145 | assert.Equal(t, events.BombsiteA, site) 146 | 147 | site = p.getClosestBombsiteFromPosition(r3.Vector{X: 3, Y: 6, Z: 5}) 148 | 149 | assert.Equal(t, events.BombsiteB, site) 150 | } 151 | 152 | func configurePlayerEntityMock(id int, entity *stfake.Entity) { 153 | entity.On("ID").Return(id) 154 | 155 | var destroyCallback func() 156 | entity.On("OnDestroy", mock.Anything).Run(func(args mock.Arguments) { 157 | destroyCallback = args.Get(0).(func()) 158 | }) 159 | 160 | entity.On("OnPositionUpdate", mock.Anything).Return() 161 | prop := new(stfake.Property) 162 | prop.On("OnUpdate", mock.Anything).Return() 163 | entity.On("Property", mock.Anything).Return(prop) 164 | entity.On("BindProperty", mock.Anything, mock.Anything, mock.Anything) 165 | entity.On("Destroy").Run(func(mock.Arguments) { 166 | destroyCallback() 167 | }) 168 | } 169 | -------------------------------------------------------------------------------- /pkg/demoinfocs/debug_off.go: -------------------------------------------------------------------------------- 1 | //+build !debugdemoinfocs 2 | 3 | // This file is just a bunch of NOPs for the release build, see debug_on.go for debugging stuff 4 | 5 | package demoinfocs 6 | 7 | import ( 8 | msg "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg" 9 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 10 | ) 11 | 12 | func debugGameEvent(descriptor *msg.CSVCMsg_GameEventListDescriptorT, ge *msg.CSVCMsg_GameEvent) { 13 | // NOP 14 | } 15 | 16 | func debugUnhandledMessage(cmd int, name string) { 17 | // NOP 18 | } 19 | 20 | func debugIngameTick(tickNr int) { 21 | // NOP 22 | } 23 | 24 | func debugDemoCommand(cmd demoCommand) { 25 | // NOP 26 | } 27 | 28 | func debugAllServerClasses(classes st.ServerClasses) { 29 | // NOP 30 | } 31 | -------------------------------------------------------------------------------- /pkg/demoinfocs/debug_on.go: -------------------------------------------------------------------------------- 1 | //go:build debugdemoinfocs 2 | // +build debugdemoinfocs 3 | 4 | // Functions to print out debug information if the build tag is enabled 5 | 6 | package demoinfocs 7 | 8 | import ( 9 | "fmt" 10 | 11 | msg "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg" 12 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 13 | ) 14 | 15 | const ( 16 | yes = "YES" 17 | no = "NO" 18 | ) 19 | 20 | // Can be overridden via -ldflags="-X 'github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs.debugServerClasses=YES'" 21 | // e.g. `go run -tags debugdemoinfocs -ldflags="-X 'github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs.debugDemoCommands=YES'" examples/print-events/print_events.go -demo example.dem` 22 | // Oh and btw we cant use bools for this, Go says 'cannot use -X with non-string symbol' 23 | var ( 24 | debugGameEvents = yes 25 | debugUnhandledMessages = no 26 | debugIngameTicks = yes 27 | debugDemoCommands = no 28 | debugServerClasses = no 29 | ) 30 | 31 | func debugGameEvent(d *msg.CSVCMsg_GameEventListDescriptorT, ge *msg.CSVCMsg_GameEvent) { 32 | const ( 33 | typeStr = 1 34 | typeFloat = 2 35 | typeLong = 3 36 | typeShort = 4 37 | typeByte = 5 38 | typeBool = 6 39 | typeUint64 = 7 40 | ) 41 | 42 | if debugGameEvents == yes { 43 | // Map only the relevant data for each type 44 | data := make(map[string]any) 45 | 46 | for k, v := range mapGameEventData(d, ge) { 47 | switch v.GetType() { 48 | case typeStr: 49 | data[k] = v.GetValString() 50 | case typeFloat: 51 | data[k] = v.GetValFloat() 52 | case typeLong: 53 | data[k] = v.GetValLong() 54 | case typeShort: 55 | data[k] = v.GetValShort() 56 | case typeByte: 57 | data[k] = v.GetValByte() 58 | case typeBool: 59 | data[k] = v.GetValBool() 60 | case typeUint64: 61 | data[k] = v.GetValUint64() 62 | } 63 | } 64 | 65 | fmt.Println("GameEvent:", d.GetName(), "Data:", data) 66 | } 67 | } 68 | 69 | func debugUnhandledMessage(cmd int, name string) { 70 | if debugUnhandledMessages == yes { 71 | fmt.Printf("UnhandledMessage: id=%d name=%s\n", cmd, name) 72 | } 73 | } 74 | 75 | func debugIngameTick(tickNr int) { 76 | if debugIngameTicks == yes { 77 | fmt.Printf("IngameTick=%d\n", tickNr) 78 | } 79 | } 80 | 81 | func (dc demoCommand) String() string { 82 | switch dc { 83 | case dcConsoleCommand: 84 | return "ConsoleCommand" 85 | case dcCustomData: 86 | return "CustomData" 87 | case dcDataTables: 88 | return "DataTables" 89 | case dcPacket: 90 | return "Packet" 91 | case dcSignon: 92 | return "Signon" 93 | case dcStop: 94 | return "Stop" 95 | case dcStringTables: 96 | return "StringTables" 97 | case dcSynctick: 98 | return "Synctick" 99 | case dcUserCommand: 100 | return "UserCommand" 101 | default: 102 | return "UnknownCommand" 103 | } 104 | } 105 | 106 | func debugDemoCommand(cmd demoCommand) { 107 | if debugDemoCommands == yes { 108 | fmt.Println("Demo-Command:", cmd) 109 | } 110 | } 111 | 112 | func debugAllServerClasses(classes st.ServerClasses) { 113 | if debugServerClasses == yes { 114 | for _, sc := range classes.All() { 115 | fmt.Println(sc) 116 | fmt.Println() 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /pkg/demoinfocs/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package demoinfocs provides a demo parser for the game Counter-Strike: Global Offensive. 3 | It is based on the official demoinfogo tool by Valve as well as Stats Helix's demoinfo. 4 | 5 | A good entry point to using the library is the parser interface. 6 | 7 | Demo events are documented in the events package. 8 | */ 9 | package demoinfocs 10 | -------------------------------------------------------------------------------- /pkg/demoinfocs/event-list-dump/13990.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/pkg/demoinfocs/event-list-dump/13990.bin -------------------------------------------------------------------------------- /pkg/demoinfocs/event-list-dump/13992.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/pkg/demoinfocs/event-list-dump/13992.bin -------------------------------------------------------------------------------- /pkg/demoinfocs/event-list-dump/14023.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/pkg/demoinfocs/event-list-dump/14023.bin -------------------------------------------------------------------------------- /pkg/demoinfocs/event-list-dump/14070.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/pkg/demoinfocs/event-list-dump/14070.bin -------------------------------------------------------------------------------- /pkg/demoinfocs/events/events_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 10 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 11 | ) 12 | 13 | func TestPlayerFlashed_FlashDuration(t *testing.T) { 14 | p := common.NewPlayer(demoInfoProviderMock{}) 15 | e := PlayerFlashed{Player: p} 16 | 17 | assert.Equal(t, time.Duration(0), e.FlashDuration()) 18 | 19 | p.FlashDuration = 2.3 20 | 21 | assert.Equal(t, 2300*time.Millisecond, e.FlashDuration()) 22 | } 23 | 24 | func TestGrenadeEvent_Base(t *testing.T) { 25 | base := GrenadeEvent{GrenadeEntityID: 1} 26 | flashEvent := FlashExplode{base} 27 | 28 | assert.Equal(t, base, flashEvent.Base()) 29 | } 30 | 31 | func TestBombEvents(t *testing.T) { 32 | events := []BombEventIf{ 33 | BombDefuseStart{}, 34 | BombDefuseAborted{}, 35 | BombDefused{}, 36 | BombExplode{}, 37 | BombPlantBegin{}, 38 | BombPlanted{}, 39 | } 40 | 41 | for _, e := range events { 42 | e.implementsBombEventIf() 43 | } 44 | } 45 | 46 | func TestRankUpdate_SteamID64(t *testing.T) { 47 | event := RankUpdate{SteamID32: 52686539} 48 | 49 | assert.Equal(t, uint64(76561198012952267), event.SteamID64()) 50 | } 51 | 52 | func TestKill_IsWallBang(t *testing.T) { 53 | event := Kill{PenetratedObjects: 1} 54 | 55 | assert.True(t, event.IsWallBang()) 56 | } 57 | 58 | type demoInfoProviderMock struct { 59 | isSource2 bool 60 | } 61 | 62 | func (p demoInfoProviderMock) FindEntityByHandle(handle uint64) st.Entity { 63 | panic("implement me") 64 | } 65 | 66 | func (p demoInfoProviderMock) IngameTick() int { 67 | return 0 68 | } 69 | 70 | func (p demoInfoProviderMock) IsSource2() bool { 71 | return p.isSource2 72 | } 73 | 74 | func (p demoInfoProviderMock) TickRate() float64 { 75 | return 128 76 | } 77 | 78 | func (p demoInfoProviderMock) FindPlayerByHandle(uint64) *common.Player { 79 | return nil 80 | } 81 | 82 | func (p demoInfoProviderMock) FindPlayerByPawnHandle(uint64) *common.Player { 83 | return nil 84 | } 85 | 86 | func (p demoInfoProviderMock) PlayerResourceEntity() st.Entity { 87 | return nil 88 | } 89 | 90 | func (p demoInfoProviderMock) FindWeaponByEntityID(int) *common.Equipment { 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /pkg/demoinfocs/examples_test.go: -------------------------------------------------------------------------------- 1 | package demoinfocs_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | demoinfocs "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 10 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 11 | ) 12 | 13 | /* 14 | This will print all kills of a demo in the format '[[killer]] <[[weapon]] [(HS)] [(WB)]> [[victim]]' 15 | */ 16 | //noinspection GoUnhandledErrorResult 17 | func ExampleParser() { 18 | f, err := os.Open("../../test/cs-demos/s2/s2.dem") 19 | if err != nil { 20 | log.Panic("failed to open demo file: ", err) 21 | } 22 | 23 | defer f.Close() 24 | 25 | p := demoinfocs.NewParser(f) 26 | defer p.Close() 27 | 28 | // Register handler on kill events 29 | p.RegisterEventHandler(func(e events.Kill) { 30 | var hs string 31 | if e.IsHeadshot { 32 | hs = " (HS)" 33 | } 34 | 35 | var wallBang string 36 | if e.PenetratedObjects > 0 { 37 | wallBang = " (WB)" 38 | } 39 | 40 | fmt.Printf("%s <%v%s%s> %s\n", e.Killer, e.Weapon, hs, wallBang, e.Victim) 41 | }) 42 | 43 | // Parse to end 44 | err = p.ParseToEnd() 45 | if err != nil { 46 | log.Panic("failed to parse demo: ", err) 47 | } 48 | } 49 | 50 | func TestExamplesWithoutOutput(t *testing.T) { 51 | if testing.Short() { 52 | t.Skip("skipping long running test") 53 | } 54 | 55 | ExampleParser() 56 | } 57 | -------------------------------------------------------------------------------- /pkg/demoinfocs/fake/game_rules.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stretchr/testify/mock" 7 | 8 | demoinfocs "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 9 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 10 | ) 11 | 12 | var _ demoinfocs.GameRules = new(GameRules) 13 | 14 | // GameRules is a mock for of demoinfocs.GameRules. 15 | type GameRules struct { 16 | mock.Mock 17 | } 18 | 19 | func (gr *GameRules) Entity() st.Entity { 20 | return gr.Called().Get(0).(st.Entity) 21 | } 22 | 23 | // BombTime is a mock-implementation of GameRules.BombTime(). 24 | func (gr *GameRules) BombTime() (time.Duration, error) { 25 | return gr.Called().Get(0).(time.Duration), gr.Called().Get(0).(error) 26 | } 27 | 28 | // FreezeTime is a mock-implementation of GameRules.FreezeTime(). 29 | func (gr *GameRules) FreezeTime() (time.Duration, error) { 30 | return gr.Called().Get(0).(time.Duration), gr.Called().Get(0).(error) 31 | } 32 | 33 | // RoundTime is a mock-implementation of GameRules.RoundTime(). 34 | func (gr *GameRules) RoundTime() (time.Duration, error) { 35 | return gr.Called().Get(0).(time.Duration), gr.Called().Get(0).(error) 36 | } 37 | 38 | // ConVars is a mock-implementation of GameRules.ConVars(). 39 | func (gr *GameRules) ConVars() map[string]string { 40 | return gr.Called().Get(0).(map[string]string) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/demoinfocs/fake/game_state.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | 6 | demoinfocs "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 7 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 8 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 9 | ) 10 | 11 | var _ demoinfocs.GameState = new(GameState) 12 | 13 | // GameState is a mock for of demoinfocs.GameState. 14 | type GameState struct { 15 | mock.Mock 16 | } 17 | 18 | // OvertimeCount is a mock-implementation of GameState.OvertimeCount(). 19 | func (gs *GameState) OvertimeCount() int { 20 | return gs.Called().Int(0) 21 | } 22 | 23 | // IngameTick is a mock-implementation of GameState.IngameTick(). 24 | func (gs *GameState) IngameTick() int { 25 | return gs.Called().Int(0) 26 | } 27 | 28 | // TeamCounterTerrorists is a mock-implementation of GameState.TeamCounterTerrorists(). 29 | func (gs *GameState) TeamCounterTerrorists() *common.TeamState { 30 | return gs.Called().Get(0).(*common.TeamState) 31 | } 32 | 33 | // TeamTerrorists is a mock-implementation of GameState.TeamTerrorists(). 34 | func (gs *GameState) TeamTerrorists() *common.TeamState { 35 | return gs.Called().Get(0).(*common.TeamState) 36 | } 37 | 38 | // Team is a mock-implementation of GameState.Team(). 39 | func (gs *GameState) Team(team common.Team) *common.TeamState { 40 | return gs.Called().Get(0).(*common.TeamState) 41 | } 42 | 43 | // Participants is a mock-implementation of GameState.Participants(). 44 | func (gs *GameState) Participants() demoinfocs.Participants { 45 | return gs.Called().Get(0).(demoinfocs.Participants) 46 | } 47 | 48 | // GrenadeProjectiles is a mock-implementation of GameState.GrenadeProjectiles(). 49 | func (gs *GameState) GrenadeProjectiles() map[int]*common.GrenadeProjectile { 50 | return gs.Called().Get(0).(map[int]*common.GrenadeProjectile) 51 | } 52 | 53 | // Infernos is a mock-implementation of GameState.Infernos(). 54 | func (gs *GameState) Infernos() map[int]*common.Inferno { 55 | return gs.Called().Get(0).(map[int]*common.Inferno) 56 | } 57 | 58 | // Weapons is a mock-implementation of GameState.Weapons(). 59 | func (gs *GameState) Weapons() map[int]*common.Equipment { 60 | return gs.Called().Get(0).(map[int]*common.Equipment) 61 | } 62 | 63 | // Entities is a mock-implementation of GameState.Entities(). 64 | func (gs *GameState) Entities() map[int]st.Entity { 65 | return gs.Called().Get(0).(map[int]st.Entity) 66 | } 67 | 68 | // Bomb is a mock-implementation of GameState.Bomb(). 69 | func (gs *GameState) Bomb() *common.Bomb { 70 | return gs.Called().Get(0).(*common.Bomb) 71 | } 72 | 73 | // TotalRoundsPlayed is a mock-implementation of GameState.TotalRoundsPlayed(). 74 | func (gs *GameState) TotalRoundsPlayed() int { 75 | return gs.Called().Int(0) 76 | } 77 | 78 | // GamePhase is a mock-implementation of GameState.GamePhase(). 79 | func (gs *GameState) GamePhase() common.GamePhase { 80 | return gs.Called().Get(0).(common.GamePhase) 81 | } 82 | 83 | // IsWarmupPeriod is a mock-implementation of GameState.IsWarmupPeriod(). 84 | func (gs *GameState) IsWarmupPeriod() bool { 85 | return gs.Called().Bool(0) 86 | } 87 | 88 | // IsFreezetimePeriod is a mock-implementation of GameState.IsFreezetimePeriod(). 89 | func (gs *GameState) IsFreezetimePeriod() bool { 90 | return gs.Called().Bool(0) 91 | } 92 | 93 | // IsMatchStarted is a mock-implementation of GameState.IsMatchStarted(). 94 | func (gs *GameState) IsMatchStarted() bool { 95 | return gs.Called().Bool(0) 96 | } 97 | 98 | // Rules is a mock-implementation of GameState.Rules(). 99 | func (gs *GameState) Rules() demoinfocs.GameRules { 100 | return gs.Called().Get(0).(demoinfocs.GameRules) 101 | } 102 | 103 | // PlayerResourceEntity is a mock-implementation of GameState.PlayerResourceEntity(). 104 | func (gs *GameState) PlayerResourceEntity() st.Entity { 105 | return gs.Called().Get(0).(st.Entity) 106 | } 107 | 108 | // Hostages is a mock-implementation of GameState.Hostages(). 109 | func (gs *GameState) Hostages() []*common.Hostage { 110 | return gs.Called().Get(0).([]*common.Hostage) 111 | } 112 | 113 | // EntityByHandle is a mock-implementation of GameState.EntityByHandle(). 114 | func (gs *GameState) EntityByHandle(handle uint64) st.Entity { 115 | return gs.Called(handle).Get(0).(st.Entity) 116 | } 117 | -------------------------------------------------------------------------------- /pkg/demoinfocs/fake/parser_test.go: -------------------------------------------------------------------------------- 1 | package fake_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | assert "github.com/stretchr/testify/assert" 8 | "google.golang.org/protobuf/proto" 9 | 10 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 11 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 12 | fake "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/fake" 13 | msg "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg" 14 | ) 15 | 16 | func TestParseHeader(t *testing.T) { 17 | p := fake.NewParser() 18 | expected := common.DemoHeader{ 19 | Filestamp: "HL2DEMO", 20 | MapName: "de_cache", 21 | PlaybackFrames: 64 * 1000, 22 | PlaybackTicks: 128 * 1000, 23 | PlaybackTime: time.Second * 1000, 24 | } 25 | p.On("ParseHeader").Return(expected, nil) 26 | 27 | actual, err := p.ParseHeader() 28 | 29 | assert.Nil(t, err) 30 | assert.Equal(t, expected, actual) 31 | } 32 | 33 | func TestParseNextFrameEvents(t *testing.T) { 34 | p := fake.NewParser() 35 | p.On("ParseNextFrame").Return(true, nil) 36 | 37 | expected := []any{kill(common.EqAK47), kill(common.EqScout)} 38 | p.MockEvents(expected...) 39 | 40 | // Kill on second frame that shouldn't be dispatched during the first frame 41 | p.MockEvents(kill(common.EqAUG)) 42 | 43 | var actual []any 44 | p.RegisterEventHandler(func(e events.Kill) { 45 | actual = append(actual, e) 46 | }) 47 | 48 | next, err := p.ParseNextFrame() 49 | 50 | assert.True(t, next) 51 | assert.Nil(t, err) 52 | assert.Equal(t, expected, actual) 53 | } 54 | 55 | func kill(wepType common.EquipmentType) events.Kill { 56 | wep := common.NewEquipment(wepType) 57 | return events.Kill{ 58 | Killer: new(common.Player), 59 | Weapon: wep, 60 | Victim: new(common.Player), 61 | } 62 | } 63 | 64 | func TestParseToEndEvents(t *testing.T) { 65 | p := fake.NewParser() 66 | p.On("ParseToEnd").Return(nil) 67 | expected := []any{kill(common.EqAK47), kill(common.EqScout), kill(common.EqAUG)} 68 | p.MockEvents(expected[:1]...) 69 | p.MockEvents(expected[1:]...) 70 | 71 | var actual []any 72 | p.RegisterEventHandler(func(e events.Kill) { 73 | actual = append(actual, e) 74 | }) 75 | 76 | err := p.ParseToEnd() 77 | 78 | assert.Nil(t, err) 79 | assert.Equal(t, expected, actual) 80 | } 81 | 82 | func TestParseNextFrameNetMessages(t *testing.T) { 83 | p := fake.NewParser() 84 | p.On("ParseNextFrame").Return(true, nil) 85 | expected := []any{ 86 | cmdKey(1, 2, 3), 87 | cmdKey(100, 255, 8), 88 | } 89 | 90 | p.MockNetMessages(expected...) 91 | // Message on second frame that shouldn't be dispatched during the first frame 92 | p.MockNetMessages(msg.CSVCMsg_Menu{DialogType: proto.Int32(1), MenuKeyValues: []byte{1, 55, 99}}) 93 | 94 | var actual []any 95 | p.RegisterNetMessageHandler(func(message any) { 96 | actual = append(actual, message) 97 | }) 98 | 99 | next, err := p.ParseNextFrame() 100 | 101 | assert.True(t, next) 102 | assert.Nil(t, err) 103 | assert.Equal(t, expected, actual) 104 | } 105 | 106 | func TestParseToEndNetMessages(t *testing.T) { 107 | p := fake.NewParser() 108 | p.On("ParseToEnd").Return(nil) 109 | expected := []any{ 110 | cmdKey(1, 2, 3), 111 | cmdKey(100, 255, 8), 112 | msg.CSVCMsg_Menu{DialogType: proto.Int32(1), MenuKeyValues: []byte{1, 55, 99}}, 113 | } 114 | 115 | p.MockNetMessages(expected[:1]...) 116 | p.MockNetMessages(expected[1:]...) 117 | 118 | var actual []any 119 | p.RegisterNetMessageHandler(func(message any) { 120 | actual = append(actual, message) 121 | }) 122 | 123 | err := p.ParseToEnd() 124 | 125 | assert.Nil(t, err) 126 | assert.Equal(t, expected, actual) 127 | } 128 | 129 | func cmdKey(b ...byte) msg.CSVCMsg_CmdKeyValues { 130 | return msg.CSVCMsg_CmdKeyValues{ 131 | Keyvalues: b, 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /pkg/demoinfocs/fake/participants.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | 6 | demoinfocs "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 7 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 8 | ) 9 | 10 | var _ demoinfocs.Participants = new(Participants) 11 | 12 | // Participants is a mock for of demoinfocs.Participants. 13 | type Participants struct { 14 | mock.Mock 15 | } 16 | 17 | // ByUserID is a mock-implementation of Participants.ByUserID(). 18 | func (ptcp *Participants) ByUserID() map[int]*common.Player { 19 | return ptcp.Called().Get(0).(map[int]*common.Player) 20 | } 21 | 22 | // ByEntityID is a mock-implementation of Participants.ByEntityID(). 23 | func (ptcp *Participants) ByEntityID() map[int]*common.Player { 24 | return ptcp.Called().Get(0).(map[int]*common.Player) 25 | } 26 | 27 | // AllByUserID is a mock-implementation of Participants.AllByUserID(). 28 | func (ptcp *Participants) AllByUserID() map[int]*common.Player { 29 | return ptcp.Called().Get(0).(map[int]*common.Player) 30 | } 31 | 32 | // All is a mock-implementation of Participants.All(). 33 | func (ptcp *Participants) All() []*common.Player { 34 | return ptcp.Called().Get(0).([]*common.Player) 35 | } 36 | 37 | // Connected is a mock-implementation of Participants.Connected(). 38 | func (ptcp *Participants) Connected() []*common.Player { 39 | return ptcp.Called().Get(0).([]*common.Player) 40 | } 41 | 42 | // Playing is a mock-implementation of Participants.Playing(). 43 | func (ptcp *Participants) Playing() []*common.Player { 44 | return ptcp.Called().Get(0).([]*common.Player) 45 | } 46 | 47 | // TeamMembers is a mock-implementation of Participants.TeamMembers(). 48 | func (ptcp *Participants) TeamMembers(team common.Team) []*common.Player { 49 | return ptcp.Called().Get(0).([]*common.Player) 50 | } 51 | 52 | // FindByHandle is a mock-implementation of Participants.FindByHandle(). 53 | func (ptcp *Participants) FindByHandle(handle int) *common.Player { 54 | return ptcp.Called().Get(0).(*common.Player) 55 | } 56 | 57 | // FindByHandle64 is a mock-implementation of Participants.FindByHandle64(). 58 | func (ptcp *Participants) FindByHandle64(handle uint64) *common.Player { 59 | return ptcp.Called().Get(0).(*common.Player) 60 | } 61 | 62 | // FindByPawnHandle is a mock-implementation of Participants.FindByPawnHandle(). 63 | func (ptcp *Participants) FindByPawnHandle(handle uint64) *common.Player { 64 | return ptcp.Called().Get(0).(*common.Player) 65 | } 66 | 67 | // SpottersOf is a mock-implementation of Participants.SpottersOf(). 68 | func (ptcp *Participants) SpottersOf(spotted *common.Player) []*common.Player { 69 | return ptcp.Called().Get(0).([]*common.Player) 70 | } 71 | 72 | // SpottedBy is a mock-implementation of Participants.SpottedBy(). 73 | func (ptcp *Participants) SpottedBy(spotter *common.Player) []*common.Player { 74 | return ptcp.Called().Get(0).([]*common.Player) 75 | } 76 | -------------------------------------------------------------------------------- /pkg/demoinfocs/game_rules_interface.go: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: Auto generated 2 | 3 | package demoinfocs 4 | 5 | import ( 6 | "time" 7 | 8 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 9 | ) 10 | 11 | // GameRules is an auto-generated interface for gameRules. 12 | type GameRules interface { 13 | // RoundTime returns how long rounds in the current match last for (excluding freeze time). 14 | // May return error if cs_gamerules_data.m_iRoundTime is not set. 15 | RoundTime() (time.Duration, error) 16 | // FreezeTime returns how long freeze time lasts for in the current match (mp_freezetime). 17 | // May return error if mp_freezetime cannot be converted to a time duration. 18 | FreezeTime() (time.Duration, error) 19 | // BombTime returns how long freeze time lasts for in the current match (mp_freezetime). 20 | // May return error if mp_c4timer cannot be converted to a time duration. 21 | BombTime() (time.Duration, error) 22 | // ConVars returns a map of CVar keys and values. 23 | // Not all values might be set. 24 | // See also: https://developer.valvesoftware.com/wiki/List_of_CS:GO_Cvars. 25 | ConVars() map[string]string 26 | // Entity returns the game's CCSGameRulesProxy entity. 27 | Entity() st.Entity 28 | } 29 | -------------------------------------------------------------------------------- /pkg/demoinfocs/game_state_interface.go: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: Auto generated 2 | 3 | package demoinfocs 4 | 5 | import ( 6 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 7 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 8 | ) 9 | 10 | // GameState is an auto-generated interface for gameState. 11 | // gameState contains all game-state relevant information. 12 | type GameState interface { 13 | // IngameTick returns the latest actual tick number of the server during the game. 14 | // 15 | // Watch out, I've seen this return wonky negative numbers at the start of demos. 16 | IngameTick() int 17 | // Team returns the TeamState corresponding to team. 18 | // Returns nil if team != TeamTerrorists && team != TeamCounterTerrorists. 19 | // 20 | // Make sure to handle swapping sides properly if you keep the reference. 21 | Team(team common.Team) *common.TeamState 22 | // TeamCounterTerrorists returns the TeamState of the CT team. 23 | // 24 | // Make sure to handle swapping sides properly if you keep the reference. 25 | TeamCounterTerrorists() *common.TeamState 26 | // TeamTerrorists returns the TeamState of the T team. 27 | // 28 | // Make sure to handle swapping sides properly if you keep the reference. 29 | TeamTerrorists() *common.TeamState 30 | // Participants returns a struct with all currently connected players & spectators and utility functions. 31 | // The struct contains references to the original maps so it's always up-to-date. 32 | Participants() Participants 33 | // Rules returns the GameRules for the current match. 34 | // Contains information like freeze time duration etc. 35 | Rules() GameRules 36 | // Hostages returns all current hostages. 37 | Hostages() []*common.Hostage 38 | // GrenadeProjectiles returns a map from entity-IDs to all live grenade projectiles. 39 | // 40 | // Only constains projectiles currently in-flight or still active (smokes etc.), 41 | // i.e. have been thrown but have yet to detonate. 42 | GrenadeProjectiles() map[int]*common.GrenadeProjectile 43 | // Infernos returns a map from entity-IDs to all currently burning infernos (fires from incendiaries and Molotovs). 44 | Infernos() map[int]*common.Inferno 45 | // Weapons returns a map from entity-IDs to all weapons currently in the game. 46 | Weapons() map[int]*common.Equipment 47 | // Entities returns all currently existing entities. 48 | // (Almost?) everything in the game is an entity, such as weapons, players, fire etc. 49 | Entities() map[int]st.Entity 50 | // Bomb returns the current bomb state. 51 | Bomb() *common.Bomb 52 | // TotalRoundsPlayed returns the amount of total rounds played according to CCSGameRulesProxy. 53 | TotalRoundsPlayed() int 54 | // GamePhase returns the game phase of the current game state. See common/gamerules.go for more. 55 | GamePhase() common.GamePhase 56 | // IsWarmupPeriod returns whether the game is currently in warmup period according to CCSGameRulesProxy. 57 | IsWarmupPeriod() bool 58 | // IsFreezetimePeriod returns whether the game is currently in freezetime period according to CCSGameRulesProxy. 59 | IsFreezetimePeriod() bool 60 | // IsMatchStarted returns whether the match has started according to CCSGameRulesProxy. 61 | IsMatchStarted() bool 62 | // OvertimeCount returns the number of overtime according to CCSGameRulesProxy. 63 | OvertimeCount() int 64 | // PlayerResourceEntity returns the game's CCSPlayerResource entity. 65 | // Contains scoreboard information and more. 66 | PlayerResourceEntity() st.Entity 67 | // EntityByHandle returns the entity corresponding to the given handle. 68 | // Returns nil if the handle is invalid. 69 | EntityByHandle(handle uint64) st.Entity 70 | } 71 | -------------------------------------------------------------------------------- /pkg/demoinfocs/matchinfo.go: -------------------------------------------------------------------------------- 1 | package demoinfocs 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | "google.golang.org/protobuf/proto" 9 | 10 | "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg" 11 | ) 12 | 13 | // MatchInfoDecryptionKey extracts the net-message decryption key stored in `match730_*.dem.info`. 14 | // Pass the whole contents of `match730_*.dem.info` to this function to get the key. 15 | // See also: ParserConfig.NetMessageDecryptionKey 16 | func MatchInfoDecryptionKey(b []byte) ([]byte, error) { 17 | m := new(msg.CDataGCCStrike15V2_MatchInfo) 18 | 19 | err := proto.Unmarshal(b, m) 20 | if err != nil { 21 | return nil, errors.Wrap(err, "failed to unmarshal MatchInfo message") 22 | } 23 | 24 | k := []byte(strings.ToUpper(fmt.Sprintf("%016x", m.Watchablematchinfo.GetClDecryptdataKeyPub()))) 25 | 26 | return k, nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msg/doc.go: -------------------------------------------------------------------------------- 1 | // Package msg contains the generated protobuf demo message code. 2 | // 3 | // Use 'go generate' to generate the code from the .proto files inside the proto sub directory. 4 | // If you're on Windows you'll need to run go generate from CMD, not Bash. 5 | package msg 6 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msg/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | protoc -Iproto \ 4 | --go_out=. \ 5 | --go_opt=Mcstrike15_usermessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg \ 6 | --go_opt=Mcstrike15_gcmessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg \ 7 | --go_opt=Mengine_gcmessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg \ 8 | --go_opt=Mnetmessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg \ 9 | --go_opt=Msteammessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg \ 10 | --go_opt=Mgcsdk_gcmessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg \ 11 | --go_opt=Mnetworkbasetypes.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg \ 12 | --go_opt=Mnetwork_connection.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg \ 13 | --go_opt=module=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg \ 14 | cstrike15_gcmessages.proto \ 15 | cstrike15_usermessages.proto \ 16 | engine_gcmessages.proto \ 17 | netmessages.proto \ 18 | steammessages.proto 19 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msg/msg.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | //go:generate ./generate.sh 4 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msg/proto/engine_gcmessages.proto: -------------------------------------------------------------------------------- 1 | import "google/protobuf/descriptor.proto"; 2 | 3 | option cc_generic_services = false; 4 | 5 | message CEngineGotvSyncPacket { 6 | optional uint64 match_id = 1; 7 | optional uint32 instance_id = 2; 8 | optional uint32 signupfragment = 3; 9 | optional uint32 currentfragment = 4; 10 | optional float tickrate = 5; 11 | optional uint32 tick = 6; 12 | optional float rtdelay = 8; 13 | optional float rcvage = 9; 14 | optional float keyframe_interval = 10; 15 | optional uint32 cdndelay = 11; 16 | } 17 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msg/proto/steammessages.proto: -------------------------------------------------------------------------------- 1 | import "google/protobuf/descriptor.proto"; 2 | 3 | option optimize_for = SPEED; 4 | option cc_generic_services = false; 5 | 6 | extend .google.protobuf.FieldOptions { 7 | optional bool key_field = 60000 [default = false]; 8 | } 9 | 10 | extend .google.protobuf.MessageOptions { 11 | optional int32 msgpool_soft_limit = 60000 [default = 32]; 12 | optional int32 msgpool_hard_limit = 60001 [default = 384]; 13 | } 14 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/doc.go: -------------------------------------------------------------------------------- 1 | // Package msg contains the generated protobuf demo message code. 2 | // 3 | // Use 'go generate' to generate the code from the .proto files inside the proto sub directory. 4 | // If you're on Windows you'll need to run go generate from CMD, not Bash. 5 | package msgs2 6 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # sed -i '1i\package com.github.markus_wa.demoinfocs_golang.s2;\n' proto/s2/*.proto 4 | # sed -i 's/ \.(?!google)/ com.github.markus_wa.demoinfocs_golang.s2./g' proto/s2/*.proto 5 | 6 | protoc -Iproto \ 7 | --go_out=. \ 8 | --go_opt=Ms2/cstrike15_usermessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 9 | --go_opt=Ms2/cstrike15_gcmessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 10 | --go_opt=Ms2/engine_gcmessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 11 | --go_opt=Ms2/netmessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 12 | --go_opt=Ms2/steammessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 13 | --go_opt=Ms2/gcsdk_gcmessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 14 | --go_opt=Ms2/networkbasetypes.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 15 | --go_opt=Ms2/network_connection.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 16 | --go_opt=Ms2/demo.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 17 | --go_opt=Ms2/gameevents.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 18 | --go_opt=Ms2/usermessages.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 19 | --go_opt=Ms2/cs_gameevents.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 20 | --go_opt=Ms2/te.proto=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 21 | --go_opt=module=github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2 \ 22 | s2/cstrike15_gcmessages.proto \ 23 | s2/cstrike15_usermessages.proto \ 24 | s2/engine_gcmessages.proto \ 25 | s2/netmessages.proto \ 26 | s2/steammessages.proto \ 27 | s2/gcsdk_gcmessages.proto \ 28 | s2/networkbasetypes.proto \ 29 | s2/network_connection.proto \ 30 | s2/demo.proto \ 31 | s2/gameevents.proto \ 32 | s2/usermessages.proto \ 33 | s2/cs_gameevents.proto \ 34 | s2/te.proto 35 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/msg.go: -------------------------------------------------------------------------------- 1 | package msgs2 2 | 3 | //go:generate ./generate.sh 4 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/c_peer2peer_netmessages.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/netmessages.proto"; 4 | import "s2/networkbasetypes.proto"; 5 | 6 | enum P2P_Messages { 7 | p2p_TextMessage = 256; 8 | p2p_Voice = 257; 9 | p2p_Ping = 258; 10 | p2p_VRAvatarPosition = 259; 11 | p2p_WatchSynchronization = 260; 12 | p2p_FightingGame_GameData = 261; 13 | p2p_FightingGame_Connection = 262; 14 | } 15 | 16 | message CP2P_TextMessage { 17 | optional bytes text = 1; 18 | } 19 | 20 | message CSteam_Voice_Encoding { 21 | optional bytes voice_data = 1; 22 | } 23 | 24 | message CP2P_Voice { 25 | enum Handler_Flags { 26 | Played_Audio = 1; 27 | } 28 | 29 | optional CMsgVoiceAudio audio = 1; 30 | optional uint32 broadcast_group = 2; 31 | } 32 | 33 | message CP2P_Ping { 34 | required uint64 send_time = 1; 35 | required bool is_reply = 2; 36 | } 37 | 38 | message CP2P_VRAvatarPosition { 39 | message COrientation { 40 | optional CMsgVector pos = 1; 41 | optional CMsgQAngle ang = 2; 42 | } 43 | 44 | repeated CP2P_VRAvatarPosition.COrientation body_parts = 1; 45 | optional int32 hat_id = 2; 46 | optional int32 scene_id = 3; 47 | optional int32 world_scale = 4; 48 | } 49 | 50 | message CP2P_WatchSynchronization { 51 | optional int32 demo_tick = 1; 52 | optional bool paused = 2; 53 | optional uint64 tv_listen_voice_indices = 3; 54 | optional int32 dota_spectator_mode = 4; 55 | optional bool dota_spectator_watching_broadcaster = 5; 56 | optional int32 dota_spectator_hero_index = 6; 57 | optional int32 dota_spectator_autospeed = 7; 58 | optional int32 dota_replay_speed = 8; 59 | } 60 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/clientmessages.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | enum EBaseClientMessages { 4 | CM_CustomGameEvent = 280; 5 | CM_CustomGameEventBounce = 281; 6 | CM_ClientUIEvent = 282; 7 | CM_DevPaletteVisibilityChanged = 283; 8 | CM_WorldUIControllerHasPanelChanged = 284; 9 | CM_RotateAnchor = 285; 10 | CM_ListenForResponseFound = 286; 11 | CM_MAX_BASE = 300; 12 | } 13 | 14 | enum EClientUIEvent { 15 | EClientUIEvent_Invalid = 0; 16 | EClientUIEvent_DialogFinished = 1; 17 | EClientUIEvent_FireOutput = 2; 18 | } 19 | 20 | message CClientMsg_CustomGameEvent { 21 | optional string event_name = 1; 22 | optional bytes data = 2; 23 | } 24 | 25 | message CClientMsg_CustomGameEventBounce { 26 | optional string event_name = 1; 27 | optional bytes data = 2; 28 | optional int32 player_slot = 3 [default = -1]; 29 | } 30 | 31 | message CClientMsg_ClientUIEvent { 32 | optional EClientUIEvent event = 1 [default = EClientUIEvent_Invalid]; 33 | optional uint32 ent_ehandle = 2; 34 | optional uint32 client_ehandle = 3; 35 | optional string data1 = 4; 36 | optional string data2 = 5; 37 | } 38 | 39 | message CClientMsg_DevPaletteVisibilityChangedEvent { 40 | optional bool visible = 1; 41 | } 42 | 43 | message CClientMsg_WorldUIControllerHasPanelChangedEvent { 44 | optional bool has_panel = 1; 45 | optional uint32 client_ehandle = 2; 46 | optional uint32 literal_hand_type = 3; 47 | } 48 | 49 | message CClientMsg_RotateAnchor { 50 | optional float angle = 1; 51 | } 52 | 53 | message CClientMsg_ListenForResponseFound { 54 | optional int32 player_slot = 1 [default = -1]; 55 | } 56 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/connectionless_netmessages.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/netmessages.proto"; 4 | 5 | message C2S_CONNECT_Message { 6 | optional uint32 host_version = 1; 7 | optional uint32 auth_protocol = 2; 8 | optional uint32 challenge_number = 3; 9 | optional fixed64 reservation_cookie = 4; 10 | optional bool low_violence = 5; 11 | optional bytes encrypted_password = 6; 12 | repeated CCLCMsg_SplitPlayerConnect splitplayers = 7; 13 | optional bytes auth_steam = 8; 14 | optional string challenge_context = 9; 15 | } 16 | 17 | message C2S_CONNECTION_Message { 18 | optional string addon_name = 1; 19 | } 20 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/cs_gameevents.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/networkbasetypes.proto"; 4 | 5 | enum ECsgoGameEvents { 6 | GE_PlayerAnimEventId = 450; 7 | GE_RadioIconEventId = 451; 8 | GE_FireBulletsId = 452; 9 | } 10 | 11 | message CMsgTEPlayerAnimEvent { 12 | optional fixed32 player = 1 [default = 16777215]; 13 | optional uint32 event = 2; 14 | optional int32 data = 3; 15 | } 16 | 17 | message CMsgTERadioIcon { 18 | optional fixed32 player = 1 [default = 16777215]; 19 | } 20 | 21 | message CMsgTEFireBullets { 22 | optional CMsgVector origin = 1; 23 | optional CMsgQAngle angles = 2; 24 | optional uint32 weapon_id = 3 [default = 16777215]; 25 | optional uint32 mode = 4; 26 | optional uint32 seed = 5; 27 | optional fixed32 player = 6 [default = 16777215]; 28 | optional float inaccuracy = 7; 29 | optional float recoil_index = 8; 30 | optional float spread = 9; 31 | optional int32 sound_type = 10; 32 | optional uint32 item_def_index = 11; 33 | optional fixed32 sound_dsp_effect = 12; 34 | optional CMsgVector ent_origin = 13; 35 | optional uint32 num_bullets_remaining = 14; 36 | optional uint32 attack_type = 15; 37 | } 38 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/cs_usercmd.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/networkbasetypes.proto"; 4 | import "s2/usercmd.proto"; 5 | 6 | message CSGOInterpolationInfoPB { 7 | optional int32 src_tick = 1 [default = -1]; 8 | optional int32 dst_tick = 2 [default = -1]; 9 | optional float frac = 3 [default = 0]; 10 | } 11 | 12 | message CSGOInterpolationInfoPB_CL { 13 | optional float frac = 3 [default = 0]; 14 | } 15 | 16 | message CSGOInputHistoryEntryPB { 17 | optional CMsgQAngle view_angles = 2; 18 | optional int32 render_tick_count = 4; 19 | optional float render_tick_fraction = 5; 20 | optional int32 player_tick_count = 6; 21 | optional float player_tick_fraction = 7; 22 | optional CSGOInterpolationInfoPB_CL cl_interp = 12; 23 | optional CSGOInterpolationInfoPB sv_interp0 = 13; 24 | optional CSGOInterpolationInfoPB sv_interp1 = 14; 25 | optional CSGOInterpolationInfoPB player_interp = 15; 26 | optional int32 frame_number = 64; 27 | optional int32 target_ent_index = 65 [default = -1]; 28 | optional CMsgVector shoot_position = 66; 29 | optional CMsgVector target_head_pos_check = 67; 30 | optional CMsgVector target_abs_pos_check = 68; 31 | optional CMsgQAngle target_abs_ang_check = 69; 32 | } 33 | 34 | message CSGOUserCmdPB { 35 | optional CBaseUserCmdPB base = 1; 36 | repeated CSGOInputHistoryEntryPB input_history = 2; 37 | optional int32 attack1_start_history_index = 6 [default = -1]; 38 | optional int32 attack2_start_history_index = 7 [default = -1]; 39 | optional int32 attack3_start_history_index = 8 [default = -1]; 40 | optional bool left_hand_desired = 9 [default = false]; 41 | optional bool is_predicting_body_shot_fx = 11 [default = false]; 42 | optional bool is_predicting_head_shot_fx = 12 [default = false]; 43 | optional bool is_predicting_kill_ragdolls = 13 [default = false]; 44 | } 45 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/demo.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | enum EDemoCommands { 4 | DEM_Error = -1; 5 | DEM_Stop = 0; 6 | DEM_FileHeader = 1; 7 | DEM_FileInfo = 2; 8 | DEM_SyncTick = 3; 9 | DEM_SendTables = 4; 10 | DEM_ClassInfo = 5; 11 | DEM_StringTables = 6; 12 | DEM_Packet = 7; 13 | DEM_SignonPacket = 8; 14 | DEM_ConsoleCmd = 9; 15 | DEM_CustomData = 10; 16 | DEM_CustomDataCallbacks = 11; 17 | DEM_UserCmd = 12; 18 | DEM_FullPacket = 13; 19 | DEM_SaveGame = 14; 20 | DEM_SpawnGroups = 15; 21 | DEM_AnimationData = 16; 22 | DEM_AnimationHeader = 17; 23 | DEM_Recovery = 18; 24 | DEM_Max = 19; 25 | DEM_IsCompressed = 64; 26 | } 27 | 28 | message CDemoFileHeader { 29 | required string demo_file_stamp = 1; 30 | optional int32 network_protocol = 2; 31 | optional string server_name = 3; 32 | optional string client_name = 4; 33 | optional string map_name = 5; 34 | optional string game_directory = 6; 35 | optional int32 fullpackets_version = 7; 36 | optional bool allow_clientside_entities = 8; 37 | optional bool allow_clientside_particles = 9; 38 | optional string addons = 10; 39 | optional string demo_version_name = 11; 40 | optional string demo_version_guid = 12; 41 | optional int32 build_num = 13; 42 | optional string game = 14; 43 | optional int32 server_start_tick = 15; 44 | } 45 | 46 | message CGameInfo { 47 | message CDotaGameInfo { 48 | message CPlayerInfo { 49 | optional string hero_name = 1; 50 | optional string player_name = 2; 51 | optional bool is_fake_client = 3; 52 | optional uint64 steamid = 4; 53 | optional int32 game_team = 5; 54 | } 55 | 56 | message CHeroSelectEvent { 57 | optional bool is_pick = 1; 58 | optional uint32 team = 2; 59 | optional int32 hero_id = 3; 60 | } 61 | 62 | optional uint64 match_id = 1; 63 | optional int32 game_mode = 2; 64 | optional int32 game_winner = 3; 65 | repeated CGameInfo.CDotaGameInfo.CPlayerInfo player_info = 4; 66 | optional uint32 leagueid = 5; 67 | repeated CGameInfo.CDotaGameInfo.CHeroSelectEvent picks_bans = 6; 68 | optional uint32 radiant_team_id = 7; 69 | optional uint32 dire_team_id = 8; 70 | optional string radiant_team_tag = 9; 71 | optional string dire_team_tag = 10; 72 | optional uint32 end_time = 11; 73 | } 74 | 75 | message CCSGameInfo { 76 | repeated int32 round_start_ticks = 1; 77 | } 78 | 79 | optional CGameInfo.CDotaGameInfo dota = 4; 80 | optional CGameInfo.CCSGameInfo cs = 5; 81 | } 82 | 83 | message CDemoFileInfo { 84 | optional float playback_time = 1; 85 | optional int32 playback_ticks = 2; 86 | optional int32 playback_frames = 3; 87 | optional CGameInfo game_info = 4; 88 | } 89 | 90 | message CDemoPacket { 91 | optional bytes data = 3; 92 | } 93 | 94 | message CDemoFullPacket { 95 | optional CDemoStringTables string_table = 1; 96 | optional CDemoPacket packet = 2; 97 | } 98 | 99 | message CDemoSaveGame { 100 | optional bytes data = 1; 101 | optional fixed64 steam_id = 2; 102 | optional fixed64 signature = 3; 103 | optional int32 version = 4; 104 | } 105 | 106 | message CDemoSyncTick { 107 | } 108 | 109 | message CDemoConsoleCmd { 110 | optional string cmdstring = 1; 111 | } 112 | 113 | message CDemoSendTables { 114 | optional bytes data = 1; 115 | } 116 | 117 | message CDemoClassInfo { 118 | message class_t { 119 | optional int32 class_id = 1; 120 | optional string network_name = 2; 121 | optional string table_name = 3; 122 | } 123 | 124 | repeated CDemoClassInfo.class_t classes = 1; 125 | } 126 | 127 | message CDemoCustomData { 128 | optional int32 callback_index = 1; 129 | optional bytes data = 2; 130 | } 131 | 132 | message CDemoCustomDataCallbacks { 133 | repeated string save_id = 1; 134 | } 135 | 136 | message CDemoAnimationHeader { 137 | optional sint32 entity_id = 1; 138 | optional int32 tick = 2; 139 | optional bytes data = 3; 140 | } 141 | 142 | message CDemoAnimationData { 143 | optional sint32 entity_id = 1; 144 | optional int32 start_tick = 2; 145 | optional int32 end_tick = 3; 146 | optional bytes data = 4; 147 | optional int64 data_checksum = 5; 148 | } 149 | 150 | message CDemoStringTables { 151 | message items_t { 152 | optional string str = 1; 153 | optional bytes data = 2; 154 | } 155 | 156 | message table_t { 157 | optional string table_name = 1; 158 | repeated CDemoStringTables.items_t items = 2; 159 | repeated CDemoStringTables.items_t items_clientside = 3; 160 | optional int32 table_flags = 4; 161 | } 162 | 163 | repeated CDemoStringTables.table_t tables = 1; 164 | } 165 | 166 | message CDemoStop { 167 | } 168 | 169 | message CDemoUserCmd { 170 | optional int32 cmd_number = 1; 171 | optional bytes data = 2; 172 | } 173 | 174 | message CDemoSpawnGroups { 175 | repeated bytes msgs = 3; 176 | } 177 | 178 | message CDemoRecovery { 179 | message DemoInitialSpawnGroupEntry { 180 | optional uint32 spawngrouphandle = 1; 181 | optional bool was_created = 2; 182 | } 183 | 184 | optional CDemoRecovery.DemoInitialSpawnGroupEntry initial_spawn_group = 1; 185 | optional bytes spawn_group_message = 2; 186 | } 187 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/engine_gcmessages.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "google/protobuf/descriptor.proto"; 4 | 5 | message CEngineGotvSyncPacket { 6 | optional uint64 match_id = 1; 7 | optional uint32 instance_id = 2; 8 | optional uint32 signupfragment = 3; 9 | optional uint32 currentfragment = 4; 10 | optional float tickrate = 5; 11 | optional uint32 tick = 6; 12 | optional float rtdelay = 8; 13 | optional float rcvage = 9; 14 | optional float keyframe_interval = 10; 15 | optional uint32 cdndelay = 11; 16 | } 17 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/fatdemo.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/networkbasetypes.proto"; 4 | 5 | enum EHitGroup { 6 | EHG_Generic = 0; 7 | EHG_Head = 1; 8 | EHG_Chest = 2; 9 | EHG_Stomach = 3; 10 | EHG_LeftArm = 4; 11 | EHG_RightArm = 5; 12 | EHG_LeftLeg = 6; 13 | EHG_RightLeg = 7; 14 | EHG_Gear = 8; 15 | EHG_Miss = 9; 16 | } 17 | 18 | enum ETeam { 19 | ET_Unknown = 0; 20 | ET_Spectator = 1; 21 | ET_Terrorist = 2; 22 | ET_CT = 3; 23 | } 24 | 25 | enum EWeaponType { 26 | EWT_Knife = 0; 27 | EWT_Pistol = 1; 28 | EWT_SubMachineGun = 2; 29 | EWT_Rifle = 3; 30 | EWT_Shotgun = 4; 31 | EWT_SniperRifle = 5; 32 | EWT_MachineGun = 6; 33 | EWT_C4 = 7; 34 | EWT_Grenade = 8; 35 | EWT_Equipment = 9; 36 | EWT_StackableItem = 10; 37 | EWT_Unknown = 11; 38 | } 39 | 40 | message MLDict { 41 | optional string key = 1; 42 | optional string val_string = 2; 43 | optional int32 val_int = 3; 44 | optional float val_float = 4; 45 | } 46 | 47 | message MLEvent { 48 | optional string event_name = 1; 49 | repeated MLDict data = 2; 50 | } 51 | 52 | message MLMatchState { 53 | optional string game_mode = 1; 54 | optional string phase = 2; 55 | optional int32 round = 3; 56 | optional int32 score_ct = 4; 57 | optional int32 score_t = 5; 58 | } 59 | 60 | message MLRoundState { 61 | optional string phase = 1; 62 | optional ETeam win_team = 2 [default = ET_Unknown]; 63 | optional string bomb_state = 3; 64 | } 65 | 66 | message MLWeaponState { 67 | optional int32 index = 1; 68 | optional string name = 2; 69 | optional EWeaponType type = 3 [default = EWT_Knife]; 70 | optional int32 ammo_clip = 4; 71 | optional int32 ammo_clip_max = 5; 72 | optional int32 ammo_reserve = 6; 73 | optional string state = 7; 74 | optional float recoil_index = 8; 75 | } 76 | 77 | message MLPlayerState { 78 | optional int32 account_id = 1; 79 | optional int32 player_slot = 2 [default = -1]; 80 | optional int32 entindex = 3; 81 | optional string name = 4; 82 | optional string clan = 5; 83 | optional ETeam team = 6 [default = ET_Unknown]; 84 | optional CMsgVector abspos = 7; 85 | optional CMsgQAngle eyeangle = 8; 86 | optional CMsgVector eyeangle_fwd = 9; 87 | optional int32 health = 10; 88 | optional int32 armor = 11; 89 | optional float flashed = 12; 90 | optional float smoked = 13; 91 | optional int32 money = 14; 92 | optional int32 round_kills = 15; 93 | optional int32 round_killhs = 16; 94 | optional float burning = 17; 95 | optional bool helmet = 18; 96 | optional bool defuse_kit = 19; 97 | repeated MLWeaponState weapons = 20; 98 | } 99 | 100 | message MLGameState { 101 | optional MLMatchState match = 1; 102 | optional MLRoundState round = 2; 103 | repeated MLPlayerState players = 3; 104 | } 105 | 106 | message MLDemoHeader { 107 | optional string map_name = 1; 108 | optional int32 tick_rate = 2; 109 | optional uint32 version = 3; 110 | optional uint32 steam_universe = 4; 111 | } 112 | 113 | message MLTick { 114 | optional int32 tick_count = 1; 115 | optional MLGameState state = 2; 116 | repeated MLEvent events = 3; 117 | } 118 | 119 | message VacNetShot { 120 | optional fixed64 steamid_player = 1; 121 | optional int32 round_number = 2; 122 | optional int32 hit_type = 3; 123 | optional int32 weapon_type = 4; 124 | optional float distance_to_hurt_target = 5; 125 | repeated float delta_yaw_window = 6; 126 | repeated float delta_pitch_window = 7; 127 | } 128 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/gameevents.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/networkbasetypes.proto"; 4 | 5 | enum EBaseGameEvents { 6 | GE_VDebugGameSessionIDEvent = 200; 7 | GE_PlaceDecalEvent = 201; 8 | GE_ClearWorldDecalsEvent = 202; 9 | GE_ClearEntityDecalsEvent = 203; 10 | GE_ClearDecalsForSkeletonInstanceEvent = 204; 11 | GE_Source1LegacyGameEventList = 205; 12 | GE_Source1LegacyListenEvents = 206; 13 | GE_Source1LegacyGameEvent = 207; 14 | GE_SosStartSoundEvent = 208; 15 | GE_SosStopSoundEvent = 209; 16 | GE_SosSetSoundEventParams = 210; 17 | GE_SosSetLibraryStackFields = 211; 18 | GE_SosStopSoundEventHash = 212; 19 | } 20 | 21 | message CMsgVDebugGameSessionIDEvent { 22 | optional int32 clientid = 1; 23 | optional string gamesessionid = 2; 24 | } 25 | 26 | message CMsgPlaceDecalEvent { 27 | optional CMsgVector position = 1; 28 | optional CMsgVector normal = 2; 29 | optional CMsgVector saxis = 3; 30 | optional uint32 decalmaterialindex = 4; 31 | optional uint32 flags = 5; 32 | optional fixed32 color = 6; 33 | optional float width = 7; 34 | optional float height = 8; 35 | optional float depth = 9; 36 | optional uint32 entityhandleindex = 10; 37 | optional fixed32 skeletoninstancehash = 11; 38 | optional int32 boneindex = 12; 39 | optional bool translucenthit = 13; 40 | optional bool is_adjacent = 14; 41 | } 42 | 43 | message CMsgClearWorldDecalsEvent { 44 | optional uint32 flagstoclear = 1; 45 | } 46 | 47 | message CMsgClearEntityDecalsEvent { 48 | optional uint32 flagstoclear = 1; 49 | } 50 | 51 | message CMsgClearDecalsForSkeletonInstanceEvent { 52 | optional uint32 flagstoclear = 1; 53 | optional uint32 entityhandleindex = 2; 54 | optional uint32 skeletoninstancehash = 3; 55 | } 56 | 57 | message CMsgSource1LegacyGameEventList { 58 | message key_t { 59 | optional int32 type = 1; 60 | optional string name = 2; 61 | } 62 | 63 | message descriptor_t { 64 | optional int32 eventid = 1; 65 | optional string name = 2; 66 | repeated CMsgSource1LegacyGameEventList.key_t keys = 3; 67 | } 68 | 69 | repeated CMsgSource1LegacyGameEventList.descriptor_t descriptors = 1; 70 | } 71 | 72 | message CMsgSource1LegacyListenEvents { 73 | optional int32 playerslot = 1; 74 | repeated uint32 eventarraybits = 2; 75 | } 76 | 77 | message CMsgSource1LegacyGameEvent { 78 | message key_t { 79 | optional int32 type = 1; 80 | optional string val_string = 2; 81 | optional float val_float = 3; 82 | optional int32 val_long = 4; 83 | optional int32 val_short = 5; 84 | optional int32 val_byte = 6; 85 | optional bool val_bool = 7; 86 | optional uint64 val_uint64 = 8; 87 | } 88 | 89 | optional string event_name = 1; 90 | optional int32 eventid = 2; 91 | repeated CMsgSource1LegacyGameEvent.key_t keys = 3; 92 | optional int32 server_tick = 4; 93 | optional int32 passthrough = 5; 94 | } 95 | 96 | message CMsgSosStartSoundEvent { 97 | optional int32 soundevent_guid = 1; 98 | optional fixed32 soundevent_hash = 2; 99 | optional int32 source_entity_index = 3 [default = -1]; 100 | optional int32 seed = 4; 101 | optional bytes packed_params = 5; 102 | optional float start_time = 6; 103 | } 104 | 105 | message CMsgSosStopSoundEvent { 106 | optional int32 soundevent_guid = 1; 107 | } 108 | 109 | message CMsgSosStopSoundEventHash { 110 | optional fixed32 soundevent_hash = 1; 111 | optional int32 source_entity_index = 2 [default = -1]; 112 | } 113 | 114 | message CMsgSosSetSoundEventParams { 115 | optional int32 soundevent_guid = 1; 116 | optional bytes packed_params = 5; 117 | } 118 | 119 | message CMsgSosSetLibraryStackFields { 120 | optional fixed32 stack_hash = 1; 121 | optional bytes packed_fields = 5; 122 | } 123 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/networksystem_protomessages.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | message NetMessageSplitscreenUserChanged { 4 | optional uint32 slot = 1; 5 | } 6 | 7 | message NetMessageConnectionClosed { 8 | optional uint32 reason = 1; 9 | optional string message = 2; 10 | } 11 | 12 | message NetMessageConnectionCrashed { 13 | optional uint32 reason = 1; 14 | optional string message = 2; 15 | } 16 | 17 | message NetMessagePacketStart { 18 | } 19 | 20 | message NetMessagePacketEnd { 21 | } 22 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/steamdatagram_messages_auth.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/steamnetworkingsockets_messages_certs.proto"; 4 | 5 | option optimize_for = SPEED; 6 | option cc_generic_services = false; 7 | 8 | message CMsgSteamDatagramRelayAuthTicket { 9 | message ExtraField { 10 | optional string name = 1; 11 | optional string string_value = 2; 12 | optional sint64 int64_value = 3; 13 | optional fixed64 fixed64_value = 5; 14 | } 15 | 16 | optional fixed32 time_expiry = 1; 17 | optional string authorized_client_identity_string = 14; 18 | optional string gameserver_identity_string = 15; 19 | optional fixed32 authorized_public_ip = 3; 20 | optional bytes gameserver_address = 11; 21 | optional uint32 app_id = 7; 22 | optional uint32 virtual_port = 10; 23 | repeated CMsgSteamDatagramRelayAuthTicket.ExtraField extra_fields = 8; 24 | optional fixed64 legacy_authorized_steam_id = 2; 25 | optional fixed64 legacy_gameserver_steam_id = 4; 26 | optional fixed32 legacy_gameserver_pop_id = 9; 27 | optional bytes legacy_authorized_client_identity_binary = 12; 28 | optional bytes legacy_gameserver_identity_binary = 13; 29 | } 30 | 31 | message CMsgSteamDatagramSignedRelayAuthTicket { 32 | optional fixed64 reserved_do_not_use = 1; 33 | optional bytes ticket = 3; 34 | optional bytes signature = 4; 35 | optional fixed64 key_id = 2; 36 | repeated CMsgSteamDatagramCertificateSigned certs = 5; 37 | } 38 | 39 | message CMsgSteamDatagramCachedCredentialsForApp { 40 | optional bytes private_key = 1; 41 | optional bytes cert = 2; 42 | repeated bytes relay_tickets = 3; 43 | } 44 | 45 | message CMsgSteamDatagramGameCoordinatorServerLogin { 46 | optional uint32 time_generated = 1; 47 | optional uint32 appid = 2; 48 | optional bytes routing = 3; 49 | optional bytes appdata = 4; 50 | optional bytes legacy_identity_binary = 5; 51 | optional string identity_string = 6; 52 | optional fixed64 dummy_steam_id = 99; 53 | } 54 | 55 | message CMsgSteamDatagramSignedGameCoordinatorServerLogin { 56 | optional CMsgSteamDatagramCertificateSigned cert = 1; 57 | optional bytes login = 2; 58 | optional bytes signature = 3; 59 | } 60 | 61 | message CMsgSteamDatagramHostedServerAddressPlaintext { 62 | optional fixed32 ipv4 = 1; 63 | optional bytes ipv6 = 2; 64 | optional uint32 port = 3; 65 | optional fixed64 routing_secret = 4; 66 | optional uint32 protocol_version = 5; 67 | } 68 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/steammessages.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "google/protobuf/descriptor.proto"; 4 | 5 | extend .google.protobuf.FieldOptions { 6 | optional bool key_field = 70000 [default = false]; 7 | } 8 | 9 | extend .google.protobuf.MessageOptions { 10 | optional int32 msgpool_soft_limit = 70000 [default = 32]; 11 | optional int32 msgpool_hard_limit = 70001 [default = 384]; 12 | } 13 | 14 | enum GCProtoBufMsgSrc { 15 | GCProtoBufMsgSrc_Unspecified = 0; 16 | GCProtoBufMsgSrc_FromSystem = 1; 17 | GCProtoBufMsgSrc_FromSteamID = 2; 18 | GCProtoBufMsgSrc_FromGC = 3; 19 | GCProtoBufMsgSrc_ReplySystem = 4; 20 | } 21 | 22 | message CMsgProtoBufHeader { 23 | option (msgpool_soft_limit) = 256; 24 | option (msgpool_hard_limit) = 1024; 25 | 26 | optional fixed64 client_steam_id = 1; 27 | optional int32 client_session_id = 2; 28 | optional uint32 source_app_id = 3; 29 | optional fixed64 job_id_source = 10 [default = 18446744073709551615]; 30 | optional fixed64 job_id_target = 11 [default = 18446744073709551615]; 31 | optional string target_job_name = 12; 32 | optional int32 eresult = 13 [default = 2]; 33 | optional string error_message = 14; 34 | optional uint32 ip = 15; 35 | optional GCProtoBufMsgSrc gc_msg_src = 200 [default = GCProtoBufMsgSrc_Unspecified]; 36 | optional uint32 gc_dir_index_source = 201; 37 | } 38 | 39 | message CChinaAgreementSessions_StartAgreementSessionInGame_Request { 40 | optional uint32 appid = 1; 41 | optional fixed64 steamid = 2; 42 | optional string client_ipaddress = 3; 43 | } 44 | 45 | message CChinaAgreementSessions_StartAgreementSessionInGame_Response { 46 | optional string agreement_url = 1; 47 | } 48 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/steammessages_cloud.steamworkssdk.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/steammessages_unified_base.steamworkssdk.proto"; 4 | 5 | message CCloud_GetUploadServerInfo_Request { 6 | optional uint32 appid = 1 [(description) = "App ID to which a file will be uploaded to."]; 7 | } 8 | 9 | message CCloud_GetUploadServerInfo_Response { 10 | optional string server_url = 1; 11 | } 12 | 13 | message CCloud_GetFileDetails_Request { 14 | optional uint64 ugcid = 1 [(description) = "ID of the Cloud file to get details for."]; 15 | optional uint32 appid = 2 [(description) = "App ID the file belongs to."]; 16 | } 17 | 18 | message CCloud_UserFile { 19 | optional uint32 appid = 1; 20 | optional uint64 ugcid = 2; 21 | optional string filename = 3; 22 | optional uint64 timestamp = 4; 23 | optional uint32 file_size = 5; 24 | optional string url = 6; 25 | optional fixed64 steamid_creator = 7; 26 | } 27 | 28 | message CCloud_GetFileDetails_Response { 29 | optional CCloud_UserFile details = 1; 30 | } 31 | 32 | message CCloud_EnumerateUserFiles_Request { 33 | optional uint32 appid = 1 [(description) = "App ID to enumerate the files of."]; 34 | optional bool extended_details = 2 [(description) = "(Optional) Get extended details back on the files found. Defaults to only returned the app Id and UGC Id of the files found."]; 35 | optional uint32 count = 3 [(description) = "(Optional) Maximum number of results to return on this call. Defaults to a maximum of 500 files returned."]; 36 | optional uint32 start_index = 4 [(description) = "(Optional) Starting index to begin enumeration at. Defaults to the beginning of the list."]; 37 | } 38 | 39 | message CCloud_EnumerateUserFiles_Response { 40 | repeated CCloud_UserFile files = 1; 41 | optional uint32 total_files = 2; 42 | } 43 | 44 | message CCloud_Delete_Request { 45 | optional string filename = 1; 46 | optional uint32 appid = 2 [(description) = "App ID the file belongs to."]; 47 | } 48 | 49 | message CCloud_Delete_Response { 50 | } 51 | 52 | service Cloud { 53 | option (service_description) = "A service for Steam Cloud operations."; 54 | 55 | rpc GetUploadServerInfo (.CCloud_GetUploadServerInfo_Request) returns (.CCloud_GetUploadServerInfo_Response) { 56 | option (method_description) = "Returns the URL of the proper cloud server for a user."; 57 | } 58 | 59 | rpc GetFileDetails (.CCloud_GetFileDetails_Request) returns (.CCloud_GetFileDetails_Response) { 60 | option (method_description) = "Returns details on a Cloud file."; 61 | } 62 | 63 | rpc EnumerateUserFiles (.CCloud_EnumerateUserFiles_Request) returns (.CCloud_EnumerateUserFiles_Response) { 64 | option (method_description) = "Enumerates Cloud files for a user of a given app ID. Returns up to 500 files at a time."; 65 | } 66 | 67 | rpc Delete (.CCloud_Delete_Request) returns (.CCloud_Delete_Response) { 68 | option (method_description) = "Deletes a file from the user's cloud."; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/steammessages_gamenetworkingui.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/steamnetworkingsockets_messages.proto"; 4 | import "s2/steamdatagram_messages_sdr.proto"; 5 | 6 | option optimize_for = SPEED; 7 | option cc_generic_services = true; 8 | 9 | message CGameNetworkingUI_GlobalState { 10 | } 11 | 12 | message CGameNetworkingUI_ConnectionState { 13 | optional string connection_key = 1; 14 | optional uint32 appid = 2; 15 | optional fixed32 connection_id_local = 3; 16 | optional string identity_local = 4; 17 | optional string identity_remote = 5; 18 | optional uint32 connection_state = 10; 19 | optional uint32 start_time = 12; 20 | optional uint32 close_time = 13; 21 | optional uint32 close_reason = 14; 22 | optional string close_message = 15; 23 | optional string status_loc_token = 16; 24 | optional uint32 transport_kind = 20; 25 | optional string sdrpopid_local = 21; 26 | optional string sdrpopid_remote = 22; 27 | optional string address_remote = 23; 28 | optional CMsgSteamDatagramP2PRoutingSummary p2p_routing = 24; 29 | optional uint32 ping_interior = 25; 30 | optional uint32 ping_remote_front = 26; 31 | optional uint32 ping_default_internet_route = 27; 32 | optional CMsgSteamDatagramConnectionQuality e2e_quality_local = 30; 33 | optional CMsgSteamDatagramConnectionQuality e2e_quality_remote = 31; 34 | optional uint64 e2e_quality_remote_instantaneous_time = 32; 35 | optional uint64 e2e_quality_remote_lifetime_time = 33; 36 | optional CMsgSteamDatagramConnectionQuality front_quality_local = 40; 37 | optional CMsgSteamDatagramConnectionQuality front_quality_remote = 41; 38 | optional uint64 front_quality_remote_instantaneous_time = 42; 39 | optional uint64 front_quality_remote_lifetime_time = 43; 40 | } 41 | 42 | message CGameNetworkingUI_Message { 43 | repeated CGameNetworkingUI_ConnectionState connection_state = 1; 44 | } 45 | 46 | message CGameNetworkingUI_ConnectionSummary { 47 | optional uint32 transport_kind = 1; 48 | optional uint32 connection_state = 8; 49 | optional string sdrpop_local = 2; 50 | optional string sdrpop_remote = 3; 51 | optional uint32 ping_ms = 4; 52 | optional float packet_loss = 5; 53 | optional uint32 ping_default_internet_route = 6; 54 | optional bool ip_was_shared = 7; 55 | } 56 | 57 | message CGameNetworkingUI_AppSummary { 58 | optional uint32 appid = 1; 59 | optional bool ip_was_shared_with_friend = 10; 60 | optional bool ip_was_shared_with_nonfriend = 11; 61 | optional uint32 active_connections = 20; 62 | optional CGameNetworkingUI_ConnectionSummary main_cxn = 30; 63 | } 64 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/steammessages_helprequest.steamworkssdk.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/steammessages_unified_base.steamworkssdk.proto"; 4 | 5 | option cc_generic_services = true; 6 | 7 | message CHelpRequestLogs_UploadUserApplicationLog_Request { 8 | optional uint32 appid = 1; 9 | optional string log_type = 2; 10 | optional string version_string = 3; 11 | optional string log_contents = 4; 12 | } 13 | 14 | message CHelpRequestLogs_UploadUserApplicationLog_Response { 15 | optional uint64 id = 1; 16 | } 17 | 18 | service HelpRequestLogs { 19 | option (service_description) = "Service for dealing with user-submitted logs"; 20 | 21 | rpc UploadUserApplicationLog (.CHelpRequestLogs_UploadUserApplicationLog_Request) returns (.CHelpRequestLogs_UploadUserApplicationLog_Response) { 22 | option (method_description) = "User uploading application logs"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/steammessages_oauth.steamworkssdk.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/steammessages_unified_base.steamworkssdk.proto"; 4 | 5 | message COAuthToken_ImplicitGrantNoPrompt_Request { 6 | optional string clientid = 1 [(description) = "Client ID for which to count the number of issued tokens"]; 7 | } 8 | 9 | message COAuthToken_ImplicitGrantNoPrompt_Response { 10 | optional string access_token = 1 [(description) = "OAuth Token, granted on success"]; 11 | optional string redirect_uri = 2 [(description) = "Redirection URI provided during client registration."]; 12 | } 13 | 14 | service OAuthToken { 15 | option (service_description) = "Service containing methods to manage OAuth tokens"; 16 | 17 | rpc ImplicitGrantNoPrompt (.COAuthToken_ImplicitGrantNoPrompt_Request) returns (.COAuthToken_ImplicitGrantNoPrompt_Response) { 18 | option (method_description) = "Grants an implicit OAuth token (grant type 'token') for the specified client ID on behalf of a user without prompting"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/steammessages_unified_base.steamworkssdk.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "google/protobuf/descriptor.proto"; 4 | 5 | option optimize_for = SPEED; 6 | option cc_generic_services = false; 7 | 8 | extend .google.protobuf.FieldOptions { 9 | optional string description = 50000; 10 | } 11 | 12 | extend .google.protobuf.ServiceOptions { 13 | optional string service_description = 50000; 14 | optional EProtoExecutionSite service_execution_site = 50008 [default = k_EProtoExecutionSiteUnknown]; 15 | } 16 | 17 | extend .google.protobuf.MethodOptions { 18 | optional string method_description = 50000; 19 | } 20 | 21 | extend .google.protobuf.EnumOptions { 22 | optional string enum_description = 50000; 23 | } 24 | 25 | extend .google.protobuf.EnumValueOptions { 26 | optional string enum_value_description = 50000; 27 | } 28 | 29 | enum EProtoExecutionSite { 30 | k_EProtoExecutionSiteUnknown = 0; 31 | k_EProtoExecutionSiteSteamClient = 3; 32 | } 33 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/steamnetworkingsockets_messages_certs.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | option optimize_for = SPEED; 4 | option cc_generic_services = false; 5 | 6 | message CMsgSteamNetworkingIdentityLegacyBinary { 7 | optional fixed64 steam_id = 16; 8 | optional bytes generic_bytes = 2; 9 | optional string generic_string = 3; 10 | optional bytes ipv6_and_port = 4; 11 | } 12 | 13 | message CMsgSteamDatagramCertificate { 14 | enum EKeyType { 15 | INVALID = 0; 16 | ED25519 = 1; 17 | } 18 | 19 | optional CMsgSteamDatagramCertificate.EKeyType key_type = 1 [default = INVALID]; 20 | optional bytes key_data = 2; 21 | optional fixed64 legacy_steam_id = 4; 22 | optional CMsgSteamNetworkingIdentityLegacyBinary legacy_identity_binary = 11; 23 | optional string identity_string = 12; 24 | repeated fixed32 gameserver_datacenter_ids = 5; 25 | optional fixed32 time_created = 8; 26 | optional fixed32 time_expiry = 9; 27 | repeated uint32 app_ids = 10; 28 | repeated string ip_addresses = 13; 29 | } 30 | 31 | message CMsgSteamDatagramCertificateSigned { 32 | optional bytes cert = 4; 33 | optional fixed64 ca_key_id = 5; 34 | optional bytes ca_signature = 6; 35 | optional bytes private_key_data = 1; 36 | } 37 | 38 | message CMsgSteamDatagramCertificateRequest { 39 | optional CMsgSteamDatagramCertificate cert = 1; 40 | } 41 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/steamnetworkingsockets_messages_udp.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/steamnetworkingsockets_messages_certs.proto"; 4 | import "s2/steamnetworkingsockets_messages.proto"; 5 | 6 | option optimize_for = SPEED; 7 | option cc_generic_services = false; 8 | 9 | enum ESteamNetworkingUDPMsgID { 10 | k_ESteamNetworkingUDPMsg_ChallengeRequest = 32; 11 | k_ESteamNetworkingUDPMsg_ChallengeReply = 33; 12 | k_ESteamNetworkingUDPMsg_ConnectRequest = 34; 13 | k_ESteamNetworkingUDPMsg_ConnectOK = 35; 14 | k_ESteamNetworkingUDPMsg_ConnectionClosed = 36; 15 | k_ESteamNetworkingUDPMsg_NoConnection = 37; 16 | } 17 | 18 | message CMsgSteamSockets_UDP_ChallengeRequest { 19 | optional fixed32 connection_id = 1; 20 | optional fixed64 my_timestamp = 3; 21 | optional uint32 protocol_version = 4; 22 | } 23 | 24 | message CMsgSteamSockets_UDP_ChallengeReply { 25 | optional fixed32 connection_id = 1; 26 | optional fixed64 challenge = 2; 27 | optional fixed64 your_timestamp = 3; 28 | optional uint32 protocol_version = 4; 29 | } 30 | 31 | message CMsgSteamSockets_UDP_ConnectRequest { 32 | optional fixed32 client_connection_id = 1; 33 | optional fixed64 challenge = 2; 34 | optional fixed64 my_timestamp = 5; 35 | optional uint32 ping_est_ms = 6; 36 | optional CMsgSteamDatagramSessionCryptInfoSigned crypt = 7; 37 | optional CMsgSteamDatagramCertificateSigned cert = 4; 38 | optional uint32 legacy_protocol_version = 8; 39 | optional string identity_string = 10; 40 | optional fixed64 legacy_client_steam_id = 3; 41 | optional CMsgSteamNetworkingIdentityLegacyBinary legacy_identity_binary = 9; 42 | } 43 | 44 | message CMsgSteamSockets_UDP_ConnectOK { 45 | optional fixed32 client_connection_id = 1; 46 | optional fixed32 server_connection_id = 5; 47 | optional fixed64 your_timestamp = 3; 48 | optional uint32 delay_time_usec = 4; 49 | optional CMsgSteamDatagramSessionCryptInfoSigned crypt = 7; 50 | optional CMsgSteamDatagramCertificateSigned cert = 8; 51 | optional string identity_string = 11; 52 | optional fixed64 legacy_server_steam_id = 2; 53 | optional CMsgSteamNetworkingIdentityLegacyBinary legacy_identity_binary = 10; 54 | } 55 | 56 | message CMsgSteamSockets_UDP_ConnectionClosed { 57 | optional fixed32 to_connection_id = 4; 58 | optional fixed32 from_connection_id = 5; 59 | optional string debug = 2; 60 | optional uint32 reason_code = 3; 61 | } 62 | 63 | message CMsgSteamSockets_UDP_NoConnection { 64 | optional fixed32 from_connection_id = 2; 65 | optional fixed32 to_connection_id = 3; 66 | } 67 | 68 | message CMsgSteamSockets_UDP_Stats { 69 | enum Flags { 70 | ACK_REQUEST_E2E = 2; 71 | ACK_REQUEST_IMMEDIATE = 4; 72 | NOT_PRIMARY_TRANSPORT_E2E = 16; 73 | } 74 | 75 | optional CMsgSteamDatagramConnectionQuality stats = 1; 76 | optional uint32 flags = 3; 77 | } 78 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/uifontfile_format.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | message CUIFontFilePB { 4 | optional string font_file_name = 1; 5 | optional bytes opentype_font_data = 2; 6 | } 7 | 8 | message CUIFontFilePackagePB { 9 | message CUIEncryptedFontFilePB { 10 | optional bytes encrypted_contents = 1; 11 | } 12 | 13 | required uint32 package_version = 1; 14 | repeated CUIFontFilePackagePB.CUIEncryptedFontFilePB encrypted_font_files = 2; 15 | } 16 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/usercmd.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "s2/networkbasetypes.proto"; 4 | 5 | message CInButtonStatePB { 6 | optional uint64 buttonstate1 = 1; 7 | optional uint64 buttonstate2 = 2; 8 | optional uint64 buttonstate3 = 3; 9 | } 10 | 11 | message CSubtickMoveStep { 12 | optional uint64 button = 1; 13 | optional bool pressed = 2; 14 | optional float when = 3; 15 | optional float analog_forward_delta = 4; 16 | optional float analog_left_delta = 5; 17 | } 18 | 19 | message CBaseUserCmdPB { 20 | optional int32 legacy_command_number = 1; 21 | optional int32 client_tick = 2; 22 | optional CInButtonStatePB buttons_pb = 3; 23 | optional CMsgQAngle viewangles = 4; 24 | optional float forwardmove = 5; 25 | optional float leftmove = 6; 26 | optional float upmove = 7; 27 | optional int32 impulse = 8; 28 | optional int32 weaponselect = 9; 29 | optional int32 random_seed = 10; 30 | optional int32 mousedx = 11; 31 | optional int32 mousedy = 12; 32 | optional uint32 pawn_entity_handle = 14 [default = 16777215]; 33 | repeated CSubtickMoveStep subtick_moves = 18; 34 | optional bytes move_crc = 19; 35 | optional uint32 consumed_server_angle_changes = 20; 36 | optional int32 cmd_flags = 21; 37 | } 38 | 39 | message CUserCmdBasePB { 40 | optional CBaseUserCmdPB base = 1; 41 | } 42 | -------------------------------------------------------------------------------- /pkg/demoinfocs/msgs2/proto/s2/valveextensions.proto: -------------------------------------------------------------------------------- 1 | package com.github.markus_wa.demoinfocs_golang.s2; 2 | 3 | import "google/protobuf/descriptor.proto"; 4 | 5 | extend .google.protobuf.FieldOptions { 6 | optional bool valve_map_field = 61000 [default = false]; 7 | optional bool valve_map_key = 61001 [default = false]; 8 | optional int32 diff_encode_field = 61002 [default = 0]; 9 | optional bool delta_ignore = 61003 [default = false]; 10 | optional uint32 steamml_max_entries = 61004 [default = 0]; 11 | optional bool steamml_is_timestamp = 61005 [default = false]; 12 | optional uint32 steamlearn_count = 61006 [default = 0]; 13 | } 14 | 15 | extend .google.protobuf.EnumValueOptions { 16 | optional string schema_friendly_name = 1000; 17 | optional string schema_description = 1001; 18 | optional bool schema_suppress_enumerator = 1002; 19 | } 20 | -------------------------------------------------------------------------------- /pkg/demoinfocs/parser_test.go: -------------------------------------------------------------------------------- 1 | package demoinfocs 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "math" 8 | "testing" 9 | "time" 10 | 11 | dispatch "github.com/markus-wa/godispatch" 12 | "github.com/stretchr/testify/assert" 13 | 14 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 15 | ) 16 | 17 | func TestParser_CurrentFrame(t *testing.T) { 18 | assert.Equal(t, 1, (&parser{currentFrame: 1}).CurrentFrame()) 19 | } 20 | 21 | func TestParser_GameState(t *testing.T) { 22 | gs := new(gameState) 23 | assert.Equal(t, gs, (&parser{gameState: gs}).GameState()) 24 | } 25 | 26 | func TestParser_CurrentTime(t *testing.T) { 27 | p := &parser{ 28 | tickInterval: 2, 29 | gameState: &gameState{ingameTick: 3}, 30 | } 31 | 32 | assert.Equal(t, 6*time.Second, p.CurrentTime()) 33 | } 34 | 35 | func TestParser_TickRate(t *testing.T) { 36 | assert.Equal(t, float64(5), math.Round((&parser{tickInterval: 0.2}).TickRate())) 37 | } 38 | 39 | func TestParser_TickRate_FallbackToHeader(t *testing.T) { 40 | p := &parser{ 41 | header: &common.DemoHeader{ 42 | PlaybackTime: time.Second, 43 | PlaybackTicks: 5, 44 | }, 45 | } 46 | 47 | assert.Equal(t, float64(5), p.TickRate()) 48 | } 49 | 50 | func TestParser_TickTime(t *testing.T) { 51 | assert.Equal(t, time.Duration(200)*time.Millisecond, (&parser{tickInterval: 0.2}).TickTime()) 52 | } 53 | 54 | func TestParser_TickTime_FallbackToHeader(t *testing.T) { 55 | p := &parser{ 56 | header: &common.DemoHeader{ 57 | PlaybackTime: time.Second, 58 | PlaybackTicks: 5, 59 | }, 60 | } 61 | 62 | assert.Equal(t, time.Duration(200)*time.Millisecond, p.TickTime()) 63 | } 64 | 65 | func TestParser_Progress_NoHeader(t *testing.T) { 66 | assert.Zero(t, new(parser).Progress()) 67 | assert.Zero(t, (&parser{header: &common.DemoHeader{}}).Progress()) 68 | } 69 | 70 | func TestRecoverFromUnexpectedEOF(t *testing.T) { 71 | assert.Nil(t, recoverFromUnexpectedEOF(nil)) 72 | assert.ErrorIs(t, recoverFromUnexpectedEOF(io.ErrUnexpectedEOF), ErrUnexpectedEndOfDemo) 73 | assert.ErrorIs(t, recoverFromUnexpectedEOF(io.EOF), ErrUnexpectedEndOfDemo) 74 | 75 | assert.Panics(t, func() { 76 | r := recoverFromUnexpectedEOF(errors.New("test")) 77 | assert.Failf(t, "expected panic, got recovery", "recovered value = '%v'", r) 78 | }) 79 | } 80 | 81 | type consumerCodePanicMock struct { 82 | value any 83 | } 84 | 85 | func (ucp consumerCodePanicMock) String() string { 86 | return fmt.Sprint(ucp.value) 87 | } 88 | 89 | func (ucp consumerCodePanicMock) Value() any { 90 | return ucp.value 91 | } 92 | 93 | func TestRecoverFromPanic_ConsumerCodePanic(t *testing.T) { 94 | assert.PanicsWithValue(t, 1, func() { 95 | err := recoverFromUnexpectedEOF(consumerCodePanicMock{value: 1}) 96 | assert.Nil(t, err) 97 | }) 98 | } 99 | 100 | func TestParser_SetError(t *testing.T) { 101 | err := errors.New("test") 102 | 103 | p := new(parser) 104 | p.setError(err) 105 | 106 | assert.Same(t, err, p.error()) 107 | } 108 | 109 | func TestParser_SetError_Multiple(t *testing.T) { 110 | err := errors.New("test") 111 | 112 | p := new(parser) 113 | p.setError(err) 114 | p.setError(errors.New("second error")) 115 | 116 | assert.Same(t, err, p.error()) 117 | } 118 | 119 | func TestParser_Close(t *testing.T) { 120 | p := new(parser) 121 | q := make(chan any, 1) 122 | 123 | p.msgDispatcher = new(dispatch.Dispatcher) 124 | p.msgDispatcher.AddQueues(q) 125 | 126 | called := false 127 | p.msgDispatcher.RegisterHandler(func(any) { 128 | called = true 129 | }) 130 | 131 | err := p.Close() 132 | assert.NoError(t, err) 133 | 134 | q <- "this should not trigger the handler" 135 | 136 | p.msgDispatcher.SyncAllQueues() 137 | 138 | assert.False(t, called) 139 | } 140 | -------------------------------------------------------------------------------- /pkg/demoinfocs/participants_interface.go: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: Auto generated 2 | 3 | package demoinfocs 4 | 5 | import ( 6 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 7 | ) 8 | 9 | // Participants is an auto-generated interface for participants. 10 | // participants provides helper functions on top of the currently connected players. 11 | // E.g. ByUserID(), ByEntityID(), TeamMembers(), etc. 12 | // 13 | // See GameState.Participants() 14 | type Participants interface { 15 | // ByUserID returns all currently connected players in a map where the key is the user-ID. 16 | // The returned map is a snapshot and is not updated on changes (not a reference to the actual, underlying map). 17 | // Includes spectators. 18 | ByUserID() map[int]*common.Player 19 | // ByEntityID returns all currently connected players in a map where the key is the entity-ID. 20 | // The returned map is a snapshot and is not updated on changes (not a reference to the actual, underlying map). 21 | // Includes spectators. 22 | ByEntityID() map[int]*common.Player 23 | // AllByUserID returns all currently known players & spectators, including disconnected ones, 24 | // in a map where the key is the user-ID. 25 | // The returned map is a snapshot and is not updated on changes (not a reference to the actual, underlying map). 26 | // Includes spectators. 27 | AllByUserID() map[int]*common.Player 28 | // All returns all currently known players & spectators, including disconnected ones, of the demo. 29 | // The returned slice is a snapshot and is not updated on changes. 30 | All() []*common.Player 31 | // Connected returns all currently connected players & spectators. 32 | // The returned slice is a snapshot and is not updated on changes. 33 | Connected() []*common.Player 34 | // Playing returns all players that aren't spectating or unassigned. 35 | // The returned slice is a snapshot and is not updated on changes. 36 | Playing() []*common.Player 37 | // TeamMembers returns all players belonging to the requested team at this time. 38 | // The returned slice is a snapshot and is not updated on changes. 39 | TeamMembers(team common.Team) []*common.Player 40 | // FindByPawnHandle attempts to find a player by his pawn entity-handle. 41 | // This works only for Source 2 demos. 42 | // 43 | // Returns nil if not found. 44 | FindByPawnHandle(handle uint64) *common.Player 45 | // FindByHandle64 attempts to find a player by his entity-handle. 46 | // The entity-handle is often used in entity-properties when referencing other entities such as a weapon's owner. 47 | // 48 | // Returns nil if not found or if handle == invalidEntityHandle (used when referencing no entity). 49 | FindByHandle64(handle uint64) *common.Player 50 | // FindByHandle attempts to find a player by his entity-handle. 51 | // The entity-handle is often used in entity-properties when referencing other entities such as a weapon's owner. 52 | // 53 | // Returns nil if not found or if handle == invalidEntityHandle (used when referencing no entity). 54 | // 55 | // Deprecated: Use FindByHandle64 instead. 56 | FindByHandle(handle int) *common.Player 57 | // SpottersOf returns a list of all players who have spotted the passed player. 58 | // This is NOT "Line of Sight" / FOV - look up "CSGO TraceRay" for that. 59 | // May not behave as expected with multiple spotters. 60 | SpottersOf(spotted *common.Player) (spotters []*common.Player) 61 | // SpottedBy returns a list of all players that the passed player has spotted. 62 | // This is NOT "Line of Sight" / FOV - look up "CSGO TraceRay" for that. 63 | // May not behave as expected with multiple spotters. 64 | SpottedBy(spotter *common.Player) (spotted []*common.Player) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables/entity_interface.go: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: Auto generated 2 | 3 | package sendtables 4 | 5 | import ( 6 | "github.com/golang/geo/r3" 7 | bit "github.com/markus-wa/demoinfocs-golang/v4/internal/bitread" 8 | ) 9 | 10 | // Entity is an auto-generated interface for entity, intended to be used when mockability is needed. 11 | // entity stores a entity in the game (e.g. players etc.) with its properties. 12 | type Entity interface { 13 | // ServerClass returns the entity's server-class. 14 | ServerClass() ServerClass 15 | // ID returns the entity's ID. 16 | ID() int 17 | // SerialNum returns the entity's serial number. 18 | SerialNum() int 19 | // Properties returns all properties of the entity. 20 | Properties() (out []Property) 21 | // Property finds a property on the entity by name. 22 | // 23 | // Returns nil if the property wasn't found. 24 | Property(name string) Property 25 | // BindProperty combines Property() & Property.Bind() into one. 26 | // Essentially binds a property's value to a pointer. 27 | // See the docs of the two individual functions for more info. 28 | BindProperty(name string, variable any, valueType PropertyValueType) 29 | // PropertyValue finds a property on the entity by name and returns its value. 30 | // 31 | // Returns false as second value if the property was not found. 32 | PropertyValue(name string) (PropertyValue, bool) 33 | // PropertyValueMust finds a property on the entity by name and returns its value. 34 | // 35 | // Panics with nil pointer dereference error if the property was not found. 36 | PropertyValueMust(name string) PropertyValue 37 | // ApplyUpdate reads an update to an Enitiy's properties and 38 | // triggers registered PropertyUpdateHandlers if values changed. 39 | // 40 | // Intended for internal use only. 41 | ApplyUpdate(reader *bit.BitReader) 42 | // Position returns the entity's position in world coordinates. 43 | Position() r3.Vector 44 | // OnPositionUpdate registers a handler for the entity's position update. 45 | // The handler is called with the new position every time a position-relevant property is updated. 46 | // 47 | // See also Position() 48 | OnPositionUpdate(h func(pos r3.Vector)) 49 | // OnDestroy registers a function to be called on the entity's destruction. 50 | OnDestroy(delegate func()) 51 | // Destroy triggers all via OnDestroy() registered functions. 52 | // 53 | // Intended for internal use only. 54 | Destroy() 55 | // OnCreateFinished registers a function to be called once the entity is fully created - 56 | // i.e. once all property updates have been sent out. 57 | OnCreateFinished(delegate func()) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables/entity_test.go: -------------------------------------------------------------------------------- 1 | package sendtables 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var testData = struct { 10 | entity entity 11 | }{ 12 | entity: entity{ 13 | props: []property{ 14 | {value: PropertyValue{IntVal: 10}}, 15 | {value: PropertyValue{IntVal: 20}}, 16 | {value: PropertyValue{IntVal: 30}}, 17 | }, 18 | serialNum: 1337, 19 | serverClass: &serverClass{propNameToIndex: map[string]int{ 20 | "myProp": 0, 21 | "test": 1, 22 | "anotherOne": 2, 23 | }}, 24 | }, 25 | } 26 | 27 | func TestEntity_Properties(t *testing.T) { 28 | ent := entity{props: []property{{value: PropertyValue{IntVal: 1}}}} 29 | 30 | assert.Equal(t, &ent.props[0], ent.Properties()[0]) 31 | } 32 | 33 | func TestEntity_ServerClass(t *testing.T) { 34 | assert.Equal(t, testData.entity.serverClass, testData.entity.ServerClass()) 35 | } 36 | 37 | func TestEntity_SerialNum(t *testing.T) { 38 | assert.Equal(t, testData.entity.serialNum, testData.entity.SerialNum()) 39 | } 40 | 41 | func TestEntity_Property(t *testing.T) { 42 | assert.Equal(t, &testData.entity.props[1], testData.entity.Property("test")) 43 | } 44 | 45 | func TestEntity_Property_Nil(t *testing.T) { 46 | assert.Nil(t, testData.entity.Property("not_found")) 47 | } 48 | 49 | func TestEntity_Property_Value(t *testing.T) { 50 | val, ok := testData.entity.PropertyValue("test") 51 | 52 | assert.True(t, ok) 53 | assert.Equal(t, PropertyValue{IntVal: 20}, val) 54 | } 55 | 56 | func TestEntity_PropertyValue_NotFound(t *testing.T) { 57 | val, ok := testData.entity.PropertyValue("not_found") 58 | 59 | assert.False(t, ok) 60 | assert.Empty(t, val) 61 | } 62 | 63 | func TestEntity_PropertyValueMust_NotFound_Panics(t *testing.T) { 64 | f := func() { 65 | testData.entity.PropertyValueMust("not_found") 66 | } 67 | 68 | assert.Panics(t, f) 69 | } 70 | 71 | func TestProperty_Name(t *testing.T) { 72 | prop := property{entry: &flattenedPropEntry{name: "test"}} 73 | 74 | assert.Equal(t, "test", prop.Name()) 75 | } 76 | 77 | func TestProperty_Type(t *testing.T) { 78 | prop := property{entry: &flattenedPropEntry{prop: &sendTableProperty{rawType: 1}}} 79 | 80 | assert.Equal(t, PropTypeFloat, prop.Type()) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Package fake provides basic mocks for Entity and Property. 2 | // See examples/mocking (https://github.com/markus-wa/demoinfocs-golang/tree/master/examples/mocking). 3 | package fake 4 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables/fake/entity.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "github.com/golang/geo/r3" 5 | "github.com/stretchr/testify/mock" 6 | 7 | bitread "github.com/markus-wa/demoinfocs-golang/v4/internal/bitread" 8 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 9 | ) 10 | 11 | // NewEntityWithProperty creates and returns an entity with a single mocked property. 12 | func NewEntityWithProperty(name string, val st.PropertyValue) *Entity { 13 | entity := new(Entity) 14 | 15 | prop := new(Property) 16 | prop.On("Value").Return(val) 17 | entity.On("Property", name).Return(prop) 18 | 19 | entity.On("PropertyValue", name).Return(val, true) 20 | entity.On("PropertyValueMust", name).Return(val) 21 | 22 | return entity 23 | } 24 | 25 | var _ st.Entity = new(Entity) 26 | 27 | // Entity is a mock for of sendtables.Entity. 28 | type Entity struct { 29 | mock.Mock 30 | } 31 | 32 | // ServerClass is a mock-implementation of Entity.ServerClass(). 33 | func (e *Entity) ServerClass() st.ServerClass { 34 | return e.Called().Get(0).(st.ServerClass) 35 | } 36 | 37 | // ID is a mock-implementation of Entity.ID(). 38 | func (e *Entity) ID() int { 39 | return e.Called().Int(0) 40 | } 41 | 42 | // SerialNum is a mock-implementation of Entity.SerialNum(). 43 | func (e *Entity) SerialNum() int { 44 | return e.Called().Int(0) 45 | } 46 | 47 | // Properties is a mock-implementation of Entity.Properties(). 48 | func (e *Entity) Properties() []st.Property { 49 | return e.Called().Get(0).([]st.Property) 50 | } 51 | 52 | // Property is a mock-implementation of Entity.Property(). 53 | func (e *Entity) Property(name string) st.Property { 54 | v := e.Called(name).Get(0) 55 | if v == nil { 56 | // see https://stackoverflow.com/questions/13476349/check-for-nil-and-nil-interface-in-go 57 | return nil 58 | } 59 | 60 | return v.(st.Property) 61 | } 62 | 63 | // BindProperty is a mock-implementation of Entity.BindProperty(). 64 | func (e *Entity) BindProperty(name string, variable any, valueType st.PropertyValueType) { 65 | e.Called(name, variable, valueType) 66 | } 67 | 68 | // PropertyValue is a mock-implementation of Entity.PropertyValue(). 69 | func (e *Entity) PropertyValue(name string) (st.PropertyValue, bool) { 70 | args := e.Called(name) 71 | 72 | return args.Get(0).(st.PropertyValue), args.Bool(1) 73 | } 74 | 75 | // PropertyValueMust is a mock-implementation of Entity.PropertyValueMust(). 76 | func (e *Entity) PropertyValueMust(name string) st.PropertyValue { 77 | args := e.Called(name) 78 | 79 | return args.Get(0).(st.PropertyValue) 80 | } 81 | 82 | // ApplyUpdate is a mock-implementation of Entity.ApplyUpdate(). 83 | func (e *Entity) ApplyUpdate(reader *bitread.BitReader) { 84 | e.Called(reader) 85 | } 86 | 87 | // Position is a mock-implementation of Entity.Position(). 88 | func (e *Entity) Position() r3.Vector { 89 | return e.Called().Get(0).(r3.Vector) 90 | } 91 | 92 | // OnPositionUpdate is a mock-implementation of Entity.OnPositionUpdate(). 93 | func (e *Entity) OnPositionUpdate(handler func(pos r3.Vector)) { 94 | e.Called(handler) 95 | } 96 | 97 | // BindPosition is a mock-implementation of Entity.BindPosition(). 98 | func (e *Entity) BindPosition(pos *r3.Vector) { 99 | e.Called(pos) 100 | } 101 | 102 | // OnDestroy is a mock-implementation of Entity.OnDestroy(). 103 | func (e *Entity) OnDestroy(delegate func()) { 104 | e.Called(delegate) 105 | } 106 | 107 | // Destroy is a mock-implementation of Entity.Destroy(). 108 | func (e *Entity) Destroy() { 109 | e.Called() 110 | } 111 | 112 | // OnCreateFinished is a mock-implementation of Entity.OnCreateFinished(). 113 | func (e *Entity) OnCreateFinished(delegate func()) { 114 | e.Called() 115 | } 116 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables/fake/property.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | 6 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 7 | ) 8 | 9 | var _ st.Property = new(Property) 10 | 11 | // Property is a mock for of sendtables.Property. 12 | type Property struct { 13 | mock.Mock 14 | } 15 | 16 | // Name is a mock-implementation of Property.Name(). 17 | func (p *Property) Name() string { 18 | return p.Called().Get(0).(string) 19 | } 20 | 21 | // Value is a mock-implementation of Property.Value(). 22 | func (p *Property) Value() st.PropertyValue { 23 | return p.Called().Get(0).(st.PropertyValue) 24 | } 25 | 26 | // Type is a mock-implementation of Property.Type(). 27 | func (p *Property) Type() st.PropertyType { 28 | return p.Called().Get(0).(st.PropertyType) 29 | } 30 | 31 | // OnUpdate is a mock-implementation of Property.OnUpdate(). 32 | func (p *Property) OnUpdate(handler st.PropertyUpdateHandler) { 33 | p.Called(handler) 34 | } 35 | 36 | // Bind is a mock-implementation of Property.Bind(). 37 | func (p *Property) Bind(variable any, valueType st.PropertyValueType) { 38 | p.Called(variable, valueType) 39 | } 40 | 41 | // ArrayElementType is a mock-implementation of Property.ArrayElementType(). 42 | func (p *Property) ArrayElementType() st.PropertyType { 43 | return p.Called().Get(0).(st.PropertyType) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables/propdecoder_test.go: -------------------------------------------------------------------------------- 1 | package sendtables 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "testing" 7 | 8 | bit "github.com/markus-wa/demoinfocs-golang/v4/internal/bitread" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestPropertyValue_BoolVal(t *testing.T) { 13 | assert.True(t, PropertyValue{IntVal: 1}.BoolVal()) 14 | assert.False(t, PropertyValue{IntVal: 0}.BoolVal()) 15 | } 16 | 17 | func TestPropertyValue_Int64Val(t *testing.T) { 18 | expected := int64(76561198000697560) 19 | prop := &property{entry: &flattenedPropEntry{prop: &sendTableProperty{rawType: propTypeInt64, flags: propFlagUnsigned, numberOfBits: 64}}} 20 | b := make([]byte, 8) 21 | binary.LittleEndian.PutUint64(b, uint64(expected)) 22 | r := bit.NewSmallBitReader(bytes.NewReader(b)) 23 | 24 | propDecoder.decodeProp(prop, r) 25 | 26 | assert.Equal(t, expected, prop.value.Int64Val) 27 | } 28 | 29 | func TestDecodeProp_UnknownType(t *testing.T) { 30 | prop := &property{entry: &flattenedPropEntry{prop: &sendTableProperty{rawType: -1}}} 31 | 32 | f := func() { 33 | propDecoder.decodeProp(prop, nil) 34 | } 35 | 36 | assert.Panics(t, f) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables/property_interface.go: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: Auto generated 2 | 3 | package sendtables 4 | 5 | // Property is an auto-generated interface for property, intended to be used when mockability is needed. 6 | // property wraps a flattenedPropEntry and allows registering handlers 7 | // that can be triggered on a update of the property. 8 | type Property interface { 9 | // Name returns the property's name. 10 | Name() string 11 | // Value returns the current value of the property. 12 | Value() PropertyValue 13 | // Type returns the data type of the property. 14 | Type() PropertyType 15 | // ArrayElementType returns the data type of array entries, if Property.Type() is PropTypeArray. 16 | ArrayElementType() PropertyType 17 | // OnUpdate registers a handler for updates of the property's value. 18 | // 19 | // The handler will be called with the current value upon registration. 20 | OnUpdate(handler PropertyUpdateHandler) 21 | /* 22 | Bind binds a property's value to a pointer. 23 | 24 | Example: 25 | 26 | var i int 27 | property.Bind(&i, ValTypeInt) 28 | 29 | This will bind the property's value to i so every time it's updated i is updated as well. 30 | 31 | The valueType indicates which field of the PropertyValue to use for the binding. 32 | */ 33 | Bind(variable any, valueType PropertyValueType) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables/sendtables_test.go: -------------------------------------------------------------------------------- 1 | package sendtables 2 | 3 | import ( 4 | "testing" 5 | 6 | assert "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestServerClassGetters(t *testing.T) { 10 | sc := serverClass{ 11 | id: 1, 12 | name: "TestClass", 13 | dataTableID: 2, 14 | dataTableName: "ADataTable", 15 | } 16 | 17 | assert.Equal(t, sc.id, sc.ID(), "ID should return the id field") 18 | assert.Equal(t, sc.name, sc.Name(), "Name should return the name field") 19 | assert.Equal(t, sc.dataTableID, sc.DataTableID(), "DataTableID should return the dataTableID field") 20 | assert.Equal(t, sc.dataTableName, sc.DataTableName(), "DataTableName should return the dataTableName field") 21 | } 22 | 23 | func TestServerClassPropertyEntries(t *testing.T) { 24 | var sc serverClass 25 | 26 | assert.Empty(t, sc.PropertyEntries()) 27 | 28 | sc.flattenedProps = []flattenedPropEntry{{name: "prop1"}, {name: "prop2"}} 29 | 30 | assert.ElementsMatch(t, []string{"prop1", "prop2"}, sc.PropertyEntries()) 31 | } 32 | 33 | func TestServerClassString(t *testing.T) { 34 | sc := serverClass{ 35 | id: 1, 36 | name: "TestClass", 37 | dataTableID: 2, 38 | dataTableName: "ADataTable", 39 | } 40 | 41 | expectedString := `serverClass: id=1 name=TestClass 42 | dataTableId=2 43 | dataTableName=ADataTable 44 | baseClasses: 45 | - 46 | properties: 47 | -` 48 | 49 | assert.Equal(t, expectedString, sc.String()) 50 | 51 | sc.baseClasses = []*serverClass{{name: "AnotherClass"}, {name: "YetAnotherClass"}} 52 | sc.flattenedProps = []flattenedPropEntry{{name: "prop1"}, {name: "prop2"}} 53 | 54 | expectedString = `serverClass: id=1 name=TestClass 55 | dataTableId=2 56 | dataTableName=ADataTable 57 | baseClasses: 58 | AnotherClass 59 | YetAnotherClass 60 | properties: 61 | prop1 62 | prop2` 63 | 64 | assert.Equal(t, expectedString, sc.String()) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables/serverclass_interface.go: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: Auto generated 2 | 3 | package sendtables 4 | 5 | // ServerClass is an auto-generated interface for property, intended to be used when mockability is needed. 6 | // serverClass stores meta information about Entity types (e.g. palyers, teams etc.). 7 | type ServerClass interface { 8 | // ID returns the server-class's ID. 9 | ID() int 10 | // Name returns the server-class's name. 11 | Name() string 12 | // DataTableID returns the data-table ID. 13 | DataTableID() int 14 | // DataTableName returns the data-table name. 15 | DataTableName() string 16 | // BaseClasses returns the base-classes of this server-class. 17 | BaseClasses() (res []ServerClass) 18 | // PropertyEntries returns the names of all property-entries on this server-class. 19 | PropertyEntries() []string 20 | // PropertyEntryDefinitions returns all property-entries on this server-class. 21 | PropertyEntryDefinitions() []PropertyEntry 22 | // OnEntityCreated registers a function to be called when a new entity is created from this serverClass. 23 | OnEntityCreated(handler EntityCreatedHandler) 24 | String() string 25 | } 26 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables2/class.go: -------------------------------------------------------------------------------- 1 | package sendtables2 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | st "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/sendtables" 8 | ) 9 | 10 | type fpNameTreeCache struct { 11 | next map[int]*fpNameTreeCache 12 | name string 13 | } 14 | 15 | type class struct { 16 | classId int32 17 | name string 18 | serializer *serializer 19 | createdHandlers []st.EntityCreatedHandler 20 | fpNameCache *fpNameTreeCache 21 | } 22 | 23 | func (c *class) ID() int { 24 | return int(c.classId) 25 | } 26 | 27 | func (c *class) Name() string { 28 | return c.name 29 | } 30 | 31 | func (c *class) DataTableID() int { 32 | panic("not implemented") 33 | } 34 | 35 | func (c *class) DataTableName() string { 36 | panic("not implemented") 37 | } 38 | 39 | func (c *class) BaseClasses() (res []st.ServerClass) { 40 | panic("not implemented") 41 | } 42 | 43 | func (c *class) PropertyEntries() []string { 44 | return c.collectFieldsEntries(c.serializer.fields, "") 45 | } 46 | 47 | func (c *class) PropertyEntryDefinitions() []st.PropertyEntry { 48 | panic("not implemented") 49 | } 50 | 51 | func (c *class) OnEntityCreated(handler st.EntityCreatedHandler) { 52 | c.createdHandlers = append(c.createdHandlers, handler) 53 | } 54 | 55 | func (c *class) String() string { 56 | props := make([]string, 0, len(c.serializer.fields)) 57 | 58 | for _, f := range c.serializer.fields { 59 | props = append(props, fmt.Sprintf("%s: %s", f.varName, f.varType)) 60 | } 61 | 62 | return fmt.Sprintf("%d %s\n %s", c.classId, c.name, strings.Join(props, "\n ")) 63 | } 64 | 65 | func (c *class) collectFieldsEntries(fields []*field, prefix string) []string { 66 | paths := make([]string, 0) 67 | 68 | for _, field := range fields { 69 | if field.serializer != nil { 70 | subPaths := c.collectFieldsEntries(field.serializer.fields, prefix+field.serializer.name+".") 71 | paths = append(paths, subPaths...) 72 | } else { 73 | paths = append(paths, prefix+field.varName) 74 | } 75 | } 76 | 77 | return paths 78 | } 79 | 80 | func (c *class) getNameForFieldPath(fp *fieldPath) string { 81 | currentCacheNode := c.fpNameCache 82 | 83 | for i := 0; i <= fp.last; i++ { 84 | if currentCacheNode.next == nil { 85 | currentCacheNode.next = make(map[int]*fpNameTreeCache) 86 | } 87 | 88 | pos := fp.path[i] 89 | next, exists := currentCacheNode.next[pos] 90 | if !exists { 91 | next = &fpNameTreeCache{} 92 | currentCacheNode.next[pos] = next 93 | } 94 | currentCacheNode = next 95 | } 96 | 97 | if currentCacheNode.name == "" { 98 | currentCacheNode.name = strings.Join(c.serializer.getNameForFieldPath(fp, 0), ".") 99 | } 100 | 101 | return currentCacheNode.name 102 | } 103 | 104 | func (c *class) getTypeForFieldPath(fp *fieldPath) *fieldType { 105 | return c.serializer.getTypeForFieldPath(fp, 0) 106 | } 107 | 108 | func (c *class) getDecoderForFieldPath(fp *fieldPath) fieldDecoder { 109 | return c.serializer.getDecoderForFieldPath(fp, 0) 110 | } 111 | 112 | func (c *class) getFieldPathForName(fp *fieldPath, name string) bool { 113 | return c.serializer.getFieldPathForName(fp, name) 114 | } 115 | 116 | func (c *class) getFieldPaths(fp *fieldPath, state *fieldState) []*fieldPath { 117 | return c.serializer.getFieldPaths(fp, state) 118 | } 119 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables2/field_patch.go: -------------------------------------------------------------------------------- 1 | package sendtables2 2 | 3 | type fieldPatch struct { 4 | minBuild uint32 5 | maxBuild uint32 6 | patch func(f *field) 7 | } 8 | 9 | var fieldPatches = []fieldPatch{ 10 | /* 11 | m_FieldEncoderOverrides = 12 | [ 13 | DemoSimpleEncoders_t { m_Name = "m_flSimulationTime" m_VarType = "NET_DATA_TYPE_UINT64" }, 14 | DemoSimpleEncoders_t { m_Name = "m_flAnimTime" m_VarType = "NET_DATA_TYPE_UINT64" }, 15 | ] 16 | */ 17 | {0, 0, func(f *field) { 18 | switch f.varName { 19 | case "m_flSimulationTime", "m_flAnimTime": 20 | f.encoder = "simtime" 21 | } 22 | }}, 23 | } 24 | 25 | func (p *fieldPatch) shouldApply(build uint32) bool { 26 | if p.minBuild == 0 && p.maxBuild == 0 { 27 | return true 28 | } 29 | 30 | return build >= p.minBuild && build <= p.maxBuild 31 | } 32 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables2/field_state.go: -------------------------------------------------------------------------------- 1 | package sendtables2 2 | 3 | type fieldState struct { 4 | state []interface{} 5 | } 6 | 7 | func newFieldState() *fieldState { 8 | return &fieldState{ 9 | state: make([]interface{}, 8), 10 | } 11 | } 12 | 13 | func (s *fieldState) get(fp *fieldPath) interface{} { 14 | x := s 15 | z := 0 16 | for i := 0; i <= fp.last; i++ { 17 | z = fp.path[i] 18 | if len(x.state) < z+1 { 19 | return nil 20 | } 21 | if i == fp.last { 22 | return x.state[z] 23 | } 24 | if _, ok := x.state[z].(*fieldState); !ok { 25 | return nil 26 | } 27 | x = x.state[z].(*fieldState) 28 | } 29 | return nil 30 | } 31 | 32 | func (s *fieldState) set(fp *fieldPath, v interface{}) { 33 | x := s 34 | z := 0 35 | 36 | for i := 0; i <= fp.last; i++ { 37 | z = fp.path[i] 38 | 39 | if y := len(x.state); y <= z { 40 | newCap := max(z+2, y*2) 41 | if z+2 > cap(x.state) { 42 | newSlice := make([]interface{}, z+1, newCap) 43 | copy(newSlice, x.state) 44 | x.state = newSlice 45 | } else { 46 | // Re-slice to update the length without allocating new memory 47 | x.state = x.state[:z+1] 48 | } 49 | } 50 | 51 | if i == fp.last { 52 | if _, ok := x.state[z].(*fieldState); !ok { 53 | x.state[z] = v 54 | } 55 | return 56 | } 57 | 58 | if _, ok := x.state[z].(*fieldState); !ok { 59 | x.state[z] = newFieldState() 60 | } 61 | 62 | x = x.state[z].(*fieldState) 63 | } 64 | } 65 | 66 | func max(a, b int) int { 67 | if a > b { 68 | return a 69 | } 70 | 71 | return b 72 | } 73 | 74 | func min(a, b int) int { 75 | if a < b { 76 | return a 77 | } 78 | 79 | return b 80 | } 81 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables2/field_type.go: -------------------------------------------------------------------------------- 1 | package sendtables2 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | var fieldTypeRe = regexp.MustCompile(`([^\<\[\*]+)(\<\s(.*)\s\>)?(\*)?(\[(.*)\])?`) // (\<\s.*?\s\>)?([.*?])?`) 10 | 11 | type fieldType struct { 12 | baseType string 13 | genericType *fieldType 14 | pointer bool 15 | count int 16 | } 17 | 18 | func newFieldType(name string) *fieldType { 19 | ss := fieldTypeRe.FindStringSubmatch(name) 20 | if len(ss) != 7 { 21 | panic(fmt.Sprintf("bad regexp: %s -> %#v", name, ss)) 22 | } 23 | 24 | x := &fieldType{ 25 | baseType: ss[1], 26 | pointer: ss[4] == "*", 27 | } 28 | 29 | if ss[3] != "" { 30 | x.genericType = newFieldType(ss[3]) 31 | } 32 | 33 | if n, ok := itemCounts[ss[6]]; ok { 34 | x.count = n 35 | } else if n, _ := strconv.Atoi(ss[6]); n > 0 { 36 | x.count = n 37 | } else if ss[6] != "" { 38 | x.count = 1024 39 | } 40 | 41 | return x 42 | } 43 | 44 | func (t *fieldType) String() string { 45 | x := t.baseType 46 | if t.genericType != nil { 47 | x += "<" + t.genericType.String() + ">" 48 | } 49 | if t.pointer { 50 | x += "*" 51 | } 52 | if t.count > 0 { 53 | x += "[" + strconv.Itoa(t.count) + "]" 54 | } 55 | return x 56 | } 57 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables2/huffman.go: -------------------------------------------------------------------------------- 1 | package sendtables2 2 | 3 | import ( 4 | "container/heap" 5 | ) 6 | 7 | // Interface for the tree, only implements Weight 8 | type huffmanTree interface { 9 | Weight() int 10 | IsLeaf() bool 11 | Value() int 12 | Left() huffmanTree 13 | Right() huffmanTree 14 | } 15 | 16 | // A leaf, contains encoded value 17 | type huffmanLeaf struct { 18 | weight int 19 | value int 20 | } 21 | 22 | // A node with potential left / right nodes or leafs 23 | type huffmanNode struct { 24 | weight int 25 | value int 26 | left huffmanTree 27 | right huffmanTree 28 | } 29 | 30 | // Return weight for leaf 31 | func (self huffmanLeaf) Weight() int { 32 | return self.weight 33 | } 34 | 35 | // Return leaf state 36 | func (self huffmanLeaf) IsLeaf() bool { 37 | return true 38 | } 39 | 40 | // Return value for leaf 41 | func (self huffmanLeaf) Value() int { 42 | return self.value 43 | } 44 | 45 | func (self huffmanLeaf) Right() huffmanTree { 46 | _panicf("huffmanLeaf doesn't have right node") 47 | return nil 48 | } 49 | 50 | func (self huffmanLeaf) Left() huffmanTree { 51 | _panicf("huffmanLeaf doesn't have left node") 52 | return nil 53 | } 54 | 55 | // Return weight for node 56 | func (self huffmanNode) Weight() int { 57 | return self.weight 58 | } 59 | 60 | // Return leaf state 61 | func (self huffmanNode) IsLeaf() bool { 62 | return false 63 | } 64 | 65 | // Return value for node 66 | func (self huffmanNode) Value() int { 67 | return self.value 68 | } 69 | 70 | func (self huffmanNode) Left() huffmanTree { 71 | return huffmanTree(self.left) 72 | } 73 | 74 | func (self huffmanNode) Right() huffmanTree { 75 | return huffmanTree(self.right) 76 | } 77 | 78 | type treeHeap []huffmanTree 79 | 80 | // Returns the amount of nodes in the tree 81 | func (th treeHeap) Len() int { 82 | return len(th) 83 | } 84 | 85 | // Weight compare function 86 | func (th treeHeap) Less(i int, j int) bool { 87 | if th[i].Weight() == th[j].Weight() { 88 | return th[i].Value() >= th[j].Value() 89 | } else { 90 | return th[i].Weight() < th[j].Weight() 91 | } 92 | } 93 | 94 | // Append item, required for heap 95 | func (th *treeHeap) Push(ele interface{}) { 96 | *th = append(*th, ele.(huffmanTree)) 97 | } 98 | 99 | // Remove item, required for heap 100 | func (th *treeHeap) Pop() (popped interface{}) { 101 | popped = (*th)[len(*th)-1] 102 | *th = (*th)[:len(*th)-1] 103 | return 104 | } 105 | 106 | // Swap two items, required for heap 107 | func (th treeHeap) Swap(i, j int) { 108 | th[i], th[j] = th[j], th[i] 109 | } 110 | 111 | // Construct a tree from a map of weight -> item 112 | func buildHuffmanTree(symFreqs []int) huffmanTree { 113 | var trees treeHeap 114 | 115 | for v, w := range symFreqs { 116 | if w == 0 { 117 | w = 1 118 | } 119 | 120 | trees = append(trees, &huffmanLeaf{w, v}) 121 | } 122 | 123 | n := 40 124 | 125 | heap.Init(&trees) 126 | 127 | for trees.Len() > 1 { 128 | a := heap.Pop(&trees).(huffmanTree) 129 | b := heap.Pop(&trees).(huffmanTree) 130 | 131 | heap.Push(&trees, &huffmanNode{a.Weight() + b.Weight(), n, a, b}) 132 | n++ 133 | } 134 | 135 | return heap.Pop(&trees).(huffmanTree) 136 | } 137 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables2/panicf.go: -------------------------------------------------------------------------------- 1 | package sendtables2 2 | 3 | import "fmt" 4 | 5 | func _panicf(format string, args ...interface{}) { 6 | panic(fmt.Sprintf(format, args...)) 7 | } 8 | -------------------------------------------------------------------------------- /pkg/demoinfocs/sendtables2/serializer.go: -------------------------------------------------------------------------------- 1 | package sendtables2 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type fieldIndex struct { 9 | index int 10 | field *field 11 | } 12 | 13 | type serializer struct { 14 | name string 15 | version int32 16 | fields []*field 17 | fieldIndexes map[string]*fieldIndex 18 | fieldNameChecks map[string]bool 19 | } 20 | 21 | func newSerializer(name string, version int32) *serializer { 22 | return &serializer{ 23 | name: name, 24 | version: version, 25 | fields: []*field{}, 26 | fieldIndexes: make(map[string]*fieldIndex), 27 | fieldNameChecks: make(map[string]bool), 28 | } 29 | } 30 | 31 | func (s *serializer) id() string { 32 | return serializerId(s.name, s.version) 33 | } 34 | 35 | func (s *serializer) getNameForFieldPath(fp *fieldPath, pos int) []string { 36 | return s.fields[fp.path[pos]].getNameForFieldPath(fp, pos+1) 37 | } 38 | 39 | func (s *serializer) getTypeForFieldPath(fp *fieldPath, pos int) *fieldType { 40 | return s.fields[fp.path[pos]].getTypeForFieldPath(fp, pos+1) 41 | } 42 | 43 | func (s *serializer) getDecoderForFieldPath(fp *fieldPath, pos int) fieldDecoder { 44 | index := fp.path[pos] 45 | if len(s.fields) <= index { 46 | _panicf("serializer %s: field path %s has no field (%d)", s.name, fp, index) 47 | } 48 | 49 | dec, _ := s.fields[index].getDecoderForFieldPath(fp, pos+1) 50 | 51 | return dec 52 | } 53 | 54 | func (s *serializer) getDecoderForFieldPath2(fp *fieldPath, pos int) (fieldDecoder, bool) { 55 | index := fp.path[pos] 56 | if len(s.fields) <= index { 57 | _panicf("serializer %s: field path %s has no field (%d)", s.name, fp, index) 58 | } 59 | 60 | return s.fields[index].getDecoderForFieldPath(fp, pos+1) 61 | } 62 | 63 | func (s *serializer) getFieldForFieldPath(fp *fieldPath, pos int) *field { 64 | return s.fields[fp.path[pos]].getFieldForFieldPath(fp, pos+1) 65 | } 66 | 67 | func (s *serializer) getFieldPathForName(fp *fieldPath, name string) bool { 68 | if s.fieldIndexes[name] != nil { 69 | fp.path[fp.last] = s.fieldIndexes[name].index 70 | return true 71 | } 72 | 73 | dotIndex := strings.Index(name, ".") 74 | if dotIndex != -1 { 75 | nameBeforeDot := name[:dotIndex] 76 | if s.fieldIndexes[nameBeforeDot] != nil { 77 | fp.path[fp.last] = s.fieldIndexes[nameBeforeDot].index 78 | fp.last++ 79 | f := s.fieldIndexes[nameBeforeDot].field 80 | return f.getFieldPathForName(fp, name[len(f.varName)+1:]) 81 | } 82 | } 83 | 84 | return false 85 | } 86 | 87 | func (s *serializer) getFieldPaths(fp *fieldPath, state *fieldState) []*fieldPath { 88 | results := make([]*fieldPath, 0, 4) 89 | for i, f := range s.fields { 90 | fp.path[fp.last] = i 91 | results = append(results, f.getFieldPaths(fp, state)...) 92 | } 93 | return results 94 | } 95 | 96 | func serializerId(name string, version int32) string { 97 | return fmt.Sprintf("%s(%d)", name, version) 98 | } 99 | 100 | func (s *serializer) addField(f *field) { 101 | newFieldIndex := len(s.fields) 102 | s.fields = append(s.fields, f) 103 | 104 | s.fieldIndexes[f.varName] = &fieldIndex{ 105 | index: newFieldIndex, 106 | field: f, 107 | } 108 | } 109 | 110 | func (s *serializer) checkFieldName(name string) bool { 111 | ok, exists := s.fieldNameChecks[name] 112 | if !exists { 113 | ok = s.getFieldPathForName(newFieldPath(), name) 114 | s.fieldNameChecks[name] = ok 115 | } 116 | 117 | return ok 118 | } 119 | -------------------------------------------------------------------------------- /pkg/demoinfocs/user_messages.go: -------------------------------------------------------------------------------- 1 | package demoinfocs 2 | 3 | import ( 4 | "fmt" 5 | 6 | unassert "github.com/markus-wa/go-unassert" 7 | "google.golang.org/protobuf/proto" 8 | 9 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 10 | msg "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg" 11 | ) 12 | 13 | func (p *parser) handleUserMessage(um *msg.CSVCMsg_UserMessage) { 14 | handler := p.userMessageHandler.handler(msg.ECstrike15UserMessages(um.GetMsgType())) 15 | if handler != nil { 16 | handler(um) 17 | } 18 | } 19 | 20 | type userMessageHandler struct { 21 | parser *parser 22 | msgTypeToHandler map[msg.ECstrike15UserMessages]userMessageHandlerFunc 23 | } 24 | 25 | func (umh userMessageHandler) handler(msgType msg.ECstrike15UserMessages) userMessageHandlerFunc { 26 | if handler, eventKnown := umh.msgTypeToHandler[msgType]; eventKnown { 27 | return handler 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func (umh userMessageHandler) dispatch(event any) { 34 | umh.parser.eventDispatcher.Dispatch(event) 35 | } 36 | 37 | func (umh userMessageHandler) gameState() *gameState { 38 | return umh.parser.gameState 39 | } 40 | 41 | type userMessageHandlerFunc func(*msg.CSVCMsg_UserMessage) 42 | 43 | func newUserMessageHandler(parser *parser) userMessageHandler { 44 | umh := userMessageHandler{parser: parser} 45 | 46 | umh.msgTypeToHandler = map[msg.ECstrike15UserMessages]userMessageHandlerFunc{ 47 | msg.ECstrike15UserMessages_CS_UM_SayText: umh.sayText, 48 | msg.ECstrike15UserMessages_CS_UM_SayText2: umh.sayText2, 49 | msg.ECstrike15UserMessages_CS_UM_ServerRankUpdate: umh.rankUpdate, 50 | msg.ECstrike15UserMessages_CS_UM_RoundImpactScoreData: umh.roundImpactScoreData, 51 | // TODO: handle more user messages (if they are interesting) 52 | // Maybe msg.ECstrike15UserMessages_CS_UM_RadioText 53 | } 54 | 55 | return umh 56 | } 57 | 58 | func (umh userMessageHandler) sayText(um *msg.CSVCMsg_UserMessage) { 59 | st := new(msg.CCSUsrMsg_SayText) 60 | err := proto.Unmarshal(um.MsgData, st) 61 | 62 | if err != nil { 63 | errMsg := fmt.Sprintf("failed to decode SayText message: %s", err.Error()) 64 | 65 | umh.dispatch(events.ParserWarn{Message: errMsg}) 66 | unassert.Error(errMsg) 67 | } 68 | 69 | umh.dispatch(events.SayText{ 70 | EntIdx: int(st.GetEntIdx()), 71 | IsChat: st.GetChat(), 72 | IsChatAll: st.GetTextallchat(), 73 | Text: st.GetText(), 74 | }) 75 | } 76 | 77 | func (umh userMessageHandler) sayText2(um *msg.CSVCMsg_UserMessage) { 78 | st := new(msg.CCSUsrMsg_SayText2) 79 | err := proto.Unmarshal(um.MsgData, st) 80 | 81 | if err != nil { 82 | errMsg := fmt.Sprintf("failed to decode SayText2 message: %s", err.Error()) 83 | 84 | umh.dispatch(events.ParserWarn{Message: errMsg}) 85 | unassert.Error(errMsg) 86 | } 87 | 88 | umh.dispatch(events.SayText2{ 89 | EntIdx: int(st.GetEntIdx()), 90 | IsChat: st.GetChat(), 91 | IsChatAll: st.GetTextallchat(), 92 | MsgName: st.GetMsgName(), 93 | Params: st.Params, 94 | }) 95 | 96 | switch st.GetMsgName() { 97 | case "Cstrike_Chat_All": 98 | fallthrough 99 | case "Cstrike_Chat_AllDead": 100 | sender := umh.gameState().playersByEntityID[int(st.GetEntIdx())] 101 | 102 | umh.dispatch(events.ChatMessage{ 103 | Sender: sender, 104 | Text: st.Params[1], 105 | IsChatAll: st.GetTextallchat(), 106 | }) 107 | 108 | case "#CSGO_Coach_Join_T": // Ignore these 109 | case "#CSGO_Coach_Join_CT": 110 | case "#Cstrike_Name_Change": 111 | case "Cstrike_Chat_T_Loc": 112 | case "Cstrike_Chat_CT_Loc": 113 | case "Cstrike_Chat_T_Dead": 114 | case "Cstrike_Chat_CT_Dead": 115 | 116 | default: 117 | errMsg := fmt.Sprintf("skipped sending ChatMessageEvent for SayText2 with unknown MsgName %q", st.GetMsgName()) 118 | 119 | umh.dispatch(events.ParserWarn{Message: errMsg}) 120 | unassert.Error(errMsg) 121 | } 122 | } 123 | 124 | func (umh userMessageHandler) rankUpdate(um *msg.CSVCMsg_UserMessage) { 125 | st := new(msg.CCSUsrMsg_ServerRankUpdate) 126 | err := proto.Unmarshal(um.MsgData, st) 127 | 128 | if err != nil { 129 | errMsg := fmt.Sprintf("failed to decode ServerRankUpdate message: %s", err.Error()) 130 | 131 | umh.dispatch(events.ParserWarn{Message: errMsg}) 132 | unassert.Error(errMsg) 133 | } 134 | 135 | for _, v := range st.RankUpdate { 136 | // find player (or old instance if he has disconnected already) 137 | steamID32 := uint32(v.GetAccountId()) 138 | 139 | player, ok := umh.parser.gameState.playersBySteamID32[steamID32] 140 | if !ok { 141 | errMsg := fmt.Sprintf("rank update for unknown player with SteamID32=%d", steamID32) 142 | 143 | umh.dispatch(events.ParserWarn{Message: errMsg}) 144 | unassert.Error(errMsg) 145 | } 146 | 147 | umh.dispatch(events.RankUpdate{ 148 | SteamID32: v.GetAccountId(), 149 | RankOld: int(v.GetRankOld()), 150 | RankNew: int(v.GetRankNew()), 151 | WinCount: int(v.GetNumWins()), 152 | RankChange: v.GetRankChange(), 153 | Player: player, 154 | }) 155 | } 156 | } 157 | 158 | func (umh userMessageHandler) roundImpactScoreData(um *msg.CSVCMsg_UserMessage) { 159 | impactData := new(msg.CCSUsrMsg_RoundImpactScoreData) 160 | err := proto.Unmarshal(um.MsgData, impactData) 161 | 162 | if err != nil { 163 | errMsg := fmt.Sprintf("failed to decode RoundImpactScoreData message: %s", err.Error()) 164 | 165 | umh.dispatch(events.ParserWarn{Message: errMsg}) 166 | unassert.Error(errMsg) 167 | } 168 | 169 | umh.dispatch(events.RoundImpactScoreData{ 170 | RawMessage: impactData, 171 | }) 172 | } 173 | -------------------------------------------------------------------------------- /pkg/demoinfocs/user_messages_test.go: -------------------------------------------------------------------------------- 1 | package demoinfocs 2 | 3 | import ( 4 | "testing" 5 | 6 | assert "github.com/stretchr/testify/assert" 7 | proto "google.golang.org/protobuf/proto" 8 | 9 | common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common" 10 | events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 11 | msg "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg" 12 | ) 13 | 14 | func Test_UserMessages_ServerRankUpdate(t *testing.T) { 15 | rankUpdate := &msg.CCSUsrMsg_ServerRankUpdate{ 16 | RankUpdate: []*msg.CCSUsrMsg_ServerRankUpdate_RankUpdate{{ 17 | AccountId: proto.Int32(123), 18 | RankOld: proto.Int32(1), 19 | RankNew: proto.Int32(2), 20 | NumWins: proto.Int32(5), 21 | RankChange: proto.Float32(1), 22 | }, { 23 | AccountId: proto.Int32(456), 24 | RankOld: proto.Int32(2), 25 | RankNew: proto.Int32(3), 26 | NumWins: proto.Int32(6), 27 | RankChange: proto.Float32(2), 28 | }}, 29 | } 30 | userMessageData, err := proto.Marshal(rankUpdate) 31 | assert.Nil(t, err) 32 | um := &msg.CSVCMsg_UserMessage{ 33 | MsgType: proto.Int32(int32(msg.ECstrike15UserMessages_CS_UM_ServerRankUpdate)), 34 | MsgData: userMessageData, 35 | } 36 | 37 | p := NewParser(new(DevNullReader)).(*parser) 38 | 39 | plA := newPlayerS1() 40 | plB := newPlayerS1() 41 | p.gameState.playersBySteamID32[123] = plA 42 | p.gameState.playersBySteamID32[456] = plB 43 | 44 | var evs []events.RankUpdate 45 | p.RegisterEventHandler(func(update events.RankUpdate) { 46 | evs = append(evs, update) 47 | }) 48 | 49 | p.handleUserMessage(um) 50 | 51 | expected := []events.RankUpdate{{ 52 | SteamID32: 123, 53 | RankOld: 1, 54 | RankNew: 2, 55 | WinCount: 5, 56 | RankChange: 1, 57 | Player: plA, 58 | }, { 59 | SteamID32: 456, 60 | RankOld: 2, 61 | RankNew: 3, 62 | WinCount: 6, 63 | RankChange: 2, 64 | Player: plB, 65 | }} 66 | assert.Equal(t, expected, evs) 67 | } 68 | 69 | func Test_UserMessages_SayText(t *testing.T) { 70 | sayText := &msg.CCSUsrMsg_SayText{ 71 | EntIdx: proto.Int32(1), 72 | Text: proto.String("glhf"), 73 | Chat: proto.Bool(true), 74 | Textallchat: proto.Bool(true), 75 | } 76 | userMessageData, err := proto.Marshal(sayText) 77 | assert.Nil(t, err) 78 | um := &msg.CSVCMsg_UserMessage{ 79 | MsgType: proto.Int32(int32(msg.ECstrike15UserMessages_CS_UM_SayText)), 80 | MsgData: userMessageData, 81 | } 82 | 83 | p := NewParser(new(DevNullReader)).(*parser) 84 | 85 | var actual events.SayText 86 | p.RegisterEventHandler(func(chat events.SayText) { 87 | actual = chat 88 | }) 89 | 90 | p.handleUserMessage(um) 91 | 92 | expected := events.SayText{ 93 | EntIdx: 1, 94 | IsChat: true, 95 | Text: "glhf", 96 | IsChatAll: true, 97 | } 98 | assert.Equal(t, expected, actual) 99 | } 100 | 101 | func Test_UserMessages_SayText2_Generic(t *testing.T) { 102 | sayText2 := &msg.CCSUsrMsg_SayText2{ 103 | EntIdx: proto.Int32(1), 104 | MsgName: proto.String("#CSGO_Coach_Join_T"), 105 | Chat: proto.Bool(true), 106 | Textallchat: proto.Bool(true), 107 | Params: []string{"hi there", "hello"}, 108 | } 109 | userMessageData, err := proto.Marshal(sayText2) 110 | assert.Nil(t, err) 111 | um := &msg.CSVCMsg_UserMessage{ 112 | MsgType: proto.Int32(int32(msg.ECstrike15UserMessages_CS_UM_SayText2)), 113 | MsgData: userMessageData, 114 | } 115 | 116 | p := NewParser(new(DevNullReader)).(*parser) 117 | 118 | chatter := &common.Player{ 119 | Name: "The Suspect", 120 | } 121 | p.gameState.playersByEntityID[1] = chatter 122 | 123 | var actual events.SayText2 124 | p.RegisterEventHandler(func(event events.SayText2) { 125 | actual = event 126 | }) 127 | 128 | p.handleUserMessage(um) 129 | 130 | expected := events.SayText2{ 131 | EntIdx: 1, 132 | MsgName: "#CSGO_Coach_Join_T", 133 | Params: sayText2.Params, 134 | IsChat: true, 135 | IsChatAll: true, 136 | } 137 | assert.Equal(t, expected, actual) 138 | } 139 | 140 | func Test_UserMessages_SayText2_ChatMessage(t *testing.T) { 141 | sayText2 := &msg.CCSUsrMsg_SayText2{ 142 | EntIdx: proto.Int32(1), 143 | MsgName: proto.String("Cstrike_Chat_All"), 144 | Textallchat: proto.Bool(true), 145 | Params: []string{"The Suspect", "glhf"}, 146 | } 147 | userMessageData, err := proto.Marshal(sayText2) 148 | assert.Nil(t, err) 149 | um := &msg.CSVCMsg_UserMessage{ 150 | MsgType: proto.Int32(int32(msg.ECstrike15UserMessages_CS_UM_SayText2)), 151 | MsgData: userMessageData, 152 | } 153 | 154 | p := NewParser(new(DevNullReader)).(*parser) 155 | 156 | chatter := &common.Player{ 157 | Name: "The Suspect", 158 | } 159 | p.gameState.playersByEntityID[1] = chatter 160 | 161 | var actual events.ChatMessage 162 | p.RegisterEventHandler(func(chat events.ChatMessage) { 163 | actual = chat 164 | }) 165 | 166 | p.handleUserMessage(um) 167 | 168 | expected := events.ChatMessage{ 169 | Sender: chatter, 170 | Text: "glhf", 171 | IsChatAll: true, 172 | } 173 | assert.Equal(t, expected, actual) 174 | } 175 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | !coverage.sh 2 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # compile all packages + tests 6 | go build ./... 7 | go test -run ^$ ./... 8 | -------------------------------------------------------------------------------- /scripts/check-interfaces-generated.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | scripts_dir=$(dirname "$0") 6 | 7 | $scripts_dir/generate-interfaces.sh 8 | diff_output=$(git diff --ignore-submodules) 9 | if [[ "$diff_output" != "" ]]; then 10 | # don't keep the changes used for the check 11 | git stash save --keep-index --quiet 12 | git stash drop --quiet 13 | 14 | echo "ERROR: generated code is not up-to-date" 15 | echo "$diff_output" 16 | exit 1 17 | fi 18 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | scripts_dir=$(dirname "$0") 6 | $scripts_dir/download-test-data.sh default.7z unexpected_end_of_demo.7z regression-set.7z retake_unknwon_bombsite_index.7z valve_matchmaking.7z s2.7z 7 | 8 | # don't cover mocks and generated protobuf code 9 | coverpkg_ignore='/(fake|msg)' 10 | coverpkg=$(go list ./... | grep -v -E ${coverpkg_ignore} | awk -vORS=, '{ print $1 }' | sed 's/,$/\n/') 11 | 12 | # -timeout 30m because the CI is slow 13 | # output file must be called 'coverage.txt' for Codecov 14 | go test -v -timeout 30m -coverprofile=coverage.txt -coverpkg=$coverpkg -tags unassert_panic ./... 15 | -------------------------------------------------------------------------------- /scripts/download-test-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "downloading/updating '$@', this may take a while ..." 6 | 7 | git submodule init 8 | git submodule update 9 | 10 | pushd test/cs-demos >/dev/null 11 | 12 | minSize=1000000 13 | for f in "$@"; do 14 | fileSize=$(stat -c%s "$f") 15 | if (( $fileSize < $minSize)); then 16 | git lfs pull -I $f 17 | 7z x $f -aoa 18 | fi 19 | done 20 | 21 | popd >/dev/null 22 | 23 | echo 'download complete' 24 | -------------------------------------------------------------------------------- /scripts/generate-interfaces.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | go generate ./pkg/demoinfocs 6 | go generate ./pkg/demoinfocs/sendtables 7 | -------------------------------------------------------------------------------- /scripts/git-hooks/link-git-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Windows needs special treatment 4 | windows() { [[ -n "$WINDIR" ]]; } 5 | 6 | # Cross-platform symlink function 7 | link() { 8 | if windows; then 9 | # We need elevated privileges and some windows stuff 10 | powershell "Start-Process cmd -ArgumentList '/c','cd',(Get-Location).path,'&& mklink','${1//\//\\}','${2//\//\\}','|| cd && pause' -v RunAs" 11 | else 12 | # You know what? I think ln's parameters are backwards. 13 | ln -s "$2" "$1" 14 | fi 15 | } 16 | 17 | cd "$(dirname "$0")/../.." 18 | 19 | if [ -f .git/hooks/pre-commit ]; then 20 | echo 'removing existing pre-commit hook' 21 | rm .git/hooks/pre-commit 22 | fi 23 | if [ -f .git/hooks/pre-push ]; then 24 | echo 'removing existing pre-push hook' 25 | rm .git/hooks/pre-push 26 | fi 27 | 28 | link .git/hooks/pre-commit ../../scripts/git-hooks/pre-commit.sh 29 | echo 'added pre-commit hook' 30 | 31 | echo 'do you want to set up the pre-push hook for the regression suite?' 32 | echo -n 'this will download ~ 1 GB of test-data (if not already done) [y/N] ' 33 | 34 | read prePushYesNo 35 | if [[ "$prePushYesNo" == "y" || "$prePushYesNo" == "Y" ]]; then 36 | scripts/download-test-data.sh default.7z unexpected_end_of_demo.7z regression-set.7z s2.7z 37 | link .git/hooks/pre-push ../../scripts/git-hooks/pre-push.sh 38 | echo 'added pre-push hook' 39 | fi 40 | -------------------------------------------------------------------------------- /scripts/git-hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bin_dir=$(dirname "$0")/../../bin 4 | 5 | if [[ "$(git diff --ignore-submodules)" != "" ]]; then 6 | echo 'stashing unstaged changes for checks' 7 | git stash -q --keep-index 8 | trap 'echo "unstashing unstaged changes" && git stash pop -q' EXIT 9 | fi 10 | 11 | echo -n 'checking interfaces ...' 12 | if [[ "$(command -v ifacemaker)" != "" ]]; then 13 | INTERFACE_CHECK_OUTPUT=$($bin_dir/check-interfaces-generated.sh 2>&1) 14 | retVal=$? 15 | 16 | if [ $retVal -eq 0 ]; then 17 | echo ' OK' 18 | else 19 | echo -e "\n\n$INTERFACE_CHECK_OUTPUT\n" 20 | echo 'ERROR: interfaces not up-to-date or staged for commit. please run scripts/generate-interfaces.sh' 21 | exit 1 22 | fi 23 | else 24 | echo ' SKIPPED: ifacemaker not found on PATH' 25 | fi 26 | 27 | echo -n 'running build ...' 28 | BUILD_OUTPUT=$($bin_dir/build.sh 2>&1) 29 | retVal=$? 30 | 31 | if [ $retVal -eq 0 ]; then 32 | echo ' OK' 33 | else 34 | echo -e "\n\n$BUILD_OUTPUT\n" 35 | echo 'ERROR: build failed. please fix before committing.' 36 | exit 1 37 | fi 38 | 39 | echo -n 'running unit tests ...' 40 | TEST_OUTPUT=$($bin_dir/unit-tests.sh 2>&1) 41 | retVal=$? 42 | 43 | if [ $retVal -eq 0 ]; then 44 | echo ' OK' 45 | else 46 | echo -e "\n\n$TEST_OUTPUT\n" 47 | echo 'ERROR: unit tests failed. please fix before committing.' 48 | exit 1 49 | fi 50 | -------------------------------------------------------------------------------- /scripts/git-hooks/pre-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bin_dir=$(dirname "$0")/../../bin 4 | 5 | if [[ "$(git diff --ignore-submodules)" != "" ]]; then 6 | echo 'stashing unstaged changes for checks' 7 | git stash -q --keep-index 8 | trap 'echo "unstashing unstaged changes" && git stash pop -q' EXIT 9 | fi 10 | 11 | echo 'running regression tests' 12 | TEST_OUTPUT=$($bin_dir/regression-tests.sh 2>&1) 13 | retVal=$? 14 | 15 | if [ $retVal -eq 0 ]; then 16 | echo ' OK' 17 | else 18 | echo -e "\n\n$TEST_OUTPUT\n" 19 | echo 'ERROR: regression tests failed. please fix before pushing.' 20 | exit 1 21 | fi 22 | -------------------------------------------------------------------------------- /scripts/lint-changes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # default reference/baseline is master 6 | if [[ "$base_rev" == "" ]]; then 7 | base_rev='origin/master' 8 | fi 9 | 10 | echo "Linting changes between/since $base_rev" 11 | 12 | golangci-lint run --new-from-rev $base_rev | reviewdog -f=golangci-lint -diff="git diff $base_rev" -reporter="${REVIEWDOG_REPORTER:-local}" 13 | -------------------------------------------------------------------------------- /scripts/profile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go test -benchtime 10s -benchmem -cpuprofile cpu.out -memprofile mem.out -run NONE -bench . ./pkg/demoinfocs -concurrentdemos 8 4 | 5 | go tool pprof -web cpu.out 6 | -------------------------------------------------------------------------------- /scripts/race-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # -short to skip long running regression tests 6 | # -timeout 15m for CI, which is quite slow 7 | go test -v -race -short ./... -timeout 15m 8 | 9 | # run TestDemoInfoCs which is skipped by -short 10 | # so we at least check one demo with race tests 11 | scripts_dir=$(dirname "$0") 12 | $scripts_dir/download-test-data.sh default.7z 13 | go test -v -race -run TestDemoInfoCs ./pkg/demoinfocs -timeout 15m 14 | -------------------------------------------------------------------------------- /scripts/regression-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | scripts_dir=$(dirname "$0") 6 | $scripts_dir/download-test-data.sh default.7z unexpected_end_of_demo.7z regression-set.7z s2.7z 7 | 8 | go test -tags unassert_panic ./... 9 | -------------------------------------------------------------------------------- /scripts/unit-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | go test -tags unassert_panic -short ./... 6 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | *.actual 2 | *.expected -------------------------------------------------------------------------------- /test/default.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markus-wa/demoinfocs-golang/e55c4eaa5aaf466e2f41be6faeb4627f50fc8d3a/test/default.golden --------------------------------------------------------------------------------