├── .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 |
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 |
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 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/aug.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/awp.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/c4.svg:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/c4_exp.svg:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/c4_old.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/cz75.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/dead.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/deagle.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/decoy.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/defuse.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/duals.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/famas.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/fiveseven.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/flash.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/g3sg1.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/galil.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/glock.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/hkp2000.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/incendiary.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/knife.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/m4a1s.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/m4a4.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/mac10.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/molotov.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/mp5.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/not_found.svg:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/p90.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/para.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/smoke.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/taser.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/ump45.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/web/player/public/assets/icons/csgo/usp-s.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------