├── .gitattributes ├── .github └── workflows │ ├── dependency-review.yml │ ├── docker-publish.yml │ ├── go.yml │ ├── golangci-lint.yml │ ├── nodejs_player.yml │ └── nodejx_index.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── assets ├── faceit-logo.svg ├── favicon.ico └── style.css ├── cmd └── steamsvc.go ├── conf └── conf.go ├── go.mod ├── go.sum ├── main.go ├── pkg ├── auth │ ├── auth.go │ └── cookie.go ├── list │ ├── list.go │ └── match │ │ └── match.go ├── log │ └── logger.go ├── message │ ├── Message.pb.go │ └── message.go ├── parser │ ├── bomb.go │ ├── map.go │ ├── parser.go │ └── weapons.go ├── provider │ ├── faceit │ │ ├── api.go │ │ └── faceit.go │ ├── provider.go │ ├── steam │ │ ├── steam.go │ │ └── steam_test.go │ └── upload │ │ └── upload.go ├── steamsvc │ ├── client.go │ └── decode.go ├── tools │ └── weaponCss.go └── utils │ └── http.go ├── proto.sh ├── protos └── Message.proto ├── test_demos ├── 1-01b60b8a-0b9c-4fe1-bea2-f9e612523112-1-1.dem.gz ├── 1-2ca03eac-ea19-4ea6-9d2c-dfae175ff16c-1-1.dem.gz ├── 1-2d177174-727c-4529-b2af-d156d6457da2-1-1.dem.gz ├── 1-72fbfe3f-a924-446d-ae8b-03965920425c-1-1.dem.gz ├── 1-81f43518-aacc-45ac-a391-b2ada5e6ce53-1-1.dem.gz ├── 1-a7190a93-3116-41bf-9253-977abaa5cd13-1-1.dem.gz ├── 1-c26b4e22-66ac-4904-87cc-3b2b65a67ddb-1-1.dem.gz └── links.txt └── web ├── index ├── .env.development.local ├── .env.production ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── assets │ │ └── faceit-logo.svg │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── robots.txt │ └── style.css └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── DemoLinkInput │ └── DemoLinkInput.js │ ├── MatchTable │ ├── MatchRow.js │ └── MatchTable.js │ ├── Uploader │ ├── Uploader.css │ └── Uploader.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js └── player ├── .env ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── assets │ └── icons │ │ └── csgo │ │ ├── ak47.svg │ │ ├── aug.svg │ │ ├── awp.svg │ │ ├── bizon.svg │ │ ├── c4.svg │ │ ├── c4_exp.svg │ │ ├── c4_old.svg │ │ ├── cz75.svg │ │ ├── dead.svg │ │ ├── deagle.svg │ │ ├── decoy.svg │ │ ├── defuse.svg │ │ ├── duals.svg │ │ ├── famas.svg │ │ ├── fiveseven.svg │ │ ├── flash.svg │ │ ├── g3sg1.svg │ │ ├── galil.svg │ │ ├── glock.svg │ │ ├── he.svg │ │ ├── hkp2000.svg │ │ ├── incendiary.svg │ │ ├── knife.svg │ │ ├── m4a1s.svg │ │ ├── m4a4.svg │ │ ├── mac10.svg │ │ ├── mag7.svg │ │ ├── molotov.svg │ │ ├── mp5.svg │ │ ├── mp7.svg │ │ ├── mp9.svg │ │ ├── negev.svg │ │ ├── not_found.svg │ │ ├── nova.svg │ │ ├── p250.svg │ │ ├── p90.svg │ │ ├── para.svg │ │ ├── sawedoff.svg │ │ ├── scar.svg │ │ ├── scout.svg │ │ ├── sg556.svg │ │ ├── smoke.svg │ │ ├── taser.svg │ │ ├── tec9.svg │ │ ├── ump45.svg │ │ ├── usp-s.svg │ │ ├── vest.svg │ │ ├── vesthelm.svg │ │ ├── world.png │ │ └── xm1014.svg ├── favicon.ico ├── index.html ├── manifest.json ├── robots.txt ├── style.css └── weapons.css └── src ├── App.css ├── App.js ├── App.test.js ├── Error.js ├── MessageBus.js ├── Player.js ├── Websocket.js ├── constants.js ├── index.css ├── index.js ├── map ├── KillFeed.css ├── KillFeed.js ├── Map.css ├── Map2d.js ├── MapBomb.css ├── MapBomb.js ├── MapNade.css ├── MapNade.js ├── MapPlayer.css ├── MapPlayer.js ├── MapShot.css └── MapShot.js ├── panel ├── Controls.css ├── Controls.js ├── InfoPanel.js ├── LoadingProgressBar.js ├── PlayerList.js ├── PlayerListItem.css ├── PlayerListItem.js ├── RoundNav.css ├── RoundNav.js ├── Scoreboard.js └── Timer.js ├── protos └── Message_pb.js ├── reportWebVitals.js └── setupTests.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.dem.gz filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v2 21 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: [ "master", "dev" ] 6 | pull_request: 7 | branches: [ "master", "dev" ] 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v3 24 | 25 | - name: Setup Docker buildx 26 | uses: docker/setup-buildx-action@v2 27 | 28 | - name: Log into registry ${{ env.REGISTRY }} 29 | if: github.event_name != 'pull_request' 30 | uses: docker/login-action@v2 31 | with: 32 | registry: ${{ env.REGISTRY }} 33 | username: ${{ github.actor }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Extract Docker metadata 37 | id: meta 38 | uses: docker/metadata-action@v4 39 | with: 40 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 41 | tags: | 42 | type=sha 43 | type=semver,pattern={{version}} 44 | type=semver,pattern={{major}}.{{minor}} 45 | type=semver,pattern={{major}} 46 | type=ref,event=pr 47 | flavor: latest=auto 48 | 49 | - name: Build and push Docker image 50 | id: build-and-push 51 | uses: docker/build-push-action@v4 52 | with: 53 | context: . 54 | push: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/master' }} 55 | tags: ${{ steps.meta.outputs.tags }} 56 | labels: ${{ steps.meta.outputs.labels }} 57 | cache-from: type=gha 58 | cache-to: type=gha,mode=max 59 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master", "dev" ] 9 | pull_request: 10 | branches: [ "master", "dev" ] 11 | 12 | env: 13 | GOLANG_PROTOBUF_REGISTRATION_CONFLICT: warn 14 | 15 | jobs: 16 | 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v3 24 | with: 25 | go-version: 1.21 26 | 27 | - name: Build 28 | run: go build -v main.go 29 | 30 | - name: Test 31 | run: go test -v ./... 32 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: [ "master", "dev" ] 5 | pull_request: 6 | branches: [ "master", "dev" ] 7 | permissions: 8 | contents: read 9 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 10 | # pull-requests: read 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/setup-go@v5 17 | with: 18 | go-version: '1.21' 19 | - uses: actions/checkout@v4 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v6 22 | with: 23 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 24 | version: latest 25 | 26 | # Optional: working directory, useful for monorepos 27 | # working-directory: somedir 28 | 29 | # Optional: golangci-lint command line arguments. 30 | # args: --out-format=colored-line-number # can't do that in github actions :( 31 | 32 | # Optional: show only new issues if it's a pull request. The default value is `false`. 33 | # only-new-issues: true 34 | 35 | # Optional: if set to true then the all caching functionality will be complete disabled, 36 | # takes precedence over all other caching options. 37 | # skip-cache: true 38 | 39 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 40 | # skip-pkg-cache: true 41 | 42 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 43 | # skip-build-cache: true -------------------------------------------------------------------------------- /.github/workflows/nodejs_player.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI player 5 | 6 | on: 7 | push: 8 | branches: [ "master", "dev" ] 9 | pull_request: 10 | branches: [ "master", "dev" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [lts/*] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | cache-dependency-path: web/player/package-lock.json 30 | - name: npm build 31 | run: | 32 | npm install 33 | npm run build --if-present 34 | working-directory: ./web/player 35 | -------------------------------------------------------------------------------- /.github/workflows/nodejx_index.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI index 5 | 6 | on: 7 | push: 8 | branches: [ "master", "dev" ] 9 | pull_request: 10 | branches: [ "master", "dev" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [lts/*] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | cache-dependency-path: web/index/package-lock.json 30 | - name: npm build 31 | run: | 32 | npm install 33 | npm run build --if-present 34 | working-directory: ./web/index 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | *.DS_Store 4 | assets/icons/shapes 5 | .vscode/ 6 | *.exe 7 | .secret/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21 as builderGo 2 | 3 | USER root 4 | WORKDIR /csgo-2d-demo-player 5 | 6 | COPY go.mod . 7 | COPY go.sum . 8 | RUN go mod download 9 | 10 | COPY . . 11 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build \ 12 | -a -o _output/main \ 13 | -gcflags all=-trimpath=/ \ 14 | -asmflags all=-trimpath=/ \ 15 | main.go 16 | 17 | 18 | FROM node:lts-slim as builderNpm 19 | 20 | USER root 21 | 22 | # index build 23 | WORKDIR /csgo-2d-demo-player/index 24 | 25 | COPY web/index/package.json . 26 | COPY web/index/package-lock.json . 27 | RUN npm install 28 | 29 | COPY web/index/.env.production . 30 | COPY web/index/public public 31 | COPY web/index/src src 32 | RUN npm run build 33 | 34 | # player build 35 | WORKDIR /csgo-2d-demo-player/player 36 | 37 | COPY web/player/package.json . 38 | COPY web/player/package-lock.json . 39 | RUN npm install 40 | 41 | COPY web/player/public public 42 | COPY web/player/src src 43 | RUN npm run build 44 | 45 | 46 | FROM debian:buster-slim 47 | 48 | RUN apt-get update && apt-get install -y ca-certificates 49 | 50 | COPY --from=builderGo /csgo-2d-demo-player/_output/main /csgo-2d-demo-player/ 51 | COPY --from=builderGo /csgo-2d-demo-player/assets/ /csgo-2d-demo-player/assets/ 52 | COPY --from=builderNpm /csgo-2d-demo-player/player/build/ /csgo-2d-demo-player/web/player/build/ 53 | COPY --from=builderNpm /csgo-2d-demo-player/index/build/ /csgo-2d-demo-player/web/index/build/ 54 | 55 | WORKDIR /csgo-2d-demo-player 56 | 57 | CMD /csgo-2d-demo-player/main 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Michal Vala 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 | -------------------------------------------------------------------------------- /assets/faceit-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkoo/csgo-2d-demo-viewer/7ef4cea66229ff17e945e8dfca8164b5220d4694/assets/favicon.ico -------------------------------------------------------------------------------- /cmd/steamsvc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "csgo-2d-demo-player/conf" 5 | "csgo-2d-demo-player/pkg/log" 6 | "csgo-2d-demo-player/pkg/steamsvc" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/alexflint/go-arg" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | var config *conf.ConfSteamSvc 15 | 16 | func main() { 17 | config = &conf.ConfSteamSvc{} 18 | arg.MustParse(config) 19 | 20 | log.Init(config.Mode) 21 | defer log.Close() 22 | log.L().Debug("using config", zap.Any("config", config)) 23 | 24 | client, err := steamsvc.NewSteamClient(config) 25 | 26 | log.L().Debug("hmm, steam", zap.Any("client", client), zap.Error(err)) 27 | 28 | server() 29 | } 30 | 31 | func server() { 32 | mux := http.NewServeMux() 33 | 34 | log.L().Info("HTTP server listening on ...", zap.String("listen", config.Listen), zap.Int("port", config.Port)) 35 | // log.Println("Listening on ", config.Port, " ...") 36 | listenErr := http.ListenAndServe(fmt.Sprintf("%s:%d", config.Listen, config.Port), mux) 37 | log.L().Fatal("failed to listen", zap.Error(listenErr)) 38 | } 39 | -------------------------------------------------------------------------------- /conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | type Mode string 4 | 5 | const MODE_DEV Mode = "dev" 6 | const MODE_PROD Mode = "prod" 7 | 8 | type Conf struct { 9 | // Demodir string `arg:"--demodir, env:DEMODIR" default:"" help:"Path to directory with demos."` 10 | FaceitApiKey string `arg:"--faceitApiKey, env:FACEIT_APIKEY" help:"Faceit Server API key. Get it at https://developers.faceit.com/docs/auth/api-keys"` 11 | FaceitOAuthClientId string `arg:"--faceitOAuthClientId, env:FACEIT_OAUTH_CLIENT_ID"` 12 | FaceitOAuthClientSecret string `arg:"--faceitOAuthClientSecret, env:FACEIT_OAUTH_CLIENT_SECRET"` 13 | SteamWebApiKey string `arg:"--steamWebApiKey, env:STEAM_WEB_APIKEY"` 14 | Listen string `arg:"--listen, env:LISTEN" default:"127.0.0.1"` 15 | Port int `arg:"--port, env:PORT" default:"8080" help:"Server port"` 16 | Mode Mode `arg:"--mode, env:MODE" default:"dev" help:"Runtime environment mode, one of 'dev', 'prod'"` 17 | } 18 | 19 | type ConfSteamSvc struct { 20 | SteamWebApiKey string `arg:"--steamWebApiKey, required, env:STEAM_WEB_APIKEY"` 21 | SteamUsername string `arg:"--steamUsername, required, env:STEAM_USERNAME"` 22 | SteamPassword string `arg:"--steamPassword, required, env:STEAM_PASSWORD"` 23 | Listen string `arg:"--listen, env:LISTEN" default:"127.0.0.1"` 24 | Port int `arg:"--port, env:PORT" default:"8081" help:"Server port"` 25 | Mode Mode `arg:"--mode, env:MODE" default:"dev" help:"Runtime environment mode, one of 'dev', 'prod'"` 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module csgo-2d-demo-player 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.4 6 | 7 | require ( 8 | github.com/alexflint/go-arg v1.5.1 9 | github.com/golang/geo v0.0.0-20230421003525-6adc56603217 10 | github.com/gorilla/websocket v1.5.3 11 | github.com/markus-wa/demoinfocs-golang/v4 v4.2.6 12 | github.com/sparkoo/go-steam v0.0.0-20231112203532-968479d66868 13 | github.com/stretchr/testify v1.8.4 14 | github.com/yohcop/openid-go v1.0.1 15 | go.uber.org/zap v1.27.0 16 | golang.org/x/oauth2 v0.22.0 17 | google.golang.org/protobuf v1.34.2 18 | ) 19 | 20 | require ( 21 | github.com/alexflint/go-scalar v1.2.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/golang/protobuf v1.5.4 // indirect 24 | github.com/golang/snappy v0.0.4 // indirect 25 | github.com/google/go-cmp v0.5.9 // indirect 26 | github.com/markus-wa/go-unassert v0.1.3 // indirect 27 | github.com/markus-wa/gobitread v0.2.3 // indirect 28 | github.com/markus-wa/godispatch v1.4.1 // indirect 29 | github.com/markus-wa/ice-cipher-go v0.0.0-20230901094113-348096939ba7 // indirect 30 | github.com/markus-wa/quickhull-go/v2 v2.2.0 // indirect 31 | github.com/oklog/ulid/v2 v2.1.0 // indirect 32 | github.com/pkg/errors v0.9.1 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | go.uber.org/atomic v1.11.0 // indirect 35 | go.uber.org/multierr v1.11.0 // indirect 36 | golang.org/x/net v0.28.0 // indirect 37 | google.golang.org/appengine v1.6.8 // indirect 38 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /pkg/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "golang.org/x/oauth2" 5 | ) 6 | 7 | type FaceitAuthInfo struct { 8 | UserInfo *FaceitUserInfo `json:"user_info"` 9 | Token *oauth2.Token `json:"access_token"` 10 | } 11 | 12 | type FaceitUserInfo struct { 13 | Nickname string `json:"nickname"` 14 | Guid string `json:"guid"` 15 | Iss string `json:"iss"` 16 | Aud string `json:"aud"` 17 | } 18 | 19 | type SteamAuthInfo struct { 20 | UserId string `json:"user_id"` 21 | Username string `json:"username"` 22 | AvatarUrl string `json:"avatarUrl"` 23 | } 24 | -------------------------------------------------------------------------------- /pkg/auth/cookie.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func GetAuthCookie[T any](name string, r *http.Request, objType *T) (*T, error) { 11 | authCookie, err := r.Cookie(name) 12 | if err != nil { 13 | if err == http.ErrNoCookie { 14 | return nil, nil 15 | } 16 | return nil, fmt.Errorf("failed to get the cookie: %w", err) 17 | } 18 | 19 | if authCookie.Value == "" { 20 | return nil, nil 21 | } 22 | 23 | decoded, errDecode := base64.StdEncoding.DecodeString(authCookie.Value) 24 | if errDecode != nil { 25 | return nil, errDecode 26 | } 27 | 28 | errUnmarshall := json.Unmarshal(decoded, objType) 29 | if errUnmarshall != nil { 30 | return nil, errUnmarshall 31 | } 32 | 33 | return objType, nil 34 | } 35 | 36 | func SetAuthCookie(name string, obj any, w http.ResponseWriter) error { 37 | jsonAuth, errJsonMarshall := json.Marshal(obj) 38 | if errJsonMarshall != nil { 39 | return errJsonMarshall 40 | } 41 | 42 | authCookie := base64.StdEncoding.EncodeToString(jsonAuth) 43 | 44 | http.SetCookie(w, &http.Cookie{ 45 | Name: name, 46 | Value: authCookie, 47 | Path: "/", 48 | MaxAge: 60 * 60 * 24 * 30, 49 | Secure: false, 50 | HttpOnly: true, 51 | SameSite: http.SameSiteLaxMode, 52 | Domain: "", 53 | }) 54 | 55 | return nil 56 | } 57 | 58 | func ClearCookie(name string, w http.ResponseWriter) { 59 | http.SetCookie(w, &http.Cookie{ 60 | Name: name, 61 | Path: "/", 62 | MaxAge: -1, 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/list/list.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "csgo-2d-demo-player/conf" 5 | "csgo-2d-demo-player/pkg/auth" 6 | "csgo-2d-demo-player/pkg/list/match" 7 | "csgo-2d-demo-player/pkg/log" 8 | "csgo-2d-demo-player/pkg/provider/faceit" 9 | "csgo-2d-demo-player/pkg/utils" 10 | "encoding/json" 11 | "fmt" 12 | "net/http" 13 | "strconv" 14 | 15 | "go.uber.org/zap" 16 | ) 17 | 18 | type ListService struct { 19 | Conf *conf.Conf 20 | FaceitClient *faceit.FaceitClient 21 | } 22 | 23 | func (s *ListService) ListMatches(w http.ResponseWriter, r *http.Request) { 24 | log.L().Debug("listing matches") 25 | utils.CorsDev(w, r, s.Conf) 26 | 27 | matches := []match.MatchInfo{} 28 | 29 | limitQuery := r.URL.Query().Get("limit") 30 | if limitQuery == "" { 31 | limitQuery = "15" 32 | } 33 | 34 | limit, errConvLimit := strconv.Atoi(limitQuery) 35 | if errConvLimit != nil { 36 | log.L().Error("failed to convert limit query", zap.Error(errConvLimit)) 37 | w.WriteHeader(http.StatusBadRequest) 38 | return 39 | } 40 | 41 | faceitAuthInfo, errAuth := auth.GetAuthCookie(faceit.FaceitAuthCookieName, r, &auth.FaceitAuthInfo{}) 42 | if errAuth != nil { 43 | log.L().Error("failed to get auth cookie when listing matches", zap.Error(errAuth)) 44 | w.WriteHeader(http.StatusInternalServerError) 45 | return 46 | } 47 | if faceitAuthInfo != nil { 48 | matches = append(matches, s.FaceitClient.ListMatches(faceitAuthInfo, limit)...) 49 | } else { 50 | log.L().Error("authInfo is nil when listing matches") 51 | w.WriteHeader(http.StatusInternalServerError) 52 | return 53 | } 54 | 55 | if errWriteJsonResponse := writeJsonResponse(w, matches); errWriteJsonResponse != nil { 56 | log.L().Error("failed to write json response with list of matches", zap.Error(errWriteJsonResponse)) 57 | } 58 | } 59 | 60 | func (s *ListService) MatchDetails(w http.ResponseWriter, r *http.Request) { 61 | log.L().Debug("getting match details") 62 | utils.CorsDev(w, r, s.Conf) 63 | 64 | queryVals := r.URL.Query() 65 | 66 | platform := queryVals.Get("platform") 67 | 68 | if platform != "faceit" { 69 | log.L().Error("unexpected platform name. Expected 'faceit'.") 70 | w.WriteHeader(http.StatusBadRequest) 71 | return 72 | } 73 | 74 | match, errMatchDetail := s.FaceitClient.MatchDetails(r.Body) 75 | if errMatchDetail != nil { 76 | log.L().Error("failed to get faceit match detail", zap.Error(errMatchDetail)) 77 | w.WriteHeader(http.StatusInternalServerError) 78 | return 79 | } 80 | 81 | if errWriteJsonResponse := writeJsonResponse(w, match); errWriteJsonResponse != nil { 82 | log.L().Error("failed to write json response with match detail", zap.Error(errWriteJsonResponse)) 83 | } 84 | } 85 | 86 | func writeJsonResponse(w http.ResponseWriter, obj any) error { 87 | matchesJson, errJson := json.Marshal(obj) 88 | if errJson != nil { 89 | w.WriteHeader(http.StatusInternalServerError) 90 | return fmt.Errorf("failed to marshall matches json: %w", errJson) 91 | } 92 | 93 | if _, errWrite := w.Write(matchesJson); errWrite != nil { 94 | w.WriteHeader(http.StatusInternalServerError) 95 | return fmt.Errorf("failed to write matches response:%w", errWrite) 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/list/match/match.go: -------------------------------------------------------------------------------- 1 | package match 2 | 3 | type MatchInfo struct { 4 | Id string `json:"matchId"` 5 | Host MatchHost `json:"hostPlatform"` 6 | DateTime string `json:"dateTime"` 7 | Map string `json:"map"` 8 | Type string `json:"type"` 9 | TeamA string `json:"teamA"` 10 | TeamAPlayers []string `json:"teamAPlayers"` 11 | ScoreA int `json:"scoreA"` 12 | TeamB string `json:"teamB"` 13 | TeamBPlayers []string `json:"teamBPlayers"` 14 | ScoreB int `json:"scoreB"` 15 | DemoLink string `json:"demoLink"` 16 | MatchLink string `json:"matchLink"` 17 | } 18 | 19 | type MatchHost string 20 | -------------------------------------------------------------------------------- /pkg/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "csgo-2d-demo-player/conf" 5 | "fmt" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | var logger *zap.Logger 11 | 12 | func Init(mode conf.Mode) { 13 | switch mode { 14 | case conf.MODE_DEV: 15 | logger = zap.Must(zap.NewDevelopment()) 16 | logger.Info("initialized development logger") 17 | case conf.MODE_PROD: 18 | logger = zap.Must(zap.NewProduction()) 19 | logger.Info("initialized production logger") 20 | default: 21 | panic("unknown mode") 22 | } 23 | } 24 | 25 | func Close() { 26 | errClose := logger.Sync() 27 | if errClose != nil { 28 | panic(errClose) 29 | } 30 | } 31 | 32 | func L() *zap.Logger { 33 | if logger == nil { 34 | Init(conf.MODE_DEV) 35 | } 36 | return logger 37 | } 38 | 39 | func Print(msg string, err error) { 40 | logger.Info(msg, zap.Error(err)) 41 | } 42 | 43 | func Printf(msg string, args ...any) { 44 | logger.Info(fmt.Sprintf(msg, args...)) 45 | } 46 | 47 | func Println(msg string, err error) { 48 | logger.Info(msg, zap.Error(err)) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/message/message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 4 | 5 | func NewRound(startTick int) *Round { 6 | return &Round{ 7 | StartTick: int32(startTick), 8 | Ticks: make([]*Message, 0), 9 | } 10 | } 11 | 12 | func (r *Round) Add(message *Message) { 13 | if message != nil { 14 | r.Ticks = append(r.Ticks, message) 15 | } 16 | } 17 | 18 | func CreateTeamUpdateMessage(tick demoinfocs.GameState) *TeamUpdate { 19 | return &TeamUpdate{ 20 | TName: tick.TeamTerrorists().ClanName(), 21 | TScore: int32(tick.TeamTerrorists().Score()), 22 | CTName: tick.TeamCounterTerrorists().ClanName(), 23 | CTScore: int32(tick.TeamCounterTerrorists().Score()), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/parser/bomb.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "csgo-2d-demo-player/pkg/log" 5 | "csgo-2d-demo-player/pkg/message" 6 | 7 | "github.com/golang/geo/r3" 8 | dem "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 9 | "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events" 10 | ) 11 | 12 | type bombHandler struct { 13 | parser dem.Parser 14 | position r3.Vector 15 | state message.Bomb_BombState 16 | } 17 | 18 | const distanceDelta float64 = 5 19 | 20 | func newBombHandler(parser dem.Parser) *bombHandler { 21 | return &bombHandler{ 22 | parser: parser, 23 | position: r3.Vector{ 24 | X: 0, 25 | Y: 0, 26 | Z: 0, 27 | }, 28 | state: message.Bomb_Zero, 29 | } 30 | } 31 | 32 | func (b *bombHandler) registerEvents() { 33 | b.parser.RegisterEventHandler(func(e events.BombEventIf) { 34 | switch e.(type) { 35 | case events.BombPlantBegin: 36 | b.state = message.Bomb_Planting 37 | case events.BombPlantAborted: 38 | b.state = message.Bomb_Zero 39 | case events.BombPlanted: 40 | b.state = message.Bomb_Planted 41 | case events.BombDefuseStart: 42 | b.state = message.Bomb_Defusing 43 | case events.BombDefuseAborted: 44 | b.state = message.Bomb_Planted 45 | case events.BombDefused: 46 | b.state = message.Bomb_Defused 47 | case events.BombExplode: 48 | b.state = message.Bomb_Explode 49 | default: 50 | log.Printf("unknown bomb state ? '%+v'", e) 51 | } 52 | //log.Printf("bomb state changed '%+v', time '%v', tick '%v'", b.state, b.parser.CurrentTime(), b.parser.CurrentFrame()) 53 | }) 54 | } 55 | 56 | func (b *bombHandler) tick() { 57 | // because there is no event that tells us bomb is in zero state (in game, not planted), we detect 58 | // bomb movement by calculating distance between frames. this happens at the end of the round when bomb was 59 | // previously in different state (planted, exploded) 60 | bombPos := b.parser.GameState().Bomb().Position() 61 | bombPos.Z = 0 62 | oldBombPos := b.position 63 | oldBombPos.Z = 0 64 | distance := bombPos.Distance(oldBombPos) 65 | 66 | if distance > distanceDelta && 67 | (b.state == message.Bomb_Planted || b.state == message.Bomb_Explode || b.state == message.Bomb_Defused) { 68 | //log.Printf("bomb movement detected '%v', state back to ZERO from '%v'", distance, b.state) 69 | b.state = message.Bomb_Zero 70 | } 71 | 72 | b.position = b.parser.GameState().Bomb().Position() 73 | } 74 | 75 | func (b *bombHandler) message(mapCS *MapCS) *message.Bomb { 76 | x, y := translatePosition(b.position, mapCS) 77 | return &message.Bomb{ 78 | X: x, 79 | Y: y, 80 | Z: b.position.Z, 81 | State: b.state, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/parser/map.go: -------------------------------------------------------------------------------- 1 | // Package metadata provides metadata and utility functions, 2 | // like translations from ingame coordinates to radar image pixels (see also /assets/maps directory). 3 | package parser 4 | 5 | import ( 6 | "github.com/golang/geo/r2" 7 | ) 8 | 9 | // MapCS represents a CS:GO map. It contains information required to translate 10 | // in-game world coordinates to coordinates relative to (0, 0) on the provided map-overviews (radar images). 11 | type MapCS struct { 12 | Name string 13 | PZero r2.Point 14 | Scale float64 15 | } 16 | 17 | // Translate translates in-game world-relative coordinates to (0, 0) relative coordinates. 18 | func (m MapCS) Translate(x, y float64) (float64, float64) { 19 | return x - m.PZero.X, m.PZero.Y - y 20 | } 21 | 22 | // TranslateScale translates and scales in-game world-relative coordinates to (0, 0) relative coordinates. 23 | // The outputs are pixel coordinates for the radar images found in the maps folder. 24 | func (m MapCS) TranslateScale(x, y float64) (float64, float64) { 25 | x, y = m.Translate(x, y) 26 | return x / m.Scale, y / m.Scale 27 | } 28 | 29 | // MapNameToMap translates a map name to a Map. 30 | var MapNameToMap = make(map[string]MapCS) 31 | 32 | // makeMap creates a map stuct initialized with the given parameters. 33 | func makeMap(name string, x, y, scale float64) MapCS { 34 | m := MapCS{Name: name, PZero: r2.Point{X: x, Y: y}, Scale: scale} 35 | 36 | MapNameToMap[name] = m 37 | 38 | return m 39 | } 40 | 41 | // Pre-defined map translations. 42 | // see "steamapps/common/Counter-Strike Global Offensive/csgo/resource/overviews/*.txt" 43 | var ( 44 | MapDeAncient = makeMap("de_ancient", -2953, 2164, 5) 45 | MapDeAnubis = makeMap("de_anubis", -2796, 3328, 5.22) 46 | MapDeCache = makeMap("de_cache", -2000, 3250, 5.5) 47 | MapDeCanals = makeMap("de_canals", -2496, 1792, 4) 48 | MapDeCbble = makeMap("de_cbble", -3840, 3072, 6) 49 | MapDeDust2 = makeMap("de_dust2", -2476, 3239, 4.4) 50 | MapDeInferno = makeMap("de_inferno", -2087, 3870, 4.9) 51 | MapDeMirage = makeMap("de_mirage", -3230, 1713, 5) 52 | MapDeNuke = makeMap("de_nuke", -3453, 2887, 7) 53 | MapDeOverpass = makeMap("de_overpass", -4831, 1781, 5.2) 54 | MapDeTrain = makeMap("de_train", -2477, 2392, 4.7) 55 | MapDeVertigo = makeMap("de_vertigo", -3168, 1762, 4) 56 | MapCsAgency = makeMap("cs_agency", -2947, 2492, 5) 57 | MapCsOffice = makeMap("cs_office", -1838, 1858, 4.1) 58 | ) 59 | -------------------------------------------------------------------------------- /pkg/provider/faceit/api.go: -------------------------------------------------------------------------------- 1 | package faceit 2 | 3 | const ( 4 | TeamAKey = "faction1" 5 | TeamBKey = "faction2" 6 | ) 7 | 8 | type MatchData struct { 9 | End int64 `json:"end"` 10 | From int64 `json:"from"` 11 | Items []struct { 12 | CompetitionID string `json:"competition_id"` 13 | CompetitionName string `json:"competition_name"` 14 | CompetitionType string `json:"competition_type"` 15 | FaceitURL string `json:"faceit_url"` 16 | FinishedAt int64 `json:"finished_at"` 17 | GameID string `json:"game_id"` 18 | GameMode string `json:"game_mode"` 19 | MatchID string `json:"match_id"` 20 | MatchType string `json:"match_type"` 21 | MaxPlayers int64 `json:"max_players"` 22 | OrganizerID string `json:"organizer_id"` 23 | PlayingPlayers []string `json:"playing_players"` 24 | Region string `json:"region"` 25 | Results struct { 26 | Score map[string]int64 `json:"score"` 27 | Winner string `json:"winner"` 28 | } `json:"results"` 29 | StartedAt int64 `json:"started_at"` 30 | Status string `json:"status"` 31 | Teams map[string]struct { 32 | Avatar string `json:"avatar"` 33 | Nickname string `json:"nickname"` 34 | Players []Player `json:"players"` 35 | TeamID string `json:"team_id"` 36 | Type string `json:"type"` 37 | } `json:"teams"` 38 | TeamsSize int64 `json:"teams_size"` 39 | } `json:"items"` 40 | Start int64 `json:"start"` 41 | To int64 `json:"to"` 42 | } 43 | 44 | type Player struct { 45 | Avatar string `json:"avatar"` 46 | FaceitURL string `json:"faceit_url"` 47 | GamePlayerID string `json:"game_player_id"` 48 | GamePlayerName string `json:"game_player_name"` 49 | Nickname string `json:"nickname"` 50 | PlayerID string `json:"player_id"` 51 | SkillLevel int64 `json:"skill_level"` 52 | } 53 | 54 | type GameStats struct { 55 | Rounds []struct { 56 | BestOf string `json:"best_of"` 57 | CompetitionID interface{} `json:"competition_id"` 58 | GameID string `json:"game_id"` 59 | GameMode string `json:"game_mode"` 60 | MatchID string `json:"match_id"` 61 | MatchRound string `json:"match_round"` 62 | Played string `json:"played"` 63 | RoundStats struct { 64 | Map string `json:"Map"` 65 | Region string `json:"Region"` 66 | Score string `json:"Score"` 67 | Winner string `json:"Winner"` 68 | Rounds string `json:"Rounds"` 69 | } `json:"round_stats"` 70 | Teams []struct { 71 | TeamID string `json:"team_id"` 72 | Premade bool `json:"premade"` 73 | TeamStats struct { 74 | OvertimeScore string `json:"Overtime score"` 75 | FirstHalfScore string `json:"First Half Score"` 76 | SecondHalfScore string `json:"Second Half Score"` 77 | Team string `json:"Team"` 78 | TeamWin string `json:"Team Win"` 79 | TeamHeadshots string `json:"Team Headshots"` 80 | FinalScore string `json:"Final Score"` 81 | } `json:"team_stats"` 82 | Players []struct { 83 | PlayerID string `json:"player_id"` 84 | Nickname string `json:"nickname"` 85 | PlayerStats struct { 86 | KRRatio string `json:"K/R Ratio"` 87 | KDRatio string `json:"K/D Ratio"` 88 | Headshots string `json:"Headshots"` 89 | PentaKills string `json:"Penta Kills"` 90 | HeadshotsPct string `json:"Headshots %"` 91 | MVPs string `json:"MVPs"` 92 | Kills string `json:"Kills"` 93 | Assists string `json:"Assists"` 94 | Result string `json:"Result"` 95 | TripleKills string `json:"Triple Kills"` 96 | Deaths string `json:"Deaths"` 97 | QuadroKills string `json:"Quadro Kills"` 98 | } `json:"player_stats"` 99 | } `json:"players"` 100 | } `json:"teams"` 101 | } `json:"rounds"` 102 | } 103 | -------------------------------------------------------------------------------- /pkg/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import "io" 4 | 5 | type DemoProvider interface { 6 | DemoStream(matchId string) (io.ReadCloser, error) 7 | } 8 | -------------------------------------------------------------------------------- /pkg/provider/steam/steam_test.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestParseOpenIdUrl(t *testing.T) { 10 | id, err := parseSteamId("https://steamcommunity.com/openid/id/76561197979904892") 11 | 12 | assert.Equal(t, "76561197979904892", id) 13 | assert.NoError(t, err) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/provider/upload/upload.go: -------------------------------------------------------------------------------- 1 | package upload 2 | 3 | import ( 4 | "csgo-2d-demo-player/pkg/utils" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | type UploadProvider struct { 10 | httpClient *http.Client 11 | } 12 | 13 | func NewUploadClient() *UploadProvider { 14 | return &UploadProvider{ 15 | httpClient: utils.CreateHttpClient(), 16 | } 17 | } 18 | 19 | func (p *UploadProvider) DemoStream(matchId string) (io.ReadCloser, error) { 20 | demoUrl := matchId 21 | // TODO: verify URL ? 22 | 23 | // log.Printf("Reading file '%s'", demoUrl) 24 | req, reqErr := utils.CreateRequest(demoUrl, "") 25 | if reqErr != nil { 26 | return nil, reqErr 27 | } 28 | resp, doReqErr := utils.DoRequest(p.httpClient, req, 200, 3) 29 | if doReqErr != nil { 30 | return nil, doReqErr 31 | } 32 | return resp.Body, nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/steamsvc/client.go: -------------------------------------------------------------------------------- 1 | //nolint:golint,unused,staticcheck // unused for now, might be useful later 2 | package steamsvc 3 | 4 | import ( 5 | "csgo-2d-demo-player/conf" 6 | "fmt" 7 | "time" 8 | 9 | "csgo-2d-demo-player/pkg/log" 10 | 11 | sclient "github.com/sparkoo/go-steam" 12 | csgoproto "github.com/sparkoo/go-steam/csgo/protocol/protobuf" 13 | "github.com/sparkoo/go-steam/protocol/gamecoordinator" 14 | "github.com/sparkoo/go-steam/protocol/steamlang" 15 | "go.uber.org/zap" 16 | "google.golang.org/protobuf/proto" 17 | ) 18 | 19 | // https://api.steampowered.com/ICSGOPlayers_730/GetNextMatchSharingCode/v1?key=4272CD0C6DBFEFC0ED2D509E4EFE6165&steamid=76561197979904892&steamidkey=73YF-MQ2HM-ZAKP&knowncode=CSGO-YaLAL-2Ornh-UE8pP-bhQVr-Q4zAC 20 | 21 | // steam://rungame/730/76561202255233023/+csgo_download_match%20CSGO-dK84y-25MFt-5zT4m-XzjS3-8LhWA 22 | 23 | const ( 24 | csgoAppId = 730 25 | 26 | // matchId = "CSGO-YaLAL-2Ornh-UE8pP-bhQVr-Q4zAC" // sparko 27 | matchId = "CSGO-dK84y-25MFt-5zT4m-XzjS3-8LhWA" // CS2 game 28 | ) 29 | 30 | var connectErr chan error 31 | 32 | type steamClient struct { 33 | client *sclient.Client 34 | } 35 | 36 | func NewSteamClient(conf *conf.ConfSteamSvc) (*steamClient, error) { 37 | loginInfo := &sclient.LogOnDetails{ 38 | Username: conf.SteamUsername, 39 | Password: conf.SteamPassword, 40 | } 41 | 42 | client := sclient.NewClient() 43 | if _, errConnect := client.Connect(); errConnect != nil { 44 | panic(errConnect) 45 | } 46 | 47 | connectErr = make(chan error) 48 | go func() { 49 | for event := range client.Events() { 50 | // fmt.Printf("received event '%T' => '%+v'\n", event, event) 51 | switch e := event.(type) { 52 | case *sclient.ConnectedEvent: 53 | client.Auth.LogOn(loginInfo) 54 | // case *sclient.MachineAuthUpdateEvent: 55 | // ioutil.WriteFile("sentry", e.Hash, 0666) 56 | case *sclient.LoggedOnEvent: 57 | client.Social.SetPersonaState(steamlang.EPersonaState_Online) 58 | client.GC.RegisterPacketHandler(&handler{}) 59 | client.GC.SetGamesPlayed(730) 60 | 61 | time.Sleep(3 * time.Second) 62 | client.GC.Write(gamecoordinator.NewGCMsgProtobuf(730, uint32(csgoproto.EGCBaseClientMsg_k_EMsgGCClientHello), &csgoproto.CMsgClientHello{ 63 | Version: Ptr(uint32(1)), 64 | })) 65 | time.Sleep(3 * time.Second) 66 | // fmt.Println("sending some message to some black hole") 67 | matchData, decodeErr := decode(matchId) 68 | if decodeErr != nil { 69 | panic(decodeErr) 70 | } 71 | client.GC.Write(gamecoordinator.NewGCMsgProtobuf(csgoAppId, uint32(csgoproto.ECsgoGCMsg_k_EMsgGCCStrike15_v2_MatchListRequestFullGameInfo), &csgoproto.CMsgGCCStrike15V2_MatchListRequestFullGameInfo{ 72 | Matchid: &matchData.MatchID, 73 | Outcomeid: &matchData.OutcomeID, 74 | Token: &matchData.Token, 75 | })) 76 | 77 | case *sclient.LogOnFailedEvent: 78 | fmt.Printf("eh? %+v\n", e) 79 | case sclient.FatalErrorEvent: 80 | log.L().Error("steam client failed with FatalErrorEvent", zap.Error(e)) 81 | case error: 82 | log.L().Error("steam client failed hard", zap.Error(e)) 83 | } 84 | } 85 | }() 86 | err := <-connectErr 87 | 88 | if err != nil { 89 | return nil, err 90 | } else { 91 | return &steamClient{ 92 | client: client, 93 | }, nil 94 | } 95 | } 96 | 97 | type handler struct { 98 | } 99 | 100 | func (h *handler) HandleGCPacket(packet *gamecoordinator.GCPacket) { 101 | switch packet.MsgType { 102 | case uint32(csgoproto.EGCBaseClientMsg_k_EMsgGCClientWelcome): 103 | { 104 | log.L().Debug("Steam client connected ...") 105 | connectErr <- nil 106 | } 107 | case uint32(csgoproto.ECsgoGCMsg_k_EMsgGCCStrike15_v2_MatchList): 108 | { 109 | var msg csgoproto.CMsgGCCStrike15V2_MatchList 110 | packet.ReadProtoMsg(&msg) 111 | for _, m := range msg.Matches { 112 | for _, r := range m.GetRoundstatsall() { 113 | if r.GetMap() != "" { 114 | fmt.Printf("demo link \\o/ '%s'\n", r.GetMap()) 115 | } 116 | } 117 | } 118 | 119 | } 120 | case uint32(csgoproto.ECsgoGCMsg_k_EMsgGCCStrike15_v2_GC2ClientGlobalStats): 121 | { 122 | // fmt.Printf(">>> %+v\n", packet) 123 | // var msg csgoproto.GlobalStatistics 124 | // packet.ReadProtoMsg(&msg) 125 | // fmt.Printf("hm? %+v\n", msg) 126 | } 127 | case uint32(csgoproto.EGCBaseClientMsg_k_EMsgGCClientConnectionStatus): 128 | { 129 | var msg csgoproto.CMsgConnectionStatus 130 | packet.ReadProtoMsg(&msg) 131 | // fmt.Printf("connection status '%+v' \n", msg) 132 | } 133 | default: 134 | fmt.Printf("received unknown message type %d\n", packet.MsgType) 135 | } 136 | 137 | } 138 | 139 | func accountId(accId uint64) *uint32 { 140 | newAccID := accId - 76561197960265728 141 | return proto.Uint32(uint32(newAccID)) 142 | } 143 | 144 | func Ptr[T any](anything T) *T { 145 | return &anything 146 | } 147 | -------------------------------------------------------------------------------- /pkg/steamsvc/decode.go: -------------------------------------------------------------------------------- 1 | //nolint:golint,unused // this is unused for now, but keeping it here because it might be useful when implementing steam again 2 | package steamsvc 3 | 4 | import ( 5 | "math/big" 6 | "regexp" 7 | 8 | "strings" 9 | ) 10 | 11 | type errInvalidShareCode struct{} 12 | 13 | func (e errInvalidShareCode) Error() string { 14 | return "share_code: invalid share code" 15 | } 16 | 17 | func isInvalidShareCodeError(err error) bool { 18 | _, ok := err.(errInvalidShareCode) 19 | return ok 20 | } 21 | 22 | // shareCodeData holds the decoded match data and encoded share code. 23 | type shareCodeData struct { 24 | Encoded string `json:"encoded"` 25 | OutcomeID uint64 `json:"outcomeID"` 26 | MatchID uint64 `json:"matchID"` 27 | Token uint32 `json:"token"` 28 | } 29 | 30 | // dictionary is used for the share code decoding. 31 | const dictionary = "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789" 32 | 33 | // Used for share code decoding. 34 | var bitmask64 uint64 = 18446744073709551615 35 | 36 | // validate validates an string whether the format matches a valid share code. 37 | func validate(code string) bool { 38 | var validateRe = regexp.MustCompile(`^CSGO(-?[\w]{5}){5}$`) 39 | return validateRe.MatchString(code) 40 | } 41 | 42 | // DecodeShareCode decodes the share code. Taken from ValvePython/csgo. 43 | func decode(code string) (*shareCodeData, error) { 44 | if !validate(code) { 45 | return nil, &errInvalidShareCode{} 46 | } 47 | 48 | var re = regexp.MustCompile(`^CSGO|\-`) 49 | s := re.ReplaceAllString(code, "") 50 | s = reverse(s) 51 | 52 | bigNumber := big.NewInt(0) 53 | 54 | for _, c := range s { 55 | bigNumber = bigNumber.Mul(bigNumber, big.NewInt(int64(len(dictionary)))) 56 | bigNumber = bigNumber.Add(bigNumber, big.NewInt(int64(strings.Index(dictionary, string(c))))) 57 | } 58 | 59 | a := swapEndianness(bigNumber) 60 | 61 | matchid := big.NewInt(0) 62 | outcomeid := big.NewInt(0) 63 | token := big.NewInt(0) 64 | 65 | matchid = matchid.And(a, big.NewInt(0).SetUint64(bitmask64)) 66 | outcomeid = outcomeid.Rsh(a, 64) 67 | outcomeid = outcomeid.And(outcomeid, big.NewInt(0).SetUint64(bitmask64)) 68 | token = token.Rsh(a, 128) 69 | token = token.And(token, big.NewInt(0xFFFF)) 70 | 71 | shareCode := &shareCodeData{MatchID: matchid.Uint64(), OutcomeID: outcomeid.Uint64(), Token: uint32(token.Uint64()), Encoded: code} 72 | return shareCode, nil 73 | } 74 | func reverse(s string) (result string) { 75 | for _, v := range s { 76 | result = string(v) + result 77 | } 78 | return 79 | } 80 | 81 | // swapEndianness changes the byte order. 82 | func swapEndianness(number *big.Int) *big.Int { 83 | result := big.NewInt(0) 84 | 85 | left := big.NewInt(0) 86 | rightTemp := big.NewInt(0) 87 | rightResult := big.NewInt(0) 88 | 89 | for i := 0; i < 144; i += 8 { 90 | left = left.Lsh(result, 8) 91 | rightTemp = rightTemp.Rsh(number, uint(i)) 92 | rightResult = rightResult.And(rightTemp, big.NewInt(0xFF)) 93 | result = left.Add(left, rightResult) 94 | } 95 | 96 | return result 97 | } 98 | -------------------------------------------------------------------------------- /pkg/tools/weaponCss.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "csgo-2d-demo-player/conf" 5 | "csgo-2d-demo-player/pkg/log" 6 | "csgo-2d-demo-player/pkg/parser" 7 | "fmt" 8 | "os" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | func main() { 14 | log.Init(conf.MODE_DEV) 15 | if f, err := os.OpenFile("weapons.css_tmp", os.O_CREATE|os.O_WRONLY, 0644); err == nil { 16 | defer f.Close() 17 | 18 | _, writeWarningErr := f.WriteString("/* THIS FILE IS GENERATED, PLEASE DO NOT CHANGE !!! */\n\n") 19 | if writeWarningErr != nil { 20 | log.L().Fatal("failed to write to file", zap.Error(writeWarningErr)) 21 | } 22 | for _, g := range parser.WeaponsEqType { 23 | filename := fmt.Sprintf("%s.svg", g) 24 | if g == "world" { 25 | filename = fmt.Sprintf("%s.png", g) 26 | } 27 | _, writeLineErr := f.WriteString(fmt.Sprintf(".%s {\n background-image: url(\"assets/icons/csgo/%s\");\n}\n\n", g, filename)) 28 | if writeLineErr != nil { 29 | log.L().Fatal("failed to write to file", zap.Error(writeLineErr)) 30 | } 31 | } 32 | log.Printf("Generated styles for '%d' guns.", len(parser.WeaponsEqType)) 33 | } else { 34 | log.L().Fatal("failed to open file for write", zap.Error(err)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/tls" 5 | "csgo-2d-demo-player/conf" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | func CorsDev(w http.ResponseWriter, r *http.Request, c *conf.Conf) { 13 | if c.Mode == conf.MODE_DEV { 14 | w.Header().Add("Access-Control-Allow-Origin", r.Header.Get("Origin")) 15 | w.Header().Add("Access-Control-Allow-Credentials", "true") 16 | w.Header().Add("Access-Control-Allow-Headers", "upload-length") 17 | w.Header().Add("Access-Control-Allow-Headers", "upload-offset") 18 | w.Header().Add("Access-Control-Allow-Headers", "content-type") 19 | w.Header().Add("Access-Control-Allow-Headers", "upload-name") 20 | w.Header().Add("Access-Control-Allow-Methods", "PATCH") 21 | } 22 | } 23 | 24 | func CreateHttpClient() *http.Client { 25 | return &http.Client{ 26 | Timeout: time.Second * 120, 27 | Transport: &http.Transport{ 28 | TLSClientConfig: &tls.Config{ 29 | InsecureSkipVerify: true, 30 | }, 31 | }} 32 | } 33 | 34 | func CreateRequest(url string, token string) (*http.Request, error) { 35 | req, err := http.NewRequest("GET", url, nil) 36 | if err != nil { 37 | return nil, err 38 | } 39 | if token != "" { 40 | req.Header.Set("Authorization", "Bearer "+token) 41 | } 42 | 43 | return req, nil 44 | } 45 | 46 | func DoRequest(httpClient *http.Client, req *http.Request, expectCode int, retries int) (*http.Response, error) { 47 | var resp *http.Response 48 | for i := 0; i < retries; i++ { 49 | var err error 50 | resp, err = httpClient.Do(req) 51 | if err != nil { 52 | return nil, err 53 | } 54 | if resp.StatusCode == expectCode { 55 | return resp, nil 56 | } 57 | log.Printf("response code '%d' but expected '%d', trying again '%d/%d'", resp.StatusCode, expectCode, i, retries) 58 | } 59 | 60 | return resp, fmt.Errorf("failed request with code '%d'", resp.StatusCode) 61 | } 62 | -------------------------------------------------------------------------------- /proto.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | protoc -I="./protos" --go_out="./pkg" --js_out=import_style=commonjs,binary:"web/player/src/protos" ./protos/*.proto 4 | -------------------------------------------------------------------------------- /protos/Message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package csgo; 4 | 5 | option go_package = "/message"; 6 | 7 | message Message { 8 | enum MessageType { 9 | Zero = 0; 10 | TickStateUpdate = 1; 11 | AddPlayerType = 2; 12 | InitType = 4; 13 | DemoEndType = 5; 14 | RoundType = 6; 15 | ProgressType = 7; 16 | TimeUpdateType = 8; 17 | ShotType = 9; 18 | EmptyType = 10; 19 | FragType = 11; 20 | PlayRequestType = 12; 21 | ErrorType = 13; 22 | GrenadeEventType = 14; 23 | } 24 | MessageType msgType = 1; 25 | int32 tick = 2; 26 | optional TeamUpdate teamUpdate = 3; 27 | optional TickState tickState = 4; 28 | optional Init init = 5; 29 | optional Round round = 6; 30 | optional Progress progress = 7; 31 | optional RoundTime roundTime = 8; 32 | optional Shot shot = 9; 33 | optional Frag frag = 10; 34 | optional Demo demo = 11; 35 | optional Grenade grenadeEvent = 12; 36 | optional string message = 13; 37 | } 38 | 39 | message Player { 40 | int32 PlayerId = 1; 41 | string Name = 2; 42 | string Team = 3; 43 | double X = 4; 44 | double Y = 5; 45 | double Z = 6; 46 | float Rotation = 7; 47 | bool Alive = 8; 48 | string Weapon = 9; 49 | bool Flashed = 10; 50 | int32 Hp = 11; 51 | int32 Armor = 12; 52 | bool Helmet = 13; 53 | bool Defuse = 14; 54 | bool Bomb = 15; 55 | int32 Money = 16; 56 | string Primary = 17; 57 | int32 PrimaryAmmoMagazine = 18; 58 | int32 PrimaryAmmoReserve = 19; 59 | string Secondary = 20; 60 | int32 SecondaryAmmoMagazine = 21; 61 | int32 SecondaryAmmoReserve = 22; 62 | repeated string Grenades = 23; 63 | } 64 | 65 | message TeamUpdate { 66 | string TName = 1; 67 | int32 TScore = 2; 68 | string CTName = 3; 69 | int32 CTScore = 4; 70 | } 71 | 72 | message TickState { 73 | repeated Player Players = 1; 74 | repeated Grenade Nades = 2; 75 | Bomb Bomb = 3; 76 | } 77 | 78 | message Init { 79 | string mapName = 1; 80 | string TName = 2; 81 | string CTName = 3; 82 | } 83 | 84 | message Frag { 85 | string VictimName = 1; 86 | string VictimTeam = 2; 87 | string KillerName = 3; 88 | string KillerTeam = 4; 89 | string Weapon = 5; 90 | } 91 | 92 | message Shot { 93 | int32 PlayerId = 1; 94 | double X = 2; 95 | double Y = 3; 96 | float Rotation = 4; 97 | } 98 | 99 | message Grenade { 100 | int32 id = 1; 101 | string kind = 2; 102 | double x = 3; 103 | double y = 4; 104 | double z = 5; 105 | string action = 6; 106 | } 107 | 108 | message Bomb { 109 | enum BombState { 110 | Zero = 0; 111 | Defusing = 1; 112 | Defused = 2; 113 | Explode = 3; 114 | Planting = 4; 115 | Planted = 5; 116 | } 117 | double x = 1; 118 | double y = 2; 119 | double z = 3; 120 | BombState state = 4; 121 | } 122 | 123 | message Round { 124 | int32 RoundNo = 1; 125 | int32 RoundTookSeconds = 2; 126 | int32 StartTick = 3; 127 | int32 FreezetimeEndTick = 4; 128 | int32 EndTick = 5; 129 | repeated Message Ticks = 6; 130 | TeamUpdate TeamState = 7; 131 | string Winner = 8; 132 | } 133 | 134 | message Progress { 135 | int32 Progress = 1; 136 | string Message = 2; 137 | } 138 | 139 | message RoundTime { 140 | string RoundTime = 1; 141 | int32 FreezeTime = 2; 142 | } 143 | 144 | message Demo { 145 | enum DemoPlatformType { 146 | Upload = 0; 147 | Faceit = 1; 148 | Steam = 2; 149 | } 150 | string MatchId = 1; 151 | DemoPlatformType Platform = 2; 152 | } 153 | -------------------------------------------------------------------------------- /test_demos/1-01b60b8a-0b9c-4fe1-bea2-f9e612523112-1-1.dem.gz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:37e7093e025a6c453ca6cc1bcec97647426ea87da17491c1f308177e116de970 3 | size 169577997 4 | -------------------------------------------------------------------------------- /test_demos/1-2ca03eac-ea19-4ea6-9d2c-dfae175ff16c-1-1.dem.gz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4e7310c5724756abefd5e100baa8d0ce9533f78e749d9282b155ce676863270f 3 | size 155788929 4 | -------------------------------------------------------------------------------- /test_demos/1-2d177174-727c-4529-b2af-d156d6457da2-1-1.dem.gz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:586690dc59dcc72b0beb80f05d420c431c3af8d5db90044e671b040a2aa22a56 3 | size 133230976 4 | -------------------------------------------------------------------------------- /test_demos/1-72fbfe3f-a924-446d-ae8b-03965920425c-1-1.dem.gz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2ca3388257740992a8dd7fd0d660a37ac11e5aa7b33cc736bcf367fc464fbeec 3 | size 139971522 4 | -------------------------------------------------------------------------------- /test_demos/1-81f43518-aacc-45ac-a391-b2ada5e6ce53-1-1.dem.gz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4586b4eeb74d020eb09bce296c35a6c93e884f60fedbcf7d266c5b0e2abc961f 3 | size 209145357 4 | -------------------------------------------------------------------------------- /test_demos/1-a7190a93-3116-41bf-9253-977abaa5cd13-1-1.dem.gz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:88d344ff2ce72e15786f8255763d85d42532f536718c52832f199c88328a0353 3 | size 187044851 4 | -------------------------------------------------------------------------------- /test_demos/1-c26b4e22-66ac-4904-87cc-3b2b65a67ddb-1-1.dem.gz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:fb5c978fe709c2b6b8e7d12a7038ac9fad065552dfa6c65a9f51fe538807ad43 3 | size 79099121 4 | -------------------------------------------------------------------------------- /test_demos/links.txt: -------------------------------------------------------------------------------- 1 | http://localhost:3000/player?platform=upload&matchId=https://github.com/sparkoo/csgo-2d-demo-viewer/raw/dev/test_demos/1-01b60b8a-0b9c-4fe1-bea2-f9e612523112-1-1.dem.gz -------------------------------------------------------------------------------- /web/index/.env.development.local: -------------------------------------------------------------------------------- 1 | PORT=3001 2 | REACT_APP_SERVER_URL=http://localhost:8080 3 | -------------------------------------------------------------------------------- /web/index/.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_SERVER_URL= -------------------------------------------------------------------------------- /web/index/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | # .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /web/index/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /web/index/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "index", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "dotenv": "^16.0.3", 7 | "filepond": "^4.30.4", 8 | "react-filepond": "^7.1.2", 9 | "web-vitals": "^3.5.0", 10 | "react": "npm:@preact/compat", 11 | "react-dom": "npm:@preact/compat" 12 | }, 13 | "devDependencies": { 14 | "@testing-library/jest-dom": "^5.16.5", 15 | "@testing-library/react": "^13.4.0", 16 | "@testing-library/user-event": "^13.5.0", 17 | "react-scripts": "5.0.1" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web/index/public/assets/faceit-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/index/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 20 | 21 | 30 | CS2 2D Demo Player 31 | 32 | 33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /web/index/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkoo/csgo-2d-demo-viewer/7ef4cea66229ff17e945e8dfca8164b5220d4694/web/index/public/logo192.png -------------------------------------------------------------------------------- /web/index/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkoo/csgo-2d-demo-viewer/7ef4cea66229ff17e945e8dfca8164b5220d4694/web/index/public/logo512.png -------------------------------------------------------------------------------- /web/index/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /web/index/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web/index/public/style.css: -------------------------------------------------------------------------------- 1 | a:hover { 2 | text-shadow: 0 0 2px gray; 3 | } 4 | 5 | a { 6 | text-decoration-line: none; 7 | } 8 | -------------------------------------------------------------------------------- /web/index/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkoo/csgo-2d-demo-viewer/7ef4cea66229ff17e945e8dfca8164b5220d4694/web/index/src/App.css -------------------------------------------------------------------------------- /web/index/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /web/index/src/DemoLinkInput/DemoLinkInput.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const DemoLinkInput = (props) => { 4 | const [validationMessage, setValidationMessage] = useState([]); 5 | 6 | const onChange = function (event) { 7 | event.preventDefault() 8 | const demoLink = event.target.demoLinkInput.value 9 | if (demoLink.endsWith(".dem.gz")) { 10 | setValidationMessage("") 11 | const playerLink = window.location.origin + "/player?platform=upload&matchId=" + demoLink 12 | // console.log(playerLink) 13 | window.open(playerLink) 14 | event.target.demoLinkInput.value = "" 15 | } else { 16 | setValidationMessage("this does not look like demo link") 17 | } 18 | } 19 | return ( 20 |
21 |
22 | 23 |
24 | {validationMessage.length > 0 && 25 |
26 |

{validationMessage}

27 |
28 | } 29 | 30 |
31 | ) 32 | } 33 | 34 | export default DemoLinkInput; 35 | -------------------------------------------------------------------------------- /web/index/src/MatchTable/MatchRow.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const MatchRow = (props) => { 4 | const [match, setMatch] = useState(props.details) 5 | 6 | let winner = false 7 | let playerTeam = "" 8 | match.teamAPlayers.forEach(pId => { 9 | if (pId === props.auth.faceitGuid) { 10 | playerTeam = "A" 11 | } 12 | }) 13 | match.teamBPlayers.forEach(pId => { 14 | if (pId === props.auth.faceitGuid) { 15 | playerTeam = "B" 16 | } 17 | }) 18 | 19 | if (match.scoreA > match.scoreB) { 20 | if (playerTeam === "A") { 21 | winner = true 22 | } 23 | } else { 24 | if (playerTeam === "B") { 25 | winner = true 26 | } 27 | } 28 | 29 | useEffect(() => { 30 | if (match.map.length > 0) { 31 | return 32 | } 33 | fetch(`${props.serverHost}/match/detail?platform=${match.hostPlatform}&matchId=${match.matchId}`, { 34 | credentials: "include", method: "POST", body: JSON.stringify(match) 35 | }) 36 | .then((response) => response.json()) 37 | .then((data) => { 38 | // console.log("got", data) 39 | setMatch(data) 40 | }) 41 | .catch((err) => { 42 | console.log("failed to request matches", err.message); 43 | }); 44 | }, [match, props.serverHost]) 45 | 46 | const downloadDemo = (matchId) => { 47 | fetch(props.serverHost + `/faceit/api/matches/${matchId}`) 48 | .then(response => { 49 | if (response.ok) { 50 | response.json() 51 | .then(detail => { 52 | if (detail.demo_url.length === 1) { 53 | var element = document.createElement('a'); 54 | element.setAttribute('href', detail.demo_url[0]); 55 | document.body.appendChild(element); 56 | element.click(); 57 | document.body.removeChild(element); 58 | } else { 59 | alert(`not expected to get ${detail.demo_url.length} demos`) 60 | } 61 | }) 62 | } else { 63 | alert(`response not ok, code: '${response.status}'`) 64 | } 65 | }).catch(error => 66 | alert(`no demo for match '${matchId}'. error: ${error.message}`)) 67 | } 68 | 69 | return ( 70 | 71 | 72 | faceit-logo 73 | {match.dateTime} 74 | 75 | 76 | {match.type} 77 | 78 | 79 | {match.map} 80 | 81 | 82 | {playerTeam === "A" && {match.teamA}} 83 | {playerTeam !== "A" && match.teamA} 84 | 85 | 86 | {match.scoreA} : {match.scoreB} 87 | 88 | 89 | {playerTeam === "B" && {match.teamB}} 90 | {playerTeam !== "B" && match.teamB} 91 | 92 | 93 | { 95 | e.preventDefault() 96 | downloadDemo(match.matchId) 97 | }} 98 | className={"material-icons w3-hover-text-amber"}>{"file_download"} 99 | table_chart 102 | play_circle_outline 105 | 106 | 107 | ) 108 | } 109 | 110 | export default MatchRow; 111 | -------------------------------------------------------------------------------- /web/index/src/MatchTable/MatchTable.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import MatchRow from './MatchRow'; 3 | 4 | const MatchTable = (props) => { 5 | const [matches, setMatches] = useState([]); 6 | 7 | useEffect(() => { 8 | if (matches.length > 0) { 9 | return 10 | } 11 | fetch(props.serverHost + '/match/list?limit=30', { credentials: "include" }) 12 | .then((response) => response.json()) 13 | .then((data) => { 14 | // console.log("got", data) 15 | setMatches(data) 16 | }) 17 | .catch((err) => { 18 | console.log("failed to request matches", err.message); 19 | }); 20 | }, [matches, props.serverHost]) 21 | 22 | let table = (autorenew) 23 | console.log(matches) 24 | if (matches) { 25 | table = ( 26 | 27 | {matches.map(match => ( 28 | 29 | ))} 30 | 31 |
) 32 | } 33 | 34 | return table 35 | } 36 | 37 | export default MatchTable; 38 | -------------------------------------------------------------------------------- /web/index/src/Uploader/Uploader.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkoo/csgo-2d-demo-viewer/7ef4cea66229ff17e945e8dfca8164b5220d4694/web/index/src/Uploader/Uploader.css -------------------------------------------------------------------------------- /web/index/src/Uploader/Uploader.js: -------------------------------------------------------------------------------- 1 | import 'filepond/dist/filepond.min.css'; 2 | import { React, useState } from 'react'; 3 | import { FilePond } from 'react-filepond'; 4 | import './Uploader.css'; 5 | 6 | let uuid = crypto.randomUUID() 7 | 8 | const Uploader = (props) => { 9 | const [demofile, setFiles] = useState([]) 10 | const [serverHost] = useState(window.location.host.includes("localhost") ? "http://localhost:8080" : "") 11 | const [playerHost] = useState(window.location.host.includes("localhost") ? "http://localhost:3000" : "") 12 | console.log("random", uuid) 13 | 14 | return {console.log("preparefile", f, out);}} 27 | onactivatefile={() => {console.log("onactivatefile")}} 28 | onaddfilestart={() => {console.log("onaddfilestart"); uuid = crypto.randomUUID();}} 29 | onprocessfilestart={() => {console.log("onprocessfilestart"); window.open(playerHost + "/player?platform=upload&matchId=" + uuid, "_blank");}} 30 | onprocessfile={(e, f) => {console.log("onprocessfile",e, f);}} 31 | onaddfileprogress={() => {console.log("onaddfileprogress")}} 32 | onprocessfileprogress={(fil,ee) => {console.log("onprocessfileprogress", fil, ee)}} 33 | /> 34 | } 35 | 36 | export default Uploader; 37 | -------------------------------------------------------------------------------- /web/index/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /web/index/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | if (window.location.host.includes("localhost")) { 18 | reportWebVitals(console.log); 19 | } 20 | -------------------------------------------------------------------------------- /web/index/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/index/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /web/index/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /web/player/.env: -------------------------------------------------------------------------------- 1 | port=3000 -------------------------------------------------------------------------------- /web/player/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /web/player/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /web/player/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "player", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "/player", 6 | "dependencies": { 7 | "google-protobuf": "^3.21.2", 8 | "react": "npm:@preact/compat", 9 | "react-dom": "npm:@preact/compat", 10 | "web-vitals": "^2.1.4" 11 | }, 12 | "devDependencies": { 13 | "@testing-library/jest-dom": "^5.16.4", 14 | "@testing-library/react": "^13.1.1", 15 | "@testing-library/user-event": "^14.1.1", 16 | "react-scripts": "5.0.1" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/ak47.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/aug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/awp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/c4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/c4_exp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/c4_old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/cz75.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/dead.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/deagle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/decoy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/defuse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/duals.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/famas.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/fiveseven.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/flash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/g3sg1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/galil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/glock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/hkp2000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/incendiary.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/knife.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/m4a1s.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/m4a4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/mac10.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/molotov.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/mp5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/not_found.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/p90.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/para.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/smoke.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/taser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/ump45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/usp-s.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkoo/csgo-2d-demo-viewer/7ef4cea66229ff17e945e8dfca8164b5220d4694/web/player/public/assets/icons/csgo/world.png -------------------------------------------------------------------------------- /web/player/public/assets/icons/csgo/xm1014.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/player/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkoo/csgo-2d-demo-viewer/7ef4cea66229ff17e945e8dfca8164b5220d4694/web/player/public/favicon.ico -------------------------------------------------------------------------------- /web/player/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CS2 2D Demo Player 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /web/player/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /web/player/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web/player/public/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --TColor: #C7A247; 3 | --CTColor: #4F9EDE; 4 | --BlackColor: #0c0f12; 5 | --LightColor: #ccba7c; 6 | --DarkColor: #413a27; 7 | } 8 | 9 | .T { 10 | color: var(--TColor); 11 | } 12 | 13 | .CT { 14 | color: var(--CTColor); 15 | } 16 | 17 | html, body { 18 | height: 100%; 19 | margin: 0 auto; 20 | padding: 0; 21 | font-family: "Roboto Light", sans-serif; 22 | } 23 | 24 | .grid-container { 25 | display: grid; 26 | grid-template-columns: 60% 40%; 27 | grid-template-rows: auto; 28 | background-color: var(--BlackColor); 29 | color: var(--LightColor) !important; 30 | margin: 0; 31 | padding: 0; 32 | 33 | height: 100%; 34 | } 35 | 36 | .grid-container > div { 37 | text-align: center; 38 | padding: 0; 39 | font-size: 30px; 40 | height: calc(100vh); 41 | } 42 | 43 | .dead { 44 | background-image: url("assets/icons/csgo/dead.svg"); 45 | background-repeat: no-repeat; 46 | background-size: contain; 47 | background-position: center; 48 | } 49 | 50 | .defuse { 51 | background-image: url("assets/icons/csgo/defuse.svg"); 52 | filter: brightness(1.5) !important; 53 | } 54 | 55 | .vest { 56 | background-image: url("assets/icons/csgo/vest.svg"); 57 | background-size: 50%; 58 | } 59 | 60 | .vesthelm { 61 | background-image: url("assets/icons/csgo/vesthelm.svg"); 62 | } 63 | -------------------------------------------------------------------------------- /web/player/public/weapons.css: -------------------------------------------------------------------------------- 1 | /* THIS FILE IS GENERATED, PLEASE DO NOT CHANGE !!! */ 2 | 3 | .fiveseven { 4 | background-image: url("assets/icons/csgo/fiveseven.svg"); 5 | } 6 | 7 | .p90 { 8 | background-image: url("assets/icons/csgo/p90.svg"); 9 | } 10 | 11 | .awp { 12 | background-image: url("assets/icons/csgo/awp.svg"); 13 | } 14 | 15 | .flash { 16 | background-image: url("assets/icons/csgo/flash.svg"); 17 | } 18 | 19 | .deagle { 20 | background-image: url("assets/icons/csgo/deagle.svg"); 21 | } 22 | 23 | .glock { 24 | background-image: url("assets/icons/csgo/glock.svg"); 25 | } 26 | 27 | .nova { 28 | background-image: url("assets/icons/csgo/nova.svg"); 29 | } 30 | 31 | .xm1014 { 32 | background-image: url("assets/icons/csgo/xm1014.svg"); 33 | } 34 | 35 | .sawedoff { 36 | background-image: url("assets/icons/csgo/sawedoff.svg"); 37 | } 38 | 39 | .mp5 { 40 | background-image: url("assets/icons/csgo/mp5.svg"); 41 | } 42 | 43 | .usp-s { 44 | background-image: url("assets/icons/csgo/usp-s.svg"); 45 | } 46 | 47 | .para { 48 | background-image: url("assets/icons/csgo/para.svg"); 49 | } 50 | 51 | .scout { 52 | background-image: url("assets/icons/csgo/scout.svg"); 53 | } 54 | 55 | .sg556 { 56 | background-image: url("assets/icons/csgo/sg556.svg"); 57 | } 58 | 59 | .g3sg1 { 60 | background-image: url("assets/icons/csgo/g3sg1.svg"); 61 | } 62 | 63 | .duals { 64 | background-image: url("assets/icons/csgo/duals.svg"); 65 | } 66 | 67 | .galil { 68 | background-image: url("assets/icons/csgo/galil.svg"); 69 | } 70 | 71 | .aug { 72 | background-image: url("assets/icons/csgo/aug.svg"); 73 | } 74 | 75 | .defuse { 76 | background-image: url("assets/icons/csgo/defuse.svg"); 77 | } 78 | 79 | .mag7 { 80 | background-image: url("assets/icons/csgo/mag7.svg"); 81 | } 82 | 83 | .mp7 { 84 | background-image: url("assets/icons/csgo/mp7.svg"); 85 | } 86 | 87 | .scar { 88 | background-image: url("assets/icons/csgo/scar.svg"); 89 | } 90 | 91 | .decoy { 92 | background-image: url("assets/icons/csgo/decoy.svg"); 93 | } 94 | 95 | .he { 96 | background-image: url("assets/icons/csgo/he.svg"); 97 | } 98 | 99 | .taser { 100 | background-image: url("assets/icons/csgo/taser.svg"); 101 | } 102 | 103 | .knife { 104 | background-image: url("assets/icons/csgo/knife.svg"); 105 | } 106 | 107 | .ump45 { 108 | background-image: url("assets/icons/csgo/ump45.svg"); 109 | } 110 | 111 | .mp9 { 112 | background-image: url("assets/icons/csgo/mp9.svg"); 113 | } 114 | 115 | .bizon { 116 | background-image: url("assets/icons/csgo/bizon.svg"); 117 | } 118 | 119 | .world { 120 | background-image: url("assets/icons/csgo/world.png"); 121 | } 122 | 123 | .hkp2000 { 124 | background-image: url("assets/icons/csgo/hkp2000.svg"); 125 | } 126 | 127 | .cz75 { 128 | background-image: url("assets/icons/csgo/cz75.svg"); 129 | } 130 | 131 | .mac10 { 132 | background-image: url("assets/icons/csgo/mac10.svg"); 133 | } 134 | 135 | .m4a4 { 136 | background-image: url("assets/icons/csgo/m4a4.svg"); 137 | } 138 | 139 | .molotov { 140 | background-image: url("assets/icons/csgo/molotov.svg"); 141 | } 142 | 143 | .incendiary { 144 | background-image: url("assets/icons/csgo/incendiary.svg"); 145 | } 146 | 147 | .smoke { 148 | background-image: url("assets/icons/csgo/smoke.svg"); 149 | } 150 | 151 | .p250 { 152 | background-image: url("assets/icons/csgo/p250.svg"); 153 | } 154 | 155 | .c4 { 156 | background-image: url("assets/icons/csgo/c4.svg"); 157 | } 158 | 159 | .negev { 160 | background-image: url("assets/icons/csgo/negev.svg"); 161 | } 162 | 163 | .ak47 { 164 | background-image: url("assets/icons/csgo/ak47.svg"); 165 | } 166 | 167 | .m4a1s { 168 | background-image: url("assets/icons/csgo/m4a1s.svg"); 169 | } 170 | 171 | .famas { 172 | background-image: url("assets/icons/csgo/famas.svg"); 173 | } 174 | 175 | .tec9 { 176 | background-image: url("assets/icons/csgo/tec9.svg"); 177 | } 178 | 179 | -------------------------------------------------------------------------------- /web/player/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/player/src/App.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import './App.css'; 3 | import ErrorBoundary from "./Error"; 4 | import MessageBus from "./MessageBus"; 5 | import Player from "./Player"; 6 | import Connect from "./Websocket"; 7 | import Map2d from "./map/Map2d"; 8 | import InfoPanel from "./panel/InfoPanel"; 9 | 10 | class App extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.messageBus = new MessageBus() 14 | this.player = new Player(this.messageBus) 15 | this.messageBus.listen([13], function (msg) { 16 | alert(msg.message) 17 | }) 18 | Connect(this.messageBus) 19 | } 20 | 21 | render() { 22 | return ( 23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 |
); 33 | } 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /web/player/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | }); 7 | -------------------------------------------------------------------------------- /web/player/src/Error.js: -------------------------------------------------------------------------------- 1 | import {Component} from "react"; 2 | 3 | class ErrorBoundary extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = {hasError: false, error: ""}; 7 | } 8 | 9 | static getDerivedStateFromError(error) { 10 | return {hasError: true, error: error}; 11 | } 12 | 13 | componentDidCatch(error, errorInfo) { 14 | //TODO: somehow automatically report error here 15 | } 16 | 17 | render() { 18 | if (this.state.hasError) { 19 | return (
20 |

Something went terribly wrong.

21 |

please send me the following error, it will help a lot

22 |
{this.state.error.message}
23 |
{this.state.error.stack}
24 |
) 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | 31 | export default ErrorBoundary 32 | -------------------------------------------------------------------------------- /web/player/src/MessageBus.js: -------------------------------------------------------------------------------- 1 | class MessageBus { 2 | constructor() { 3 | this.listeners = {} 4 | this.listenersAll = [] 5 | } 6 | 7 | listen(msgtypes, callback) { 8 | msgtypes.forEach(msgtype => { 9 | if (!this.listeners[msgtype]) { 10 | this.listeners[msgtype] = [] 11 | } 12 | this.listeners[msgtype].push(callback) 13 | }) 14 | if (msgtypes.length === 0) { 15 | this.listenersAll.push(callback) 16 | } 17 | } 18 | 19 | emit(message) { 20 | if (this.listeners[message.msgtype]) { 21 | this.listeners[message.msgtype].forEach(c => c(message)) 22 | } 23 | this.listenersAll.forEach(c => c(message)) 24 | } 25 | } 26 | 27 | export default MessageBus 28 | -------------------------------------------------------------------------------- /web/player/src/Websocket.js: -------------------------------------------------------------------------------- 1 | const proto = require("./protos/Message_pb"); 2 | 3 | function Connect(messageBus) { 4 | console.log("initializing websocket connection") 5 | 6 | let websocketServerUrl = `wss://${window.location.host}/ws` 7 | if (window.location.host.includes("localhost")) { 8 | websocketServerUrl = `ws://localhost:8080/ws` 9 | } 10 | 11 | let socket = new WebSocket(websocketServerUrl) 12 | socket.binaryType = 'arraybuffer'; 13 | 14 | socket.onopen = function (e) { 15 | console.log("[open] Connection established"); 16 | const urlParams = new URLSearchParams(window.location.search); 17 | 18 | const demo = new proto.Demo() 19 | .setMatchid(urlParams.get("matchId")) 20 | .setPlatform(proto.Demo.DemoPlatformType[urlParams.get("platform").toUpperCase()]); 21 | 22 | const playRequestMessage = new proto.Message() 23 | .setMsgtype(proto.Message.MessageType.PLAYREQUESTTYPE) 24 | .setDemo(demo); 25 | socket.send(playRequestMessage.serializeBinary()); 26 | }; 27 | 28 | socket.onclose = function (event) { 29 | if (event.wasClean) { 30 | console.log( 31 | `[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`); 32 | } else { 33 | console.log('[close] Connection died'); 34 | } 35 | }; 36 | 37 | socket.onerror = function (error) { 38 | console.log(`[websocket error] ${error.message}`); 39 | alert(`websocket error. check console or ping sparko`) 40 | }; 41 | 42 | socket.onmessage = function (event) { 43 | if (event.data instanceof ArrayBuffer) { 44 | const msg = proto.Message.deserializeBinary(new Uint8Array(event.data)).toObject() 45 | messageBus.emit(msg) 46 | } else { 47 | // text frame 48 | // console.log(event.data); 49 | console.log("[message] text data received from server, this is weird. We're using protobufs ?!?!?", event.data); 50 | messageBus.emit(JSON.parse(event.data)) 51 | } 52 | 53 | // console.log(`[message] Data received from server: ${event.data}`); 54 | // let msg = JSON.parse(event.data) 55 | // messageBus.emit(msg) 56 | } 57 | } 58 | 59 | export default Connect 60 | -------------------------------------------------------------------------------- /web/player/src/constants.js: -------------------------------------------------------------------------------- 1 | export const MSG_INIT_ROUNDS = 101 2 | export const MSG_PLAY = 100 3 | export const MSG_PLAY_TOGGLE = 102 4 | export const MSG_PLAY_ROUND_INCREMENT = 103 5 | export const MSG_PLAY_SPEED = 104 6 | export const MSG_PLAY_ROUND_PROGRESS = 105 7 | export const MSG_PLAY_ROUND_UPDATE = 106 8 | export const MSG_PLAY_CHANGE = 107 9 | export const MSG_PROGRESS_MOVE = 108 10 | export const MSG_TEAMSTATE_UPDATE = 109 11 | -------------------------------------------------------------------------------- /web/player/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /web/player/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.css'; 3 | import App from './App'; 4 | import reportWebVitals from './reportWebVitals'; 5 | import {createRoot} from 'react-dom/client'; 6 | 7 | const container = document.getElementById('root'); 8 | const root = createRoot(container); 9 | root.render(); 10 | 11 | // If you want to start measuring performance in your app, pass a function 12 | // to log results (for example: reportWebVitals(console.log)) 13 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 14 | if (window.location.host.includes("localhost")) { 15 | reportWebVitals(console.log); 16 | } 17 | -------------------------------------------------------------------------------- /web/player/src/map/KillFeed.css: -------------------------------------------------------------------------------- 1 | .killfeed { 2 | width: 100%; 3 | font-size: 12px; 4 | font-weight: bold; 5 | padding-right: 20px; 6 | padding-top: 10px; 7 | } 8 | 9 | .killfeedRow { 10 | background-color: var(--DarkColor); 11 | padding: 1px 10px; 12 | border-radius: 5px; 13 | opacity: 0.75; 14 | display: inline-block; 15 | float: right; 16 | clear: right; 17 | margin: 2px 0; 18 | } 19 | 20 | .killfeedIcon { 21 | background-size: contain; 22 | background-repeat: no-repeat; 23 | background-position: center; 24 | display: inline-block; 25 | margin: 0 10px; 26 | width: 40px; 27 | } 28 | 29 | .killfeedIcon.he, .killfeedIcon.molotov, .killfeedIcon.smoke, .killfeedIcon.world { 30 | width: 10px; 31 | } 32 | 33 | .killfeedIcon.world, .killfeedIcon.c4 { 34 | width: 20px; 35 | } 36 | 37 | .killfeedIcon.c4 { 38 | filter: brightness(2) contrast(2); 39 | } 40 | -------------------------------------------------------------------------------- /web/player/src/map/KillFeed.js: -------------------------------------------------------------------------------- 1 | import "./KillFeed.css" 2 | import "../protos/Message_pb" 3 | import {Component} from "react"; 4 | import {MSG_PLAY_CHANGE} from "../constants"; 5 | 6 | class KillFeed extends Component { 7 | constructor(props) { 8 | super(props); 9 | props.messageBus.listen([11], this.fragMessage.bind(this)) 10 | props.messageBus.listen([MSG_PLAY_CHANGE], function () { 11 | this.setState({ 12 | frags: [], 13 | }) 14 | }.bind(this)) 15 | this.state = { 16 | frags: [], 17 | } 18 | } 19 | 20 | fragMessage(msg) { 21 | this.setState({ 22 | frags: [...this.state.frags, msg.frag] 23 | }) 24 | } 25 | 26 | removeFrag(index) { 27 | const newState = this.state.frags 28 | newState[index] = null 29 | this.setState({ 30 | frags: newState, 31 | }) 32 | } 33 | 34 | render() { 35 | const fragComps = this.state.frags.map((f, i) => { 36 | if (f === null) { 37 | return null 38 | } 39 | return 40 | }) 41 | 42 | return
43 | {fragComps} 44 |
45 | } 46 | } 47 | 48 | export default KillFeed 49 | 50 | class Kill extends Component { 51 | componentDidMount() { 52 | setTimeout(function () { 53 | this.props.removeCallback(this.props.index) 54 | }.bind(this), 3000) 55 | } 56 | 57 | render() { 58 | if (this.props.frag === undefined) { 59 | return null 60 | } 61 | const killer = this.props.frag.killername ? 62 | {this.props.frag.killername} : "" 63 | const victim = this.props.frag.victimname ? 64 | {this.props.frag.victimname} : "" 65 | return
66 | {killer} 67 |   68 | {victim} 69 |
70 | } 71 | } 72 | -------------------------------------------------------------------------------- /web/player/src/map/Map.css: -------------------------------------------------------------------------------- 1 | .map { 2 | background-color: black; 3 | } 4 | 5 | .map-container { 6 | position: relative; 7 | height: 100%; 8 | aspect-ratio: 1/1; 9 | background-size: contain; 10 | background-repeat: no-repeat; 11 | background-position: center; 12 | margin: 0 auto; 13 | z-index: 0; 14 | } 15 | -------------------------------------------------------------------------------- /web/player/src/map/Map2d.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { MSG_PLAY_CHANGE } from "../constants"; 3 | import KillFeed from "./KillFeed"; 4 | import './Map.css'; 5 | import MapBomb from "./MapBomb"; 6 | import MapNade from "./MapNade"; 7 | import MapPlayer from "./MapPlayer"; 8 | import MapShot from "./MapShot"; 9 | 10 | class Map2d extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | mapName: "de_dust2", 15 | players: [], 16 | shots: [], 17 | nades: [], 18 | nadeExplosions: [], 19 | bomb: {x: -100, y: -100}, 20 | } 21 | 22 | props.messageBus.listen([4], this.onMessage.bind(this)) 23 | props.messageBus.listen([1], this.tickUpdate.bind(this)) 24 | props.messageBus.listen([9], this.handleShot.bind(this)) 25 | props.messageBus.listen([MSG_PLAY_CHANGE], function () { 26 | this.setState({ 27 | shots: [], 28 | nadeExplosions: [], 29 | }) 30 | }.bind(this)) 31 | props.messageBus.listen([14], this.handleNadeExplosion.bind(this)) 32 | } 33 | 34 | tickUpdate(message) { 35 | if (message.tickstate.playersList) { 36 | this.setState({ 37 | players: message.tickstate.playersList, 38 | nades: message.tickstate.nadesList, 39 | bomb: message.tickstate.bomb, 40 | }) 41 | } 42 | } 43 | 44 | handleShot(msg) { 45 | this.setState({ 46 | shots: [...this.state.shots, msg.shot] 47 | }) 48 | } 49 | 50 | handleNadeExplosion(msg) { 51 | this.setState({ 52 | nadeExplosions: [...this.state.nadeExplosions, msg.grenadeevent] 53 | }) 54 | } 55 | 56 | onMessage(message) { 57 | switch (message.msgtype) { 58 | case 4: 59 | console.log(message.init.mapname) 60 | this.setMapName(message.init.mapname) 61 | break; 62 | default: 63 | console.log("unknown message [Map2d.js]", message) 64 | } 65 | } 66 | 67 | setMapName(name) { 68 | this.setState({ 69 | mapName: name, 70 | } 71 | ) 72 | } 73 | 74 | removeNade(index) { 75 | const newState = this.state.nadeExplosions 76 | newState[index] = null 77 | this.setState({ 78 | nadeExplosions: newState, 79 | }) 80 | } 81 | 82 | removeShot(index) { 83 | const newState = this.state.shots 84 | newState[index] = null 85 | this.setState({ 86 | shots: newState, 87 | }) 88 | } 89 | 90 | render() { 91 | const style = { 92 | backgroundImage: `url("https://raw.githubusercontent.com/zoidyzoidzoid/csgo-overviews/master/overviews/${this.state.mapName}.png")`, 93 | } 94 | const playerComponents = [] 95 | if (this.state.players && this.state.players.length > 0) { 96 | this.state.players.forEach(p => { 97 | playerComponents.push() 100 | }) 101 | } 102 | const shots = this.state.shots.map((s, i) => { 103 | if (s === null) { 104 | return null 105 | } 106 | return 107 | }) 108 | const nadeComponents = [] 109 | if (this.state.nades && this.state.nades.length > 0) { 110 | this.state.nades.forEach(n => { 111 | nadeComponents.push() 112 | }) 113 | } 114 | const nadeExplosions = this.state.nadeExplosions.map((n, i) => { 115 | if (n != null && n.id) { 116 | return 117 | } 118 | return null 119 | }) 120 | return ( 121 |
122 | 123 | {playerComponents} 124 | {nadeComponents} 125 | {shots} 126 | {nadeExplosions} 127 | 128 |
129 | ) 130 | } 131 | } 132 | 133 | export default Map2d 134 | -------------------------------------------------------------------------------- /web/player/src/map/MapBomb.css: -------------------------------------------------------------------------------- 1 | .mapBomb { 2 | /* scale size */ 3 | width: .5%; 4 | height: .5%; 5 | /* and center it */ 6 | margin-left: -.25%; 7 | margin-top: -.25%; 8 | /*make it round*/ 9 | border-radius: 50%; 10 | 11 | transition: 60ms linear; 12 | 13 | position: absolute; 14 | opacity: 1; 15 | background-color: orangered; 16 | } 17 | 18 | @keyframes planting { 19 | 0%, 50% { 20 | background-color: orangered; 21 | } 22 | 51%, 100% { 23 | background-color: darkorange; 24 | } 25 | } 26 | 27 | .mapBomb.planting { 28 | animation: planting 500ms infinite; 29 | } 30 | 31 | @keyframes planted { 32 | 0%, 25% { 33 | box-shadow: 0 0 1vh 1vh darkorange; 34 | } 35 | 26%, 100% { 36 | box-shadow: none; 37 | } 38 | } 39 | 40 | .mapBomb.planted { 41 | animation: planted 500ms infinite; 42 | } 43 | 44 | @keyframes defusing { 45 | 0%, 75% { 46 | box-shadow: none; 47 | } 48 | 76%, 100% { 49 | box-shadow: 0 0 1vh 1vh darkorange; 50 | } 51 | } 52 | 53 | .mapBomb.defusing { 54 | animation: planted 500ms infinite; 55 | background-color: deepskyblue; 56 | } 57 | 58 | @keyframes defused { 59 | 0% { 60 | box-shadow: 0 0 1vh 1vh deepskyblue; 61 | } 62 | 100% { 63 | box-shadow: none; 64 | } 65 | } 66 | 67 | .mapBomb.defused { 68 | animation: defused 2000ms; 69 | background-color: deepskyblue; 70 | } 71 | 72 | @keyframes explode { 73 | 0% { 74 | box-shadow: none; 75 | } 76 | 77 | 10%, 15% { 78 | box-shadow: 0 0 5vh 10vh darkorange; 79 | } 80 | 81 | 95% { 82 | box-shadow: 0 0 10vh 10vh darkorange; 83 | } 84 | 85 | 100% { 86 | box-shadow: none; 87 | } 88 | } 89 | 90 | .mapBomb.explode { 91 | animation: explode 5000ms linear; 92 | } 93 | -------------------------------------------------------------------------------- /web/player/src/map/MapBomb.js: -------------------------------------------------------------------------------- 1 | import "./MapBomb.css" 2 | import {Component} from "react"; 3 | 4 | const bombStateClasses = { 5 | 0: "", 6 | 1: "defusing", 7 | 2: "defused", 8 | 3: "explode", 9 | 4: "planting", 10 | 5: "planted", 11 | }; 12 | 13 | class MapBomb extends Component { 14 | render() { 15 | const style = { 16 | left: `${this.props.bomb.x}%`, 17 | top: `${this.props.bomb.y}%`, 18 | } 19 | // console.log(this.props.bomb.state) 20 | 21 | return ( 22 |
23 |   24 |
25 | ); 26 | } 27 | } 28 | 29 | export default MapBomb 30 | -------------------------------------------------------------------------------- /web/player/src/map/MapNade.css: -------------------------------------------------------------------------------- 1 | 2 | .mapNade { 3 | /* scale size */ 4 | width: 1%; 5 | height: 1%; 6 | /* and center it */ 7 | margin-left: -.5%; 8 | margin-top: -.5%; 9 | /*make it round*/ 10 | border-radius: 50%; 11 | 12 | transition: 60ms linear; 13 | 14 | position: absolute; 15 | box-shadow: 0 0 1vh 0 var(--BlackColor); 16 | opacity: .8; 17 | background-size: contain; 18 | background-repeat: no-repeat; 19 | background-position: center; 20 | } 21 | 22 | .mapNade .he { 23 | background-color: green; 24 | } 25 | 26 | .mapNade .smoke { 27 | background-color: grey; 28 | } 29 | 30 | .mapNade .molotov, .mapNade .incendiary { 31 | background-color: orangered; 32 | } 33 | 34 | .mapNade .decoy { 35 | background-color: yellow; 36 | } 37 | 38 | .mapNade .flash { 39 | background-color: lightblue; 40 | } 41 | 42 | .fire.explode { 43 | box-shadow: 0 0 1vh 1vh orangered; 44 | transition: box-shadow 0.2s linear; 45 | background: orangered; 46 | opacity: .8; 47 | } 48 | 49 | .flash.explode { 50 | box-shadow: 0 0 1vh 2vh whitesmoke; 51 | transition: box-shadow 0.05s linear; 52 | background: whitesmoke; 53 | opacity: .8; 54 | } 55 | 56 | .he.explode { 57 | box-shadow: 0 0 2vh 2vh orangered; 58 | transition: box-shadow 0.2s linear; 59 | background: orangered; 60 | opacity: .8; 61 | } 62 | 63 | .smoke.explode { 64 | transition: box-shadow 0.2s linear; 65 | background: darkgray; 66 | opacity: .6; 67 | box-shadow: 0 0 1vh 1vh darkgray; 68 | width: 5%; 69 | height: 5%; 70 | margin-left: -2.5%; 71 | margin-top: -2.5%; 72 | } 73 | 74 | .decoy.explode { 75 | animation: decoyPop .5s linear infinite; 76 | } 77 | 78 | @keyframes decoyPop { 79 | 0% { 80 | box-shadow: none; 81 | } 82 | 83 | 10% { 84 | box-shadow: 0 0 .5vh 1vh sandybrown; 85 | } 86 | 87 | 20% { 88 | box-shadow: none; 89 | } 90 | } 91 | 92 | .fire.explode { 93 | transition: box-shadow 0.2s linear; 94 | background: darkorange; 95 | opacity: .5; 96 | width: .5%; 97 | height: .5%; 98 | margin-left: -.25%; 99 | margin-top: -.25%; 100 | animation: firing 1s linear infinite; 101 | } 102 | 103 | @keyframes firing { 104 | 0% { 105 | box-shadow: 0 0 0.2vh 0.3vh darkorange; 106 | } 107 | 108 | 50% { 109 | box-shadow: 0 0 0.3vh 0.5vh darkorange; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /web/player/src/map/MapNade.js: -------------------------------------------------------------------------------- 1 | import "./MapNade.css" 2 | import {Component} from "react"; 3 | 4 | class MapNade extends Component { 5 | componentDidMount() { 6 | if (this.props.hide) { 7 | setTimeout(function () { 8 | this.props.removeCallback(this.props.index) 9 | }.bind(this), 300) 10 | } 11 | } 12 | 13 | render() { 14 | const className = `mapNade ${this.props.nade.kind} ${this.props.nade.action}` 15 | const style = { 16 | left: `${this.props.nade.x}%`, 17 | top: `${this.props.nade.y}%`, 18 | } 19 | return ( 20 |
21 |
22 | ); 23 | } 24 | } 25 | 26 | export default MapNade 27 | -------------------------------------------------------------------------------- /web/player/src/map/MapPlayer.css: -------------------------------------------------------------------------------- 1 | .player { 2 | /* scale size */ 3 | width: 1%; 4 | height: 1%; 5 | /* and center it */ 6 | margin-left: -.5%; 7 | margin-top: -.5%; 8 | /*make it round*/ 9 | border-radius: 50%; 10 | 11 | transition: 60ms linear; 12 | 13 | position: absolute; 14 | opacity: 1; 15 | } 16 | 17 | 18 | .player.CT { 19 | box-shadow: 0 0 0 .1vh var(--CTColor); 20 | } 21 | 22 | .player.T { 23 | box-shadow: 0 0 0 .1vh var(--TColor); 24 | } 25 | 26 | .player.flashed { 27 | box-shadow: 0 0 1vh .2vh white; 28 | } 29 | 30 | .player.dead { 31 | opacity: .5; 32 | } 33 | 34 | .playerArrow { 35 | width: 100%; 36 | height: 100%; 37 | clip-path: polygon(50% 0%, 25% 30%, 75% 30%); 38 | margin: auto; 39 | position: absolute; 40 | } 41 | 42 | .playerArrowContainer { 43 | width: 100%; 44 | height: 100%; 45 | } 46 | 47 | .playerShot { 48 | transition: 100ms linear; 49 | transform-origin: top; 50 | width: 1px; 51 | height: 5%; 52 | background-color: gray; 53 | position: absolute; 54 | } 55 | 56 | .player.T, .playerArrow.T { 57 | background-color: var(--TColor); 58 | } 59 | 60 | .player.CT, .playerArrow.CT { 61 | background-color: var(--CTColor); 62 | } 63 | 64 | .playerNameTag { 65 | position: absolute; 66 | transform: translate(-35%, -210%); 67 | display: block; 68 | /*border: 1px solid black;*/ 69 | border-radius: 2px; 70 | background-color: var(--DarkColor); 71 | white-space: nowrap; 72 | padding: 0 2px; 73 | opacity: .5; 74 | z-index: -5; 75 | font-size: 1vh; 76 | } 77 | 78 | .playerMapWeapon { 79 | width: 200%; 80 | height: 100%; 81 | display: block; 82 | transform: translate(-30%, -420%); 83 | opacity: .4; 84 | z-index: -4; 85 | background-size: contain; 86 | background-repeat: no-repeat; 87 | background-position: center; 88 | } 89 | -------------------------------------------------------------------------------- /web/player/src/map/MapPlayer.js: -------------------------------------------------------------------------------- 1 | import "./MapPlayer.css" 2 | import {Component} from "react"; 3 | 4 | class MapPlayer extends Component { 5 | 6 | render() { 7 | const posStyle = { 8 | left: `${this.props.player.x}%`, 9 | top: `${this.props.player.y}%`, 10 | background: `linear-gradient(0deg, var(--${this.props.player.team}Color) ${this.props.player.hp}%, transparent 0%)`, 11 | } 12 | const rotStyle = { 13 | transform: `rotate(${this.props.player.rotation}deg) translateY(-50%)`, 14 | } 15 | const playerClass = `player 16 | ${this.props.player.team} 17 | ${this.props.player.flashed ? "flashed" : ""} 18 | ${!this.props.player.alive ? "dead" : ""}` 19 | let playerArrow 20 | if (this.props.player.alive) { 21 | playerArrow =
22 | } 23 | 24 | return ( 25 |
26 |
27 | {playerArrow} 28 |
29 |
{this.props.player.name}
30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | export default MapPlayer 37 | -------------------------------------------------------------------------------- /web/player/src/map/MapShot.css: -------------------------------------------------------------------------------- 1 | .playerShot { 2 | transition: 100ms linear; 3 | transform-origin: top; 4 | width: 1px; 5 | height: 5%; 6 | background-color: gray; 7 | position: absolute; 8 | } -------------------------------------------------------------------------------- /web/player/src/map/MapShot.js: -------------------------------------------------------------------------------- 1 | import "./MapShot.css" 2 | import {Component} from "react"; 3 | 4 | class MapShot extends Component { 5 | constructor(props) { 6 | super(props); 7 | if (this.props.shot) { 8 | this.state = { 9 | transformStyle: `rotate(${this.props.shot.rotation}deg) translateY(-100%)`, 10 | } 11 | } else { 12 | this.state = { 13 | transformStyle: "", 14 | } 15 | } 16 | } 17 | 18 | componentDidMount() { 19 | setTimeout(function () { 20 | if (this.props.shot) { 21 | this.setState({ 22 | transformStyle: `rotate(${this.props.shot.rotation}deg) translateY(-500%)`, 23 | }) 24 | } 25 | }.bind(this), 10) 26 | 27 | setTimeout(function () { 28 | this.props.removeCallback(this.props.index) 29 | }.bind(this), 100) 30 | } 31 | 32 | render() { 33 | if (this.props.shot) { 34 | const style = { 35 | top: `${this.props.shot.y}%`, 36 | left: `${this.props.shot.x}%`, 37 | transform: this.state.transformStyle, 38 | } 39 | return ( 40 |
41 |
42 | ); 43 | } else { 44 | return null 45 | } 46 | } 47 | } 48 | 49 | export default MapShot 50 | -------------------------------------------------------------------------------- /web/player/src/panel/Controls.css: -------------------------------------------------------------------------------- 1 | button { 2 | margin: 0 3px; 3 | } 4 | -------------------------------------------------------------------------------- /web/player/src/panel/Controls.js: -------------------------------------------------------------------------------- 1 | import "./Controls.css" 2 | import { Component } from "react"; 3 | import { MSG_PLAY_CHANGE, MSG_PLAY_ROUND_INCREMENT, MSG_PLAY_SPEED, MSG_PLAY_TOGGLE } from "../constants"; 4 | 5 | class Controls extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | playing: false, 10 | playingSpeed: 1, 11 | } 12 | this.messageBus = props.messageBus 13 | this.messageBus.listen([MSG_PLAY_CHANGE], function (msg) { 14 | this.setState({ 15 | playing: msg.playing, 16 | }) 17 | }.bind(this)) 18 | } 19 | 20 | togglePlay() { 21 | this.messageBus.emit({ 22 | msgtype: MSG_PLAY_TOGGLE, 23 | }) 24 | } 25 | 26 | playRoundIncrement(inc) { 27 | this.messageBus.emit({ 28 | msgtype: MSG_PLAY_ROUND_INCREMENT, 29 | increment: inc, 30 | }) 31 | } 32 | 33 | playSpeed(speed) { 34 | this.setState({ 35 | playingSpeed: speed 36 | }) 37 | this.messageBus.emit({ 38 | msgtype: MSG_PLAY_SPEED, 39 | speed: speed, 40 | }) 41 | } 42 | 43 | togglePlaySpeed(speed) { 44 | if (this.state.playingSpeed === speed) { 45 | this.playSpeed(1) 46 | } else { 47 | this.playSpeed(speed) 48 | } 49 | } 50 | 51 | render() { 52 | const playButton = this.state.playing ? String.fromCodePoint(0xe034) : String.fromCodePoint(0xe037) 53 | 54 | return ( 55 |
56 |
 
57 |
58 | 61 | 64 | 69 | 72 |
73 |
74 | {/* 77 | */} 80 |
81 |
82 | ) 83 | } 84 | } 85 | 86 | export default Controls 87 | -------------------------------------------------------------------------------- /web/player/src/panel/InfoPanel.js: -------------------------------------------------------------------------------- 1 | import {Component} from "react"; 2 | import LoadingProgressBar from "./LoadingProgressBar"; 3 | import Scoreboard from "./Scoreboard"; 4 | import RoundNav from "./RoundNav"; 5 | import Controls from "./Controls"; 6 | import Timer from "./Timer"; 7 | import PlayerList from "./PlayerList"; 8 | 9 | class InfoPanel extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.messageBus = props.messageBus 13 | } 14 | 15 | render() { 16 | return
17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 | } 26 | } 27 | 28 | export default InfoPanel 29 | -------------------------------------------------------------------------------- /web/player/src/panel/LoadingProgressBar.js: -------------------------------------------------------------------------------- 1 | import {Component} from "react"; 2 | 3 | class LoadingProgressBar extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | hidden: false, 8 | progress: 0, 9 | message: "", 10 | } 11 | props.messageBus.listen([5, 7], function (msg) { 12 | switch (msg.msgtype) { 13 | case 5: 14 | this.setState({ 15 | hidden: true, 16 | }) 17 | break 18 | case 7: 19 | this.setState({ 20 | hidden: false, 21 | progress: msg.progress.progress, 22 | message: msg.progress.message, 23 | }) 24 | break 25 | default: 26 | console.log("unknown message [LoadingProgressBar.js]", msg) 27 | } 28 | }.bind(this)); 29 | } 30 | 31 | render() { 32 | if (this.state.hidden) { 33 | return 34 | } 35 | const progress = { 36 | width: `${this.state.progress}%` 37 | } 38 | return ( 39 |
40 |
41 |
42 |
43 |
44 |
45 | {this.state.message} 46 |
47 | ); 48 | } 49 | } 50 | 51 | export default LoadingProgressBar -------------------------------------------------------------------------------- /web/player/src/panel/PlayerList.js: -------------------------------------------------------------------------------- 1 | import {Component} from "react"; 2 | import PlayerListItem from "./PlayerListItem"; 3 | 4 | class PlayerList extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.messageBus = this.props.messageBus 8 | this.messageBus.listen([1], this.update.bind(this)) 9 | this.state = { 10 | players: [], 11 | } 12 | } 13 | 14 | update(msg) { 15 | this.setState({ 16 | players: msg.tickstate.playersList, 17 | }) 18 | } 19 | 20 | render() { 21 | const players = {"T": [], "CT": []} 22 | if (this.state.players && this.state.players.length > 0) { 23 | this.state.players.forEach(p => { 24 | players[p.team].push() 25 | }) 26 | } 27 | return
28 |
29 |
30 | {players.T} 31 |
32 |
33 |
34 |
35 | {players.CT} 36 |
37 |
38 |
39 | } 40 | } 41 | 42 | export default PlayerList 43 | -------------------------------------------------------------------------------- /web/player/src/panel/PlayerListItem.css: -------------------------------------------------------------------------------- 1 | .playerListItem { 2 | position: relative; 3 | text-align: left; 4 | } 5 | 6 | .playerListHp { 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | z-index: 1; 11 | } 12 | 13 | .playerListHpValue { 14 | background-color: var(--TColor); 15 | height: 100%; 16 | z-index: 1; 17 | } 18 | 19 | .playerListHpValue.T { 20 | background-color: var(--TColor); 21 | float: left; 22 | } 23 | 24 | .playerListHpValue.CT { 25 | background-color: var(--CTColor); 26 | float: right; 27 | } 28 | 29 | .playerListItemName, .playerListHpText, .playerListVest { 30 | color: white; 31 | z-index: 5; 32 | position: relative; 33 | font-weight: bold; 34 | } 35 | 36 | .playerListItemName { 37 | padding: 0 5%; 38 | } 39 | 40 | .playerListHpText { 41 | padding: 0; 42 | text-align: center; 43 | } 44 | 45 | .layerListItemName.T { 46 | text-align: left; 47 | } 48 | 49 | .playerListItemName.CT { 50 | } 51 | 52 | .playerListWeapons { 53 | padding: 5px 0; 54 | background-color: #222; 55 | } 56 | 57 | .playerListWeapons > div { 58 | background-size: contain; 59 | background-repeat: no-repeat; 60 | background-position: center; 61 | filter: brightness(.5); 62 | } 63 | 64 | .playerListWeapons > div.active { 65 | filter: brightness(1); 66 | } 67 | 68 | .playerListItemContainer { 69 | border: 1px solid var(--BlackColor); 70 | } 71 | 72 | .bckg { 73 | background-size: contain; 74 | background-repeat: no-repeat; 75 | background-position: center; 76 | } 77 | 78 | .playerListWeapons .c4 { 79 | filter: brightness(.7) !important; 80 | } 81 | 82 | .playerListWeapons .c4.active { 83 | filter: brightness(1.5) !important; 84 | } 85 | -------------------------------------------------------------------------------- /web/player/src/panel/PlayerListItem.js: -------------------------------------------------------------------------------- 1 | import {Component} from "react"; 2 | import "./PlayerListItem.css" 3 | 4 | class PlayerListItem extends Component { 5 | 6 | render() { 7 | // console.log(this.props.player) 8 | let armor = "" 9 | if (this.props.player.armor > 0) { 10 | if (this.props.player.helmet) { 11 | armor = "vesthelm" 12 | } else { 13 | armor = "vest" 14 | } 15 | } 16 | 17 | const nades = [] 18 | for (let gi = 0; gi < 4; gi++) { 19 | if (this.props.player.grenadesList && gi < this.props.player.grenadesList.length) { 20 | const active = this.props.player.weapon === this.props.player.grenadesList[gi] ? "active" : "" 21 | nades.push(
 
) 22 | } else { 23 | nades.push(
 
) 24 | } 25 | } 26 | 27 | return ( 28 |
29 |
30 |
31 |
33 |
34 |
{this.props.player.name}
35 |
 
36 |
40 | {this.props.player.alive ? this.props.player.hp : ""}  41 |
42 |
43 |
44 |
 
49 |
 
53 |
 
57 |
 
61 | {nades} 62 |
63 |
64 |
 
65 |
66 | {this.props.player.primary ? 67 | `${this.props.player.primaryammomagazine}/${this.props.player.primaryammoreserve}` 68 | : ""} 69 |   70 |
71 |
72 | {this.props.player.secondary ? 73 | `${this.props.player.secondaryammomagazine}/${this.props.player.secondaryammoreserve}` 74 | : ""} 75 |   76 |
77 |
{this.props.player.money}$ 78 |
79 |
80 |
81 | ) 82 | } 83 | } 84 | 85 | export default PlayerListItem 86 | -------------------------------------------------------------------------------- /web/player/src/panel/RoundNav.css: -------------------------------------------------------------------------------- 1 | .roundNav { 2 | width: 6%; 3 | margin: 1px; 4 | color: var(--BlackColor) !important; 5 | } 6 | 7 | .roundNav.T { 8 | background-color: var(--TColor); 9 | } 10 | 11 | .roundNav.CT { 12 | background-color: var(--CTColor); 13 | } 14 | 15 | .roundNav.active { 16 | filter: brightness(1.5); 17 | font-weight: bold; 18 | } 19 | -------------------------------------------------------------------------------- /web/player/src/panel/RoundNav.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { MSG_INIT_ROUNDS, MSG_PLAY, MSG_PLAY_ROUND_UPDATE } from "../constants"; 3 | import "./RoundNav.css"; 4 | 5 | class RoundNav extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | rounds: [], 10 | } 11 | this.messageBus = props.messageBus 12 | this.messageBus.listen([MSG_INIT_ROUNDS], function (msg) { 13 | let rounds = [] 14 | msg.rounds.forEach(r => { 15 | rounds.push() 17 | if (r.roundno <= 24 && r.roundno % 12 === 0) { 18 | rounds.push(
) 19 | } else if (r.roundno > 24 && r.roundno % 6 === 0) { 20 | rounds.push(
) 21 | } 22 | }) 23 | this.setState({ 24 | rounds: rounds, 25 | } 26 | ) 27 | }.bind(this)) 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 | {this.state.rounds} 34 |
35 | ); 36 | } 37 | } 38 | 39 | export default RoundNav 40 | 41 | class Round extends Component { 42 | constructor(props) { 43 | super(props); 44 | this.state = { 45 | active: false, 46 | } 47 | this.messageBus = props.messageBus 48 | this.messageBus.listen([MSG_PLAY_ROUND_UPDATE], function (msg) { 49 | this.setState({ 50 | active: msg.round === this.props.roundNo, 51 | }) 52 | }.bind(this)) 53 | } 54 | 55 | playRound(round) { 56 | this.messageBus.emit({ 57 | msgtype: MSG_PLAY, 58 | round: round, 59 | }) 60 | } 61 | 62 | render() { 63 | return 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /web/player/src/panel/Scoreboard.js: -------------------------------------------------------------------------------- 1 | import {Component} from "react"; 2 | import {MSG_TEAMSTATE_UPDATE} from "../constants"; 3 | 4 | class Scoreboard extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | TName: "Terrorists", 9 | TScore: 0, 10 | CTName: "Counter Terrorists", 11 | CTScore: 0, 12 | } 13 | props.messageBus.listen([4, 5], function (msg) { 14 | // console.log(msg) 15 | this.setState({ 16 | TName: msg.init.tname, 17 | CTName: msg.init.ctname, 18 | }) 19 | }.bind(this)) 20 | 21 | props.messageBus.listen([MSG_TEAMSTATE_UPDATE], function (msg) { 22 | this.setState({ 23 | TName: msg.teamstate.tname, 24 | TScore: msg.teamstate.tscore, 25 | CTName: msg.teamstate.ctname, 26 | CTScore: msg.teamstate.ctscore, 27 | }) 28 | }.bind(this)) 29 | } 30 | 31 | render() { 32 | return ( 33 |
34 |
35 |
36 |

{this.state.TScore}

37 |
38 |
39 |

{this.state.CTScore}

40 |
41 |
42 |
43 |
44 |

{this.state.TName}

45 |
46 |
47 |

{this.state.CTName}

48 |
49 |
50 |
51 | ); 52 | } 53 | } 54 | 55 | export default Scoreboard 56 | -------------------------------------------------------------------------------- /web/player/src/panel/Timer.js: -------------------------------------------------------------------------------- 1 | import {Component} from "react"; 2 | import {MSG_PLAY, MSG_PLAY_ROUND_PROGRESS, MSG_PROGRESS_MOVE} from "../constants"; 3 | 4 | class Timer extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | time: "0:00", 9 | progress: 0, 10 | } 11 | this.messageBus = props.messageBus 12 | this.messageBus.listen([8], msg => { 13 | this.setState( 14 | { 15 | time: msg.roundtime.roundtime, 16 | } 17 | ) 18 | }) 19 | this.messageBus.listen([MSG_PLAY_ROUND_PROGRESS], msg => { 20 | this.setState( 21 | { 22 | progress: msg.progress, 23 | } 24 | ) 25 | }) 26 | } 27 | 28 | mouseMove(e) { 29 | if (e.buttons === 1) { 30 | this.moveProgress(e) 31 | } 32 | } 33 | 34 | mouseDown(e) { 35 | this.moveProgress(e) 36 | } 37 | 38 | mouseUp(e) { 39 | this.messageBus.emit({ 40 | msgtype: MSG_PLAY, 41 | }) 42 | } 43 | 44 | mouseLeave(e) { 45 | if (e.buttons === 1) { 46 | this.messageBus.emit({ 47 | msgtype: MSG_PLAY, 48 | }) 49 | } 50 | } 51 | 52 | moveProgress(e) { 53 | const barRect = e.currentTarget.getBoundingClientRect() 54 | const progressWidth = barRect.right - barRect.left 55 | const x = e.nativeEvent.x - barRect.left 56 | const progress = x / progressWidth 57 | 58 | this.setState({ 59 | progress: progress, 60 | }) 61 | this.messageBus.emit({ 62 | msgtype: MSG_PROGRESS_MOVE, 63 | progress: progress, 64 | }) 65 | } 66 | 67 | render() { 68 | const progress = { 69 | width: `${this.state.progress * 100}%` 70 | } 71 | return ( 72 |
77 |
78 | {this.state.time} 79 |
80 |
81 | ); 82 | } 83 | } 84 | 85 | export default Timer 86 | -------------------------------------------------------------------------------- /web/player/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /web/player/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | --------------------------------------------------------------------------------