Compare commits

...
107 Commits
Author SHA1 Message Date
TrèsAbhi eed9d23646 fix: use root node modules 2025-12-01 16:58:18 -06:00
TrèsAbhi 2101dba7c6 New Crowdin updates (#184)
* New translations en.json (Russian)

* New translations en.json (Russian)

* New translations en.json (Russian)
2025-11-28 18:12:19 -06:00
TrèsAbhi 1b398a5f66 fix: initialize Tankopedia with model definitions for improved data handling 2025-11-28 12:55:55 -06:00
TrèsAbhi d8f9a2633d New Crowdin updates (#183)
* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)
2025-11-27 21:53:42 -06:00
TrèsAbhi 2e79ccf7eb feat: update ts-proto executable handling to support multiple locations 2025-11-27 17:59:34 -06:00
TrèsAbhi be778acf39 feat: logs 2025-11-27 17:56:28 -06:00
TrèsAbhi 8d10d7cffa feat: integrate CompareCell component into TankRow and Tanks for improved performance metrics display 2025-11-27 17:48:58 -06:00
TrèsAbhi 5b9041a972 feat: performance tank table occupy more real estate 2025-11-27 17:34:29 -06:00
TrèsAbhi 1e60b1f61f fix: remove unused PerformanceInfo component from performance page 2025-11-27 17:33:44 -06:00
TrèsAbhi 13a636c023 fix: update playerCountPeriod to use "total" value 2025-11-27 17:33:05 -06:00
TrèsAbhi 8bc0693f3b fix: compare table occupy minimum width 2025-11-27 17:31:28 -06:00
TrèsAbhi 9b842c501c feat: update guessing game text for clarity 2025-11-27 17:26:10 -06:00
TrèsAbhi 8aadda4fb8 feat: add ToolOpenGraph component to guess page 2025-11-27 17:25:54 -06:00
TrèsAbhi 58c70f98e1 refactor: remove 'naked' prop from Controls and TankSandbox components 2025-11-27 17:24:12 -06:00
TrèsAbhi 54b73d11ce Merge branch 'main' into dev 2025-11-27 17:16:12 -06:00
TrèsAbhi d1ac5644a4 feat: logs 2025-11-27 14:14:18 -06:00
TrèsAbhi ae35c1516c Merge branch 'main' into fix-lighting 2025-11-27 14:12:38 -06:00
TrèsAbhi fd364d434f feat: hemispehre 2025-11-27 14:10:57 -06:00
TrèsAbhi fd37f72f8c feat: some simplification 2025-11-27 13:54:36 -06:00
TrèsAbhi 1427f61c52 feat: complete transition 2025-11-27 13:42:37 -06:00
TrèsAbhi cd4fa366a0 feat: requestedDisplay 2025-11-27 13:20:06 -06:00
TrèsAbhi 344e7caa81 feat: overhead lighting 2025-11-27 13:05:08 -06:00
TrèsAbhi fe7b2aea02 Add files via upload 2025-11-27 15:57:34 +00:00
TrèsAbhi fced1e96ef feat: remove transition skeleton 2025-11-26 21:10:54 -06:00
TrèsAbhi 778efb2372 feat: transiton and reveal 2025-11-26 21:10:03 -06:00
TrèsAbhi b67fd5b58c feat: new lighting 2025-11-26 20:48:25 -06:00
TrèsAbhi 2559782a28 fix: ground phasing through 2025-11-26 20:25:45 -06:00
TrèsAbhi efb479fece fix: vfs in core 2025-11-26 19:27:58 -06:00
TrèsAbhi fb829d4b81 Merge branch 'main' into dev 2025-11-26 19:22:48 -06:00
TrèsAbhi 91f4f2edcf Comment out unchanged change log statement
Comment out the log statement for unchanged changes.
2025-11-26 18:23:25 -06:00
TrèsAbhi a692d1ee0b Merge branch 'main' into dev 2025-11-26 13:58:44 -06:00
TrèsAbhi 31d2ab4746 feat: workflow 2025-11-26 13:44:45 -06:00
TrèsAbhi e4e2c30748 feat: logs 2025-11-26 13:42:43 -06:00
TrèsAbhi d633874d90 fix: path normalization 2025-11-26 13:38:40 -06:00
TrèsAbhi 9cb1d0cb3f fix: read dvpl files 2025-11-26 13:25:46 -06:00
TrèsAbhi a0bccc7973 feat: update all 2025-11-26 13:18:44 -06:00
TrèsAbhi 3efe807e35 feat: migrate definitiosn 2025-11-26 13:07:53 -06:00
TrèsAbhi 1cc15f64b0 feat: augment 2025-11-26 12:58:19 -06:00
TrèsAbhi f70864a0db feat: vfs file 2025-11-26 12:46:03 -06:00
TrèsAbhi 5de40b220f feat: rename 2025-11-26 12:34:26 -06:00
TrèsAbhi bd19c9aac9 Merge branch 'main' into fix-mangler 2025-11-26 12:28:32 -06:00
TrèsAbhi 6bc5b7dcb6 feat: .well-known dir 2025-11-26 12:22:15 -06:00
TrèsAbhi 00795eecdd feat: sort 2025-11-25 22:21:58 -06:00
TrèsAbhi f4e7b2ed98 feat: normalize path 2025-11-25 22:20:46 -06:00
TrèsAbhi 81612793e3 feat: manifest storing 2025-11-25 22:16:39 -06:00
TrèsAbhi d3bdbc39d2 feat: unified abstraction 2025-11-25 22:14:09 -06:00
TrèsAbhi 7cb67bd44b feat: steam client 2025-11-25 21:37:31 -06:00
TrèsAbhi 9df09c9a53 rm nm 2025-11-24 13:01:07 -06:00
TrèsAbhi a9f1d9ebec disable patches 2025-11-24 12:58:36 -06:00
TrèsAbhi 1b834eb848 just remove it 2025-11-24 12:46:32 -06:00
TrèsAbhi c5518ec9bc hide others 2025-11-24 12:45:39 -06:00
TrèsAbhi 8c705d7e2e root reserve 2025-11-24 12:45:05 -06:00
TrèsAbhi efe9f91a2d hide lvm 2025-11-24 12:44:33 -06:00
TrèsAbhi e84bf6b31c str 2025-11-24 12:44:01 -06:00
TrèsAbhi 46234b6cbb disable all 2025-11-24 12:43:02 -06:00
TrèsAbhi 0bda743a91 feat: reintroduce 2025-11-24 12:38:50 -06:00
TrèsAbhi b19866a1f5 feat: remove maximize-build-space 2025-11-24 12:35:45 -06:00
TrèsAbhi 9ca329e53f feat: start vfs 2025-11-24 11:02:18 -06:00
TrèsAbhi 47b1eb9ab0 feat: always use max build space 2025-11-20 12:45:57 -06:00
TrèsAbhi 6c8fbf7245 fix: use package node modules 2025-11-13 12:53:19 -06:00
TrèsAbhi 2ea22512d4 feat: new protos 2025-11-13 12:47:51 -06:00
TrèsAbhi 697d1fa1b4 Merge branch 'main' into dev 2025-11-13 12:40:59 -06:00
TrèsAbhi e4429f15bd feat: moar!! 2025-11-12 14:18:54 -06:00
TrèsAbhi cd8083ae30 feat: more patchnotes 2025-11-12 14:10:02 -06:00
TrèsAbhi 9dcf7441c9 feat: start of patch notes 2025-11-12 13:50:17 -06:00
TrèsAbhi d4e1f99ce2 fix: mk dir 2025-11-12 11:00:02 -06:00
TrèsAbhi 98de246578 mooooooove 2025-11-12 10:24:38 -06:00
TrèsAbhi 45097f198c debug 2025-11-12 10:23:22 -06:00
TrèsAbhi e063719caa debug 2025-11-12 10:22:27 -06:00
TrèsAbhi 975d579d21 New translations en.json (Russian) (#181) 2025-11-07 16:39:13 -06:00
TrèsAbhi b963ef0981 fix: name 2025-11-06 09:54:06 -06:00
TrèsAbhi 49fab21113 fix: use relative script 2025-11-06 09:52:47 -06:00
TrèsAbhi cbde48d41b fix: PUBLIC_ASSET_BASE 2025-11-06 09:40:11 -06:00
TrèsAbhi 1210baa545 fix: env type 2025-11-06 09:39:27 -06:00
TrèsAbhi 2c03b530d2 feat: group array 2025-11-06 09:37:14 -06:00
TrèsAbhi ec977899fb New translations en.json (Russian) (#180) 2025-11-05 14:44:13 -06:00
TrèsAbhi e5fc2faad7 Update youtubers.ts 2025-11-05 14:43:21 -06:00
TrèsAbhi 541dfc186b New Crowdin updates (#179)
* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)
2025-11-03 09:45:21 -06:00
TrèsAbhi 182471c4d6 feat: buildCatalogItemAccessor 2025-11-02 18:10:11 -06:00
TrèsAbhi e5c66cf232 Update isHalloween.ts 2025-10-31 15:07:27 -05:00
TrèsAbhi fbbc0b82b4 Update Lighting.tsx 2025-10-31 08:06:11 -05:00
TrèsAbhi aecc2adb07 feat: generalize colored delta cells 2025-10-26 22:01:42 -05:00
TrèsAbhi f8ee2b64a6 feat: simplify ownership and test filters 2025-10-26 21:49:48 -05:00
TrèsAbhi 7c4e28bd09 fix: show remove played even when logged out 2025-10-26 21:43:23 -05:00
TrèsAbhi 80d61d9e04 fix: fallback 2025-10-26 21:19:42 -05:00
TrèsAbhi 0c22ac3a97 fix: step title 2025-10-26 21:06:38 -05:00
TrèsAbhi efad6e61f7 Update assets.yml 2025-10-23 22:15:56 -05:00
TrèsAbhi 3563aaecd5 fix: move depots 2025-10-23 21:49:54 -05:00
TrèsAbhi 5294de848f fix: manual flag 2025-10-23 21:29:44 -05:00
TrèsAbhi e82aa56cb2 feat: logs 2025-10-23 21:28:27 -05:00
TrèsAbhi e1b2b130a5 feat: hide uploader 2025-10-23 21:26:51 -05:00
TrèsAbhi 28f22f1d40 fix: include missing module 2025-10-23 21:22:34 -05:00
TrèsAbhi a3bb023309 feat: migrate to asset uploader 2025-10-23 21:22:21 -05:00
TrèsAbhi 797307fb81 feat: asset uploader 2025-10-23 20:29:30 -05:00
TrèsAbhi f9802b44ff feat: update readme 2025-10-23 19:48:23 -05:00
TrèsAbhi e26b9c78a3 feat: remove unnecessary submodules 2025-10-23 19:46:31 -05:00
TrèsAbhi 925ae1a0b7 fix: type 2025-10-23 19:37:14 -05:00
TrèsAbhi 9584ecf38b feat: logs 2025-10-23 19:36:15 -05:00
TrèsAbhi 338168d7e4 remove url echos 2025-10-23 15:36:28 -05:00
TrèsAbhi 711d92887e fix: naming 2025-10-23 15:27:50 -05:00
TrèsAbhi 78160041e8 feat: parallel 2025-10-23 15:18:12 -05:00
TrèsAbhi 1b884a52f9 name 2025-10-23 15:09:54 -05:00
TrèsAbhi f13c658ef4 no spaced 2025-10-23 15:09:19 -05:00
TrèsAbhi b72d4058be feat: unarchive 2025-10-23 15:01:18 -05:00
TrèsAbhi 029a781c50 fix: hardcode app path 2025-10-23 10:31:26 -05:00
TrèsAbhi adc1791f56 feat: game archive mounting 2025-10-23 10:30:21 -05:00
TrèsAbhi 15b6ff43c5 feat: handle different asset repors 2025-10-23 10:01:17 -05:00
116 changed files with 2686 additions and 2222 deletions

No files matched your search

+19 -85
View File
@@ -6,25 +6,26 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
cdn_branch: asset_repo:
type: choice
required: false
description: What asset repo to write to
options:
- blitzkit/assets
- blitzkit/assets-preview
- blitzkit/assets-dev
default: blitzkit/assets
archive_url:
type: string type: string
required: false required: false
default: main description: Archive URL to download instead of Steam client
description: What CDN branch to write to
zip_url:
type: string
required: false
description: ZIP URL to download instead of Steam client
zip_root:
type: string
required: false
description: Root dir of ZIP which houses the executable and Data dir
schedule: schedule:
- cron: "0 2 * * *" - cron: "0 2 * * *"
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.cdn_branch }} group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.asset_repo }}
cancel-in-progress: true cancel-in-progress: true
env: env:
@@ -36,8 +37,8 @@ env:
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }} WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
WOTB_DLC_CDN: ${{ secrets.WOTB_DLC_CDN }} WOTB_DLC_CDN: ${{ secrets.WOTB_DLC_CDN }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }} PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_REPO: blitzkit/assets PUBLIC_ASSET_REPO: ${{ github.event.inputs.asset_repo != '' && github.event.inputs.asset_repo || secrets.PUBLIC_ASSET_REPO }}
PUBLIC_ASSET_BRANCH: main PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }} WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }} WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }} PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
@@ -54,68 +55,13 @@ env:
PUBLIC_ASSET_FAST_REPO: assets PUBLIC_ASSET_FAST_REPO: assets
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }} PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
jobs: jobs:
download_client:
name: Download client
runs-on: ubuntu-latest
steps:
- if: github.event.inputs.zip_url != ''
uses: easimon/maximize-build-space@master
with:
root-reserve-mb: 512
swap-size-mb: 1024
remove-dotnet: "true"
- uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v1
if: github.event.inputs.zip_url == ''
- if: github.event.inputs.zip_url == ''
run: bun run setup
- name: (Steam) Setup DepotDownloader
if: github.event.inputs.zip_url == ''
run: bun run setup:depotdownloader
- name: (Steam) Download client
if: github.event.inputs.zip_url == ''
run: |
chmod +x ./temp/depotDownloader/DepotDownloader
./temp/depotDownloader/DepotDownloader -q -os windows -app 444200 -username ${{ secrets.DEPOT_DOWNLOADER_USERNAME }} -password "${{ secrets.DEPOT_DOWNLOADER_PASSWORD }}"
- name: (Steam) Apply patches
if: github.event.inputs.zip_url == ''
run: bun setup:patches --depot
- name: (ZIP) Download client
if: github.event.inputs.zip_url != ''
env:
ZIP_URL: ${{ github.event.inputs.zip_url }}
ZIP_ROOT: ${{ github.event.inputs.zip_root }}
run: |
wget -q -O client.zip $ZIP_URL
unzip -q client.zip -d client
rm client.zip
mkdir -p depots/444202/0
mv client/$ZIP_ROOT/* depots/444202/0
- name: Archive depots
uses: actions/upload-artifact@v4
with:
name: depots
path: depots
build_assets: build_assets:
name: Build assets name: Build assets
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: download_client
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -128,20 +74,8 @@ jobs:
- uses: oven-sh/setup-bun@v1 - uses: oven-sh/setup-bun@v1
- run: bun run setup - run: bun run setup
- name: Download depots
uses: actions/download-artifact@v4
with:
name: depots
path: depots
- name: AppArmor hack - name: AppArmor hack
run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
- name: Start server and build assets - name: Build assets
run: | run: bun run build:assets --all-targets --manual
bun dev:website --verbosepa &
SERVER_PID=$!
bun run build:assets --all-targets --depot --production
kill $SERVER_PID || true
+2 -1
View File
@@ -17,6 +17,7 @@ env:
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }} PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
jobs: jobs:
build: build:
@@ -34,4 +35,4 @@ jobs:
- run: bun run setup - run: bun run setup
- name: Find reviews - name: Find reviews
run: bun packages/scripts/src/reviews.ts run: bun run build:reviews
-6
View File
@@ -1,9 +1,3 @@
[submodule "submodules/CUE4Parse"]
path = submodules/CUE4Parse
url = https://github.com/FabianFG/CUE4Parse
[submodule "submodules/blitzkit-internal"]
path = submodules/blitzkit-internal
url = https://github.com/blitzkit/internal
[submodule "packages/closed"] [submodule "packages/closed"]
path = packages/closed path = packages/closed
url = https://github.com/blitzkit/closed.git url = https://github.com/blitzkit/closed.git
+12
View File
@@ -11,6 +11,7 @@
"analyticsreporting", "analyticsreporting",
"APAC", "APAC",
"apcr", "apcr",
"appinfo",
"applypatches", "applypatches",
"artefacts", "artefacts",
"astrojs", "astrojs",
@@ -64,8 +65,10 @@
"deburr", "deburr",
"decalmask", "decalmask",
"decompressor", "decompressor",
"dedup",
"defence", "defence",
"degressive", "degressive",
"depotdownloader",
"devkrieg", "devkrieg",
"diacriticless", "diacriticless",
"DIDS", "DIDS",
@@ -76,6 +79,7 @@
"DVPLCRC", "DVPLCRC",
"dvpm", "dvpm",
"dxgi", "dxgi",
"easimon",
"echarts", "echarts",
"Everytime", "Everytime",
"FASTNAME", "FASTNAME",
@@ -108,6 +112,7 @@
"leaderboards", "leaderboards",
"lerp", "lerp",
"LINEARSIZE", "LINEARSIZE",
"linktarget",
"lmao", "lmao",
"loadouts", "loadouts",
"loddist", "loddist",
@@ -141,11 +146,13 @@
"Oopsy", "Oopsy",
"opentest", "opentest",
"OPTML", "OPTML",
"otemp",
"outdir", "outdir",
"outfile", "outfile",
"ownedtanks", "ownedtanks",
"padder", "padder",
"partytown", "partytown",
"patchnotes",
"PIXELFORMAT", "PIXELFORMAT",
"playerachievements", "playerachievements",
"playerinfo", "playerinfo",
@@ -160,10 +167,13 @@
"protobuf", "protobuf",
"protoc", "protoc",
"protos", "protos",
"proxyclient",
"prunestrings",
"Pyogenics", "Pyogenics",
"ratingleaderboard", "ratingleaderboard",
"raycaster", "raycaster",
"Redirector", "Redirector",
"Relogin",
"researchable", "researchable",
"Resvg", "Resvg",
"rizz", "rizz",
@@ -212,12 +222,14 @@
"Unregistering", "Unregistering",
"unrotate", "unrotate",
"unwrapper", "unwrapper",
"userns",
"vercel", "vercel",
"vickers", "vickers",
"vitepress", "vitepress",
"Vovko", "Vovko",
"wargaming", "wargaming",
"webp", "webp",
"wgpkg",
"whitespaces", "whitespaces",
"WHSS", "WHSS",
"winrate", "winrate",
+5 -7
View File
@@ -16,22 +16,20 @@ If you intend to contribute to the code, note that you may need to check out sen
| [packages/i18n](packages/i18n) | | [Crowdin](https://crowdin.com/project/blitzkrieg) | Translations served as JSON shared across all other packages, including some utility code. | | [packages/i18n](packages/i18n) | | [Crowdin](https://crowdin.com/project/blitzkrieg) | Translations served as JSON shared across all other packages, including some utility code. |
| [packages/scripts](packages/scripts) | | | CLI utilities for the development and deployment of BlitzKit services. | | [packages/scripts](packages/scripts) | | | CLI utilities for the development and deployment of BlitzKit services. |
| [packages/varuna](packages/varuna) | Varuna | [NPM](https://www.npmjs.com/package/varuna) | React and Astro-oriented state management library, awaiting separation from BlitzKit. Unavailable from NPM at this time. | | [packages/varuna](packages/varuna) | Varuna | [NPM](https://www.npmjs.com/package/varuna) | React and Astro-oriented state management library, awaiting separation from BlitzKit. Unavailable from NPM at this time. |
| [packages/website](packages/website) | BlitzKit | https://blitzkit.app | The BlitzKit website provides a wide range of tools, APIs, and interconnects all other services. | | [packages/website](packages/website) | BlitzKit | https://blitzkit.app | The BlitzKit website provides a wide range of tools, APIs, and interconnects all other services. |
## Submodules ## Submodules
| Submodule | Title | Source | Memo | | Submodule | Title | Source | Memo |
| ------------------------------------------------------------ | ----------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------------------------ | --------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [submodules/blitzkit-closed](submodules/blitzkit-closed) | ClosedKit | [blitzkit/closed](https://github.com/blitzkit/closed) | Closed source, sensitive code hidden from the public to prevent bad actors from abusing the game. Discuss with `@tresabhi` on Discord for **special permission** to access. | | [submodules/blitzkit-closed](packages/blitzkit-closed) | ClosedKit | [blitzkit/closed](https://github.com/blitzkit/closed) | Closed source, sensitive code hidden from the public to prevent bad actors from abusing the game. Discuss with `@tresabhi` on Discord for **special permission** to access. |
| [submodules/blitzkit-internal](submodules/blitzkit-internal) | InternalKit | [blitzkit/internal](https://github.com/blitzkit/internal) | Closed source, sensitive internal documentation hidden from the public to prevent bad actors from abusing the game. Access to this submodule is **not negotiable**. |
| [submodules/CUE4Parse](submodules/CUE4Parse) | CUE4Parse | [FabianFG/CUE4Parse](https://github.com/FabianFG/CUE4Parse) | An Unreal Engine archives and packages parsing library in C#. |
## Environment Variables ## Environment Variables
| Name | Source | BlitzKit Default | Memo | | Name | Source | BlitzKit Default | Memo |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `PUBLIC_WARGAMING_APPLICATION_ID` | [Wargaming Developer's Room](https://developers.wargaming.net/) | | Application ID needed to communicate with Wargaming APIs. | | `PUBLIC_WARGAMING_APPLICATION_ID` | [Wargaming Developer's Room](https://developers.wargaming.net/) | | Application ID needed to communicate with Wargaming APIs. |
| `PUBLIC_PROMOTE_OPENTEST` | | `false` | Optional boolean dictating whether the website advertises [the opentest subdomain of BlitzKit](https://opentest.blitzkit.app/). | | `PUBLIC_PROMOTE_OPENTEST` | | `false` | Optional boolean dictating whether the website advertises [the opentest subdomain of BlitzKit](https://opentest.blitzkit.app/). |
| `ALLOW_ROBOTS` | | `true` | Toggles between `Allow: /` and `Disallow: /` in the website's `/robots.txt`. This is usually `false` for all BlitzKit's subdomains. | | `ALLOW_ROBOTS` | | `true` | Toggles between `Allow: /` and `Disallow: /` in the website's `/robots.txt`. This is usually `false` for all BlitzKit's subdomains. |
| `WOTB_GLOSSARY`, `WOTB_GLOSSARY_DOMAIN`, `WOTB_GLOSSARY_PATH` | | | | | `WOTB_GLOSSARY`, `WOTB_GLOSSARY_DOMAIN`, `WOTB_GLOSSARY_PATH` | | | |
| `PUBLIC_GOOGLE_TAG_MANAGER_ID` | [Google Tag Manager](https://tagmanager.google.com/) | `G-GL2JVHCGPQ` | Google Tag Manager's unique identifier for analytical purposes. | | `PUBLIC_GOOGLE_TAG_MANAGER_ID` | [Google Tag Manager](https://tagmanager.google.com/) | `G-GL2JVHCGPQ` | Google Tag Manager's unique identifier for analytical purposes. |
+99 -9
View File
@@ -50,7 +50,7 @@
"name": "@blitzkit/closed", "name": "@blitzkit/closed",
"dependencies": { "dependencies": {
"@blitzkit/core": "workspace:*", "@blitzkit/core": "workspace:*",
"@bufbuild/protobuf": "^2.9.0", "@bufbuild/protobuf": "^2.10.0",
"@types/lz4js": "^0.2.1", "@types/lz4js": "^0.2.1",
"@types/pako": "^2.0.4", "@types/pako": "^2.0.4",
"lz4js": "^0.2.0", "lz4js": "^0.2.0",
@@ -66,12 +66,14 @@
"@octokit/rest": "^22.0.0", "@octokit/rest": "^22.0.0",
"@prisma/client": "^6.16.2", "@prisma/client": "^6.16.2",
"@types/adm-zip": "^0.5.7", "@types/adm-zip": "^0.5.7",
"@types/bun": "^1.3.1",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1", "@types/lz4js": "^0.2.1",
"@types/three": "^0.180.0", "@types/three": "^0.180.0",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"crc": "^4.3.2", "crc": "^4.3.2",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"fast-xml-parser": "^5.3.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lz4js": "^0.2.0", "lz4js": "^0.2.0",
"pickleparser": "^0.2.1", "pickleparser": "^0.2.1",
@@ -103,6 +105,7 @@
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1", "@types/lz4js": "^0.2.1",
"@types/progress": "^2.0.7", "@types/progress": "^2.0.7",
"@types/steam-user": "^5.1.0",
"@types/three": "^0.180.0", "@types/three": "^0.180.0",
"crc": "^4.3.2", "crc": "^4.3.2",
"csv-parse": "^6.1.0", "csv-parse": "^6.1.0",
@@ -116,6 +119,7 @@
"progress": "^2.0.3", "progress": "^2.0.3",
"puppeteer": "^24.22.3", "puppeteer": "^24.22.3",
"sharp": "^0.34.4", "sharp": "^0.34.4",
"steam-user": "^5.2.3",
"three": "^0.180.0", "three": "^0.180.0",
"ts-proto": "^2.7.7", "ts-proto": "^2.7.7",
"yaml": "^2.8.1", "yaml": "^2.8.1",
@@ -381,6 +385,10 @@
"@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], "@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@bbob/parser": ["@bbob/[email protected]", "", { "dependencies": { "@bbob/plugin-helper": "^2.9.0" } }, "sha512-tldSYsMoEclke/B1nqL7+HbYMWZHTKvpbEHRSHuY+sZvS1o7Jpdfjb+KPpwP9wLI3p3r7GPv69/wGy+Xibs9yA=="],
"@bbob/plugin-helper": ["@bbob/[email protected]", "", {}, "sha512-idpUcNQ2co6T1oU/7/DG/ZRfipSSkTn9Ozw9f5vaXH7nzV3qhqZnhFVlHTzGGnRlzKlBwWOBzOdWi4Zeqg1c5A=="],
"@blitzkit/app": ["@blitzkit/app@workspace:packages/app"], "@blitzkit/app": ["@blitzkit/app@workspace:packages/app"],
"@blitzkit/bot": ["@blitzkit/bot@workspace:packages/bot"], "@blitzkit/bot": ["@blitzkit/bot@workspace:packages/bot"],
@@ -395,7 +403,7 @@
"@blitzkit/website": ["@blitzkit/website@workspace:packages/website"], "@blitzkit/website": ["@blitzkit/website@workspace:packages/website"],
"@bufbuild/protobuf": ["@bufbuild/protobuf@2.9.0", "", {}, "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA=="], "@bufbuild/protobuf": ["@bufbuild/protobuf@2.10.0", "", {}, "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ=="],
"@capsizecss/unpack": ["@capsizecss/[email protected]", "", { "dependencies": { "fontkit": "^2.0.2" } }, "sha512-+ntATQe1AlL7nTOYjwjj6w3299CgRot48wL761TUGYpYgAou3AaONZazp0PKZyCyWhudWsjhq1nvRHOvbMzhTA=="], "@capsizecss/unpack": ["@capsizecss/[email protected]", "", { "dependencies": { "fontkit": "^2.0.2" } }, "sha512-+ntATQe1AlL7nTOYjwjj6w3299CgRot48wL761TUGYpYgAou3AaONZazp0PKZyCyWhudWsjhq1nvRHOvbMzhTA=="],
@@ -413,6 +421,12 @@
"@discordjs/ws": ["@discordjs/[email protected]", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], "@discordjs/ws": ["@discordjs/[email protected]", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
"@doctormckay/stdlib": ["@doctormckay/[email protected]", "", { "dependencies": { "psl": "^1.9.0" } }, "sha512-bwy+gPn6oa2KTpfxJKX3leZoV/wHDVtO0/gq3usPvqPswG//dcf3jVB8LcbRRsKO3BXCt5DqctOQ+Xb07ivxnw=="],
"@doctormckay/steam-crypto": ["@doctormckay/[email protected]", "", {}, "sha512-lsxgLw640gEdZBOXpVIcYWcYD+V+QbtEsMPzRvjmjz2XXKc7QeEMyHL07yOFRmay+cUwO4ObKTJO0dSInEuq5g=="],
"@doctormckay/user-agents": ["@doctormckay/[email protected]", "", {}, "sha512-F+sL1YmebZTY2CnjoR9BXFEULpq7y8dxyLx48LZVa0BSDseXdLG/DtPISfM1iNv1XKCeiBzVNfAT/MOQ69v1Zw=="],
"@emmetio/abbreviation": ["@emmetio/[email protected]", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="], "@emmetio/abbreviation": ["@emmetio/[email protected]", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="],
"@emmetio/css-abbreviation": ["@emmetio/[email protected]", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="], "@emmetio/css-abbreviation": ["@emmetio/[email protected]", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="],
@@ -911,7 +925,9 @@
"@types/babel__traverse": ["@types/[email protected]", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], "@types/babel__traverse": ["@types/[email protected]", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/bun": ["@types/[email protected].0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], "@types/bun": ["@types/[email protected].1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
"@types/bytebuffer": ["@types/[email protected]", "", { "dependencies": { "@types/long": "^3.0.0", "@types/node": "*" } }, "sha512-lV4YLiolMdD4upDmr4vnfiwV/FN9Jg33eNTSFMkHqyMKqTIAn5TmFTYsARwXwUXeFU7yzRpECmWV1SOhIvdkRQ=="],
"@types/debug": ["@types/[email protected]", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/debug": ["@types/[email protected]", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
@@ -921,6 +937,8 @@
"@types/estree": ["@types/[email protected]", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/[email protected]", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/file-manager": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-gEq6de+iMbjmkL3Wj183JQ651h9b331wQ3svXp2K6RkgBg+TtiyIZTMols/a3UtXllVV9kSelz/N3Rgh0n6S4g=="],
"@types/fontkit": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew=="], "@types/fontkit": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew=="],
"@types/hast": ["@types/[email protected]", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], "@types/hast": ["@types/[email protected]", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
@@ -929,6 +947,8 @@
"@types/lodash-es": ["@types/[email protected]", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="], "@types/lodash-es": ["@types/[email protected]", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
"@types/long": ["@types/[email protected]", "", {}, "sha512-ZXyOOm83p7X8p3s0IYM3VeueNmHpkk/yMlP8CLeOnEcu6hIwPH7YjZBvhQkR0ZFS2DqZAxKtJ/M5fcuv3OU5BA=="],
"@types/lz4js": ["@types/[email protected]", "", {}, "sha512-aAnbA4uKPNqZqu0XK1QAwKP0Wskb4Oa7ZFqxW5CMIyGgqYQKFgBxTfK3m3KODXoOLv5t14VregzgrEak13uGQA=="], "@types/lz4js": ["@types/[email protected]", "", {}, "sha512-aAnbA4uKPNqZqu0XK1QAwKP0Wskb4Oa7ZFqxW5CMIyGgqYQKFgBxTfK3m3KODXoOLv5t14VregzgrEak13uGQA=="],
"@types/markdown-escape": ["@types/[email protected]", "", {}, "sha512-JIc1+s3y5ujKnt/+N+wq6s/QdL2qZ11fP79MijrVXsAAnzSxCbT2j/3prHRouJdZ2yFLN3vkP0HytfnoCczjOw=="], "@types/markdown-escape": ["@types/[email protected]", "", {}, "sha512-JIc1+s3y5ujKnt/+N+wq6s/QdL2qZ11fP79MijrVXsAAnzSxCbT2j/3prHRouJdZ2yFLN3vkP0HytfnoCczjOw=="],
@@ -959,6 +979,10 @@
"@types/stats.js": ["@types/[email protected]", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], "@types/stats.js": ["@types/[email protected]", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="],
"@types/steam-user": ["@types/[email protected]", "", { "dependencies": { "@types/bytebuffer": "*", "@types/file-manager": "*", "@types/node": "*", "@types/steamid": "*" } }, "sha512-jMCzwmvBA1DaqLTD9h23LzVmzyHy53cdzpJjBSYDI6qT/1LqEUM9xRt4Vb0Y9kOIr+BZckCewDw8Bk6n8e3BTA=="],
"@types/steamid": ["@types/[email protected]", "", {}, "sha512-LC0oaiNq3gqMI18MV935CnlyHmDooKRszK56jHv0+b7EbJwf5k++YsD52zWeJUD8nuYqP9LpIgpXGiYBOxpcYQ=="],
"@types/three": ["@types/[email protected]", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.22.0" } }, "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg=="], "@types/three": ["@types/[email protected]", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.22.0" } }, "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg=="],
"@types/trusted-types": ["@types/[email protected]", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/trusted-types": ["@types/[email protected]", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
@@ -1005,7 +1029,7 @@
"adm-zip": ["[email protected]", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], "adm-zip": ["[email protected]", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
@@ -1085,6 +1109,8 @@
"bignumber.js": ["[email protected]", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "bignumber.js": ["[email protected]", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"binarykvparser": ["[email protected]", "", { "dependencies": { "long": "^3.2.0" } }, "sha512-B1N5ZxC8I9oSLis7Rg36DxsZJoIikUGU2XwpI0FKFCaPIJIEYi0B9UeIk3QU006axzq0TI9KC3iXelfGGgnWew=="],
"bl": ["[email protected]", "", { "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" } }, "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww=="], "bl": ["[email protected]", "", { "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" } }, "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww=="],
"boolbase": ["[email protected]", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "boolbase": ["[email protected]", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
@@ -1115,7 +1141,9 @@
"buffer-from": ["[email protected]", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "buffer-from": ["[email protected]", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["[email protected].0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], "bun-types": ["[email protected].1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
"bytebuffer": ["[email protected]", "", { "dependencies": { "long": "~3" } }, "sha512-IuzSdmADppkZ6DlpycMkm8l9zeEq16fWtLvunEwFiYciR/BHo4E8/xs5piFquG+Za8OWmMqHF8zuRviz2LHvRQ=="],
"c12": ["[email protected]", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], "c12": ["[email protected]", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="],
@@ -1237,6 +1265,8 @@
"csv-parse": ["[email protected]", "", {}, "sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw=="], "csv-parse": ["[email protected]", "", {}, "sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw=="],
"cuint": ["[email protected]", "", {}, "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw=="],
"cwise-compiler": ["[email protected]", "", { "dependencies": { "uniq": "^1.0.0" } }, "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ=="], "cwise-compiler": ["[email protected]", "", { "dependencies": { "uniq": "^1.0.0" } }, "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ=="],
"data-uri-to-buffer": ["[email protected]", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "data-uri-to-buffer": ["[email protected]", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
@@ -1409,7 +1439,7 @@
"fast-uri": ["[email protected]", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fast-uri": ["[email protected]", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], "fast-xml-parser": ["fast-xml-parser@5.3.2", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-n8v8b6p4Z1sMgqRmqLJm3awW4NX7NkaKPfb3uJIBTSH7Pdvufi3PQ3/lJLQrvxcMYl7JI2jnDO90siPEpD8JBA=="],
"fastq": ["[email protected]", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fastq": ["[email protected]", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
@@ -1423,6 +1453,8 @@
"fflate": ["[email protected]", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], "fflate": ["[email protected]", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-manager": ["[email protected]", "", { "dependencies": { "@doctormckay/stdlib": "^1.14.1" } }, "sha512-y/K/1OCha04OXOxzo3cXJYtIzEk/CUMBb7Okipxueu0u+xCiuoocbwPyh1smUBasOobo4GAYmjgjD9Vh5zI51w=="],
"file-type": ["[email protected]", "", {}, "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ=="], "file-type": ["[email protected]", "", {}, "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ=="],
"filelist": ["[email protected]", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], "filelist": ["[email protected]", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="],
@@ -1701,6 +1733,8 @@
"ktx-parse": ["[email protected]", "", {}, "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ=="], "ktx-parse": ["[email protected]", "", {}, "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ=="],
"kvparser": ["[email protected]", "", {}, "sha512-5P/5qpTAHjVYWqcI55B3yQwSY2FUrYYrJj5i65V1Wmg7/4W4OnBcaodaEvLyVuugeOnS+BAaKm9LbPazGJcRyA=="],
"leven": ["[email protected]", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], "leven": ["[email protected]", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
"lie": ["[email protected]", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "lie": ["[email protected]", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
@@ -1727,6 +1761,8 @@
"lz4js": ["[email protected]", "", {}, "sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg=="], "lz4js": ["[email protected]", "", {}, "sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg=="],
"lzma": ["[email protected]", "", { "bin": { "lzma.js": "bin/lzma.js" } }, "sha512-DcfiawQ1avYbW+hsILhF38IKAlnguc/fjHrychs9hdxe4qLykvhT5VTGNs5YRWgaNePh7NTxGD4uv4gKsRomCQ=="],
"maath": ["[email protected]", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="], "maath": ["[email protected]", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="],
"magic-bytes.js": ["[email protected]", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], "magic-bytes.js": ["[email protected]", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="],
@@ -1867,6 +1903,8 @@
"nlcst-to-string": ["[email protected]", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], "nlcst-to-string": ["[email protected]", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
"node-bignumber": ["[email protected]", "", {}, "sha512-VoTZHmdFQpZH1+q1dz2qcHNCwTWsJg2T3PYwlAyDNFOfVhSYUKQBLFcCpCud+wJBGgCttGavZILaIggDIKqEQQ=="],
"node-domexception": ["[email protected]", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-domexception": ["[email protected]", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["[email protected]", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], "node-fetch": ["[email protected]", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
@@ -1947,6 +1985,8 @@
"perfect-debounce": ["[email protected]", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], "perfect-debounce": ["[email protected]", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"permessage-deflate": ["[email protected]", "", { "dependencies": { "safe-buffer": "*" } }, "sha512-EUNi/RIsyJ1P1u9QHFwMOUWMYetqlE22ZgGbad7YP856WF4BFF0B7DuNy6vEGsgNNud6c/SkdWzkne71hH8MjA=="],
"pickleparser": ["[email protected]", "", { "bin": { "pickletojson": "bin/pickletojson.js", "pickleparser": "bin/pickletojson.js" } }, "sha512-kMzY3uFYcR6OjOqr7nV2nkaXaBsUEOafu3zgPxeD6s/2ueMfVQH8lrymcDWBPGx0OkVxGMikxQit6jgByXjwBg=="], "pickleparser": ["[email protected]", "", { "bin": { "pickletojson": "bin/pickletojson.js", "pickleparser": "bin/pickletojson.js" } }, "sha512-kMzY3uFYcR6OjOqr7nV2nkaXaBsUEOafu3zgPxeD6s/2ueMfVQH8lrymcDWBPGx0OkVxGMikxQit6jgByXjwBg=="],
"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
@@ -1995,6 +2035,8 @@
"proxy-from-env": ["[email protected]", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "proxy-from-env": ["[email protected]", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"psl": ["[email protected]", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="],
"pump": ["[email protected]", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], "pump": ["[email protected]", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
"punycode": ["[email protected]", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["[email protected]", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@@ -2171,7 +2213,7 @@
"socks": ["[email protected]", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], "socks": ["[email protected]", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], "socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="],
"source-map": ["[email protected]", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map": ["[email protected]", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@@ -2189,6 +2231,16 @@
"statuses": ["[email protected]", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "statuses": ["[email protected]", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"steam-appticket": ["[email protected]", "", { "dependencies": { "@doctormckay/stdlib": "^1.6.0", "@doctormckay/steam-crypto": "^1.2.0", "bytebuffer": "^5.0.1", "protobufjs": "^6.8.8", "steamid": "^1.1.0" } }, "sha512-zwDwZALGv3RanE8RHNYcQU3u4Ez23EzMuQ4Lh15uIHddpDh6TI6uFGbC0HNyt6y+UJYSILe77A33VhFZKQiaqQ=="],
"steam-session": ["[email protected]", "", { "dependencies": { "@doctormckay/stdlib": "^2.9.0", "@doctormckay/user-agents": "^1.0.0", "debug": "^4.3.4", "kvparser": "^1.0.1", "node-bignumber": "^1.2.2", "protobufjs": "^7.1.0", "socks-proxy-agent": "^7.0.0", "steamid": "^2.0.0", "tiny-typed-emitter": "^2.1.0", "websocket13": "^4.0.0" } }, "sha512-MLvg1uMLEOIRHZS5LKruy1w5OqHb8EL7TeMxIY2a4mUcaVOgszz060+jo4c7s3gFeedyuoqGcfwJrR0pLKYzLw=="],
"steam-totp": ["[email protected]", "", {}, "sha512-bTKlc/NoIUQId+my+O556s55DDsNNXfVIPWFDNVu68beql7AJhV0c+GTjFxfwCDYfdc4NkAme+0WrDdnY2D2VA=="],
"steam-user": ["[email protected]", "", { "dependencies": { "@bbob/parser": "^2.2.0", "@doctormckay/stdlib": "^2.9.1", "@doctormckay/steam-crypto": "^1.2.0", "adm-zip": "^0.5.10", "binarykvparser": "^2.2.0", "bytebuffer": "^5.0.0", "file-manager": "^2.0.0", "kvparser": "^1.0.1", "lzma": "^2.3.2", "protobufjs": "^7.2.4", "socks-proxy-agent": "^7.0.0", "steam-appticket": "^1.0.1", "steam-session": "^1.8.0", "steam-totp": "^2.0.1", "steamid": "^2.0.0", "websocket13": "^4.0.0", "zstddec": "^0.1.0" } }, "sha512-6Yzaa0l7fcSRaaJzHjfuqettYqAsrW3+1AEu/cj4M/lgRYXa0DfOAjuWj0/wesfkogwuXNE4N/ZMZvNir8TVtw=="],
"steamid": ["[email protected]", "", {}, "sha512-ndt1cvuuSC+i8fcxVsmeyRlgGsR1QsoAuIXz+eabj8/Y4GIWE2+mgHA7Hys61JDHOxttfWtXHtN2m5TNYTlORg=="],
"stop-iteration-iterator": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], "stop-iteration-iterator": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"streamx": ["[email protected]", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="], "streamx": ["[email protected]", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="],
@@ -2245,6 +2297,8 @@
"tiny-inflate": ["[email protected]", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tiny-inflate": ["[email protected]", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tiny-typed-emitter": ["[email protected]", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="],
"tinyexec": ["[email protected]", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], "tinyexec": ["[email protected]", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
"tinyglobby": ["[email protected]", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinyglobby": ["[email protected]", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
@@ -2437,6 +2491,10 @@
"webidl-conversions": ["[email protected]", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], "webidl-conversions": ["[email protected]", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
"websocket-extensions": ["[email protected]", "", {}, "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="],
"websocket13": ["[email protected]", "", { "dependencies": { "@doctormckay/stdlib": "^2.7.1", "bytebuffer": "^5.0.1", "permessage-deflate": "^0.1.7", "tiny-typed-emitter": "^2.1.0", "websocket-extensions": "^0.1.4" } }, "sha512-7+hxkUVTKQlUDTzN2rJI7fJRBXCT6dvRXr1aZflxUZlpNJutHBkKiEIbZOCGs0A1s7vxAmcAXngsNUQMSUTiVQ=="],
"whatwg-encoding": ["[email protected]", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], "whatwg-encoding": ["[email protected]", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
"whatwg-mimetype": ["[email protected]", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], "whatwg-mimetype": ["[email protected]", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
@@ -2531,6 +2589,8 @@
"zod-to-ts": ["[email protected]", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="], "zod-to-ts": ["[email protected]", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
"zstddec": ["[email protected]", "", {}, "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg=="],
"zustand": ["[email protected]", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="], "zustand": ["[email protected]", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="],
"zwitch": ["[email protected]", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "zwitch": ["[email protected]", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
@@ -2553,8 +2613,6 @@
"@blitzkit/bot/prisma": ["[email protected]", "", { "dependencies": { "@prisma/config": "6.17.1", "@prisma/engines": "6.17.1" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g=="], "@blitzkit/bot/prisma": ["[email protected]", "", { "dependencies": { "@prisma/config": "6.17.1", "@prisma/engines": "6.17.1" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g=="],
"@blitzkit/core/@prisma/client": ["@prisma/[email protected]", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw=="],
"@blitzkit/website/vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="], "@blitzkit/website/vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="],
"@discordjs/rest/@discordjs/collection": ["@discordjs/[email protected]", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], "@discordjs/rest/@discordjs/collection": ["@discordjs/[email protected]", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
@@ -2593,8 +2651,12 @@
"babel-plugin-polyfill-corejs2/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "babel-plugin-polyfill-corejs2/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"binarykvparser/long": ["[email protected]", "", { "bundled": true }, "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg=="],
"boxen/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "boxen/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"bytebuffer/long": ["[email protected]", "", {}, "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg=="],
"c12/dotenv": ["[email protected]", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "c12/dotenv": ["[email protected]", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"cheerio/undici": ["[email protected]", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], "cheerio/undici": ["[email protected]", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="],
@@ -2611,6 +2673,8 @@
"favilib/sharp": ["[email protected]", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], "favilib/sharp": ["[email protected]", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
"file-manager/@doctormckay/stdlib": ["@doctormckay/[email protected]", "", {}, "sha512-XhuUOzElz6fnNdt70IYNKqhPAEpGaL4JHOhAvklRh0hAhVPW+/wLxaWT3DWUbaG5Dta5YvIp7+cZK3GhIpAuug=="],
"filelist/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], "filelist/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"get-uri/data-uri-to-buffer": ["[email protected]", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], "get-uri/data-uri-to-buffer": ["[email protected]", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
@@ -2621,6 +2685,10 @@
"http-errors/statuses": ["[email protected]", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], "http-errors/statuses": ["[email protected]", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"http-proxy-agent/agent-base": ["[email protected]", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"https-proxy-agent/agent-base": ["[email protected]", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"its-fine/@types/react-reconciler": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="], "its-fine/@types/react-reconciler": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="],
"linebreak/base64-js": ["[email protected]", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], "linebreak/base64-js": ["[email protected]", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="],
@@ -2629,12 +2697,20 @@
"micromatch/picomatch": ["[email protected]", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "micromatch/picomatch": ["[email protected]", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"pac-proxy-agent/agent-base": ["[email protected]", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"pac-proxy-agent/socks-proxy-agent": ["[email protected]", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"parse5/entities": ["[email protected]", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "parse5/entities": ["[email protected]", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"prompts/kleur": ["[email protected]", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "prompts/kleur": ["[email protected]", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"proxy-agent/agent-base": ["[email protected]", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"proxy-agent/lru-cache": ["[email protected]", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], "proxy-agent/lru-cache": ["[email protected]", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"proxy-agent/socks-proxy-agent": ["[email protected]", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"react-dom/scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "react-dom/scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"react-promise-suspense/fast-deep-equal": ["[email protected]", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="], "react-promise-suspense/fast-deep-equal": ["[email protected]", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
@@ -2649,6 +2725,12 @@
"stats-gl/three": ["[email protected]", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="], "stats-gl/three": ["[email protected]", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="],
"steam-appticket/@doctormckay/stdlib": ["@doctormckay/[email protected]", "", {}, "sha512-XhuUOzElz6fnNdt70IYNKqhPAEpGaL4JHOhAvklRh0hAhVPW+/wLxaWT3DWUbaG5Dta5YvIp7+cZK3GhIpAuug=="],
"steam-appticket/protobufjs": ["[email protected]", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/long": "^4.0.1", "@types/node": ">=13.7.0", "long": "^4.0.0" }, "bin": { "pbjs": "bin/pbjs", "pbts": "bin/pbts" } }, "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw=="],
"steam-appticket/steamid": ["[email protected]", "", { "dependencies": { "cuint": "^0.2.1" } }, "sha512-t86YjtP1LtPt8D+TaIARm6PtC9tBnF1FhxQeLFs6ohG7vDUfQuy/M8II14rx1TTUkVuYoWHP/7DlvTtoCGULcw=="],
"string_decoder/safe-buffer": ["[email protected]", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "string_decoder/safe-buffer": ["[email protected]", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"tar-fs/tar-stream": ["[email protected]", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], "tar-fs/tar-stream": ["[email protected]", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
@@ -2661,6 +2743,10 @@
"to-buffer/isarray": ["[email protected]", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "to-buffer/isarray": ["[email protected]", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"ts-proto/@bufbuild/protobuf": ["@bufbuild/[email protected]", "", {}, "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA=="],
"ts-proto-descriptors/@bufbuild/protobuf": ["@bufbuild/[email protected]", "", {}, "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA=="],
"tunnel-rat/zustand": ["[email protected]", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], "tunnel-rat/zustand": ["[email protected]", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
"unbzip2-stream/buffer": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "unbzip2-stream/buffer": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
@@ -2763,6 +2849,10 @@
"filelist/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "filelist/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"steam-appticket/protobufjs/@types/long": ["@types/[email protected]", "", {}, "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="],
"steam-appticket/protobufjs/long": ["[email protected]", "", {}, "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="],
"widest-line/string-width/emoji-regex": ["[email protected]", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], "widest-line/string-width/emoji-regex": ["[email protected]", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="],
"widest-line/string-width/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "widest-line/string-width/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.29.2
## Technical Changes
- Implemented new workflow capabilities to deploy BlitzKit OpenTest and Preview remotely, from any device
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.29.3
## Technical Changes
- Added a significantly more memory and network efficient AssetUploader abstraction
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.29.4
## Technical Changes
- BlitzKit now partially downloads game files instead of the entire game, allowing for faster updates
+11
View File
@@ -0,0 +1,11 @@
# BlitzKit 1.29.5
## Features
- Added new lighting to be a bit softer on tanks
- Added a button with the sun icon to toggle high graphics
- Added a new reveal and transition animation between tabs
## Fixes
- Track wheels on some tanks no longer look translucent
+18
View File
@@ -0,0 +1,18 @@
# BlitzKit 1.29.6
## Features
- Simplified the ownership and test tank filters in the tank search
- Renamed "Guess the tank" to simply "Guess"
- The player count value in the Performance tool is now set to Total by default
- Added Compare-like red/green highlighting to the Performance tool
## Changes
- Removed the last updated info bar in Performance for some more screen real estate
## Fixes
- The remove played tanks in the Playlist tool now shows even when logged out
- The Guess game now has a page title
- The compare table now occupies the minimum width possible instead of expanding all columns excessively
+4 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "blitzkit", "name": "blitzkit",
"version": "1.29.1", "version": "1.29.6",
"license": "BlitzKit License", "license": "BlitzKit License",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -10,6 +10,7 @@
"build:website": "cd packages/website && bun run build", "build:website": "cd packages/website && bun run build",
"build:prisma": "prisma generate", "build:prisma": "prisma generate",
"build:proxyclient": "cd packages/scripts && bun run build:proxyclient", "build:proxyclient": "cd packages/scripts && bun run build:proxyclient",
"build:catalogitemaccesssor": "cd packages/scripts && bun run build:catalogitemaccesssor",
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard", "build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
"build:app": "cd packages/app && bun run build", "build:app": "cd packages/app && bun run build",
"dev:bot": "bun run build:bot --dev && bun start:bot", "dev:bot": "bun run build:bot --dev && bun start:bot",
@@ -34,7 +35,8 @@
"build:reviews": "cd packages/scripts && bun build:reviews", "build:reviews": "cd packages/scripts && bun build:reviews",
"build:ids": "cd packages/scripts && bun build:ids", "build:ids": "cd packages/scripts && bun build:ids",
"prunestrings": "cd packages/scripts && bun prunestrings", "prunestrings": "cd packages/scripts && bun prunestrings",
"setup": "bun install && bun run build:prisma && bun run build:protos && bun run build:proxyclient" "setup": "bun install && bun run build:prisma && bun run build:protos && bun run build:proxyclient",
"patchnotes": "cd packages/scripts && bun run patchnotes"
}, },
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
+2
View File
@@ -10,12 +10,14 @@
"@octokit/rest": "^22.0.0", "@octokit/rest": "^22.0.0",
"@prisma/client": "^6.16.2", "@prisma/client": "^6.16.2",
"@types/adm-zip": "^0.5.7", "@types/adm-zip": "^0.5.7",
"@types/bun": "^1.3.1",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1", "@types/lz4js": "^0.2.1",
"@types/three": "^0.180.0", "@types/three": "^0.180.0",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"crc": "^4.3.2", "crc": "^4.3.2",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"fast-xml-parser": "^5.3.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lz4js": "^0.2.0", "lz4js": "^0.2.0",
"pickleparser": "^0.2.1", "pickleparser": "^0.2.1",
+25
View File
@@ -0,0 +1,25 @@
export enum BlitzVFSSourceType {
Steam,
}
type BlitzVFSSource = {
type: BlitzVFSSourceType;
username: string;
password: string;
app: number;
os: "windows";
};
export class BlitzVFS {
constructor(private source: BlitzVFSSource) {}
async init() {
return this;
}
async file(path: string) {
return new Uint8Array();
}
}
+33 -32
View File
@@ -1,32 +1,33 @@
export * from './canSplash'; export * from "./canSplash";
export * from './clans'; export * from "./clans";
export * from './classes'; export * from "./classes";
export * from './constants'; export * from "./constants";
export * from './equipmentPrice'; export * from "./equipmentPrice";
export * from './fetchBlitz'; export * from "./fetchBlitz";
export * from './fetchGlossary'; export * from "./fetchGlossary";
export * from './generateStats'; export * from "./generateStats";
export * from './getAccountAchievements'; export * from "./getAccountAchievements";
export * from './getAccountInfo'; export * from "./getAccountInfo";
export * from './getClanAccountInfo'; export * from "./getClanAccountInfo";
export * from './getClanInfo'; export * from "./getClanInfo";
export * from './getLeagueFromScore'; export * from "./getLeagueFromScore";
export * from './getRatingInfo'; export * from "./getRatingInfo";
export * from './getRatingLeague'; export * from "./getRatingLeague";
export * from './getRatingNeighbors'; export * from "./getRatingNeighbors";
export * from './getTankStats'; export * from "./getTankStats";
export * from './idToRegion'; export * from "./idToRegion";
export * from './isOnGoingRatingSeason'; export * from "./isOnGoingRatingSeason";
export * from './isValidBlitzId'; export * from "./isValidBlitzId";
export * from './nations'; export * from "./nations";
export * from './normalizeExtra'; export * from "./normalizeExtra";
export * from './normalizeIds'; export * from "./normalizeIds";
export * from './permanentSkills'; export * from "./permanentSkills";
export * from './rating'; export * from "./rating";
export * from './regions'; export * from "./readDVPL";
export * from './regionSubdomainToRegion'; export * from "./regions";
export * from './regionToRegionSubdomain'; export * from "./regionSubdomainToRegion";
export * from './searchClansAcrossRegions'; export * from "./regionToRegionSubdomain";
export * from './searchCurrentRatingPlayers'; export * from "./searchClansAcrossRegions";
export * from './searchPlayersAcrossRegions'; export * from "./searchCurrentRatingPlayers";
export * from './toUniqueId'; export * from "./searchPlayersAcrossRegions";
export * from "./toUniqueId";
@@ -1,5 +1,5 @@
import { crc32 } from 'crc'; import { crc32 } from "crc";
import { decompressBlock } from 'lz4js'; import { decompressBlock } from "lz4js";
/** /**
* Thanks Maddoxkkm! Modified heavily to be modern. * Thanks Maddoxkkm! Modified heavily to be modern.
@@ -9,10 +9,10 @@ export function readDVPL(buffer: Buffer) {
const footerBuffer = buffer.subarray(buffer.length - 20, buffer.length); const footerBuffer = buffer.subarray(buffer.length - 20, buffer.length);
if ( if (
footerBuffer.toString('utf8', 16, 20) !== 'DVPL' || footerBuffer.toString("utf8", 16, 20) !== "DVPL" ||
footerBuffer.length !== 20 footerBuffer.length !== 20
) { ) {
throw new SyntaxError('Invalid DVPL footer'); throw new SyntaxError("Invalid DVPL footer");
} }
const footerData = { const footerData = {
@@ -24,15 +24,15 @@ export function readDVPL(buffer: Buffer) {
const targetBlock = buffer.subarray(0, buffer.length - 20); const targetBlock = buffer.subarray(0, buffer.length - 20);
if (targetBlock.length !== footerData.cSize) { if (targetBlock.length !== footerData.cSize) {
throw new RangeError('DVPL size mismatch'); throw new RangeError("DVPL size mismatch");
} }
if (crc32(targetBlock) !== footerData.crc32) { if (crc32(targetBlock) !== footerData.crc32) {
throw new TypeError('DVPL CRC32 mismatch'); throw new TypeError("DVPL CRC32 mismatch");
} }
if (footerData.type === 0) { if (footerData.type === 0) {
if (!(footerData.oSize === footerData.cSize && footerData.type === 0)) { if (!(footerData.oSize === footerData.cSize && footerData.type === 0)) {
throw new RangeError('DVPL type and size mismatch'); throw new RangeError("DVPL type and size mismatch");
} }
return targetBlock; return targetBlock;
@@ -44,17 +44,17 @@ export function readDVPL(buffer: Buffer) {
destination, destination,
0, 0,
source.length, source.length,
0, 0
); );
if (decompressedSize !== footerData.oSize) { if (decompressedSize !== footerData.oSize) {
throw new RangeError( throw new RangeError(
`Decompressed DVPL size mismatch (${decompressedSize} vs ${footerData.oSize})`, `Decompressed DVPL size mismatch (${decompressedSize} vs ${footerData.oSize})`
); );
} }
return Buffer.from(destination); return Buffer.from(destination);
} else { } else {
throw new SyntaxError('Unknown DVPL format'); throw new SyntaxError("Unknown DVPL format");
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import { assertSecret } from './assertSecret'; import { assertSecret } from "./assertSecret";
export function asset(path: string) { export function asset(path: string) {
return `${assertSecret(import.meta.env.PUBLIC_ASSET_BASE)}/${path}`; return `${assertSecret(import.meta.env.PUBLIC_ASSET_BASE)}/${path}`;
+1
View File
@@ -6,4 +6,5 @@ export const youtubers = [
{ id: 'UCLMEfqtlwLDN-UXX21RiEww', name: 'WoT Blitz Best Replays' }, { id: 'UCLMEfqtlwLDN-UXX21RiEww', name: 'WoT Blitz Best Replays' },
{ id: 'UC3fZbetRUFoJL0IEN7IhG0Q', name: 'EconomicsMate' }, { id: 'UC3fZbetRUFoJL0IEN7IhG0Q', name: 'EconomicsMate' },
{ id: 'UCtsUVwBlhEnUunAKLn2dtsg', name: 'Megalion69' }, { id: 'UCtsUVwBlhEnUunAKLn2dtsg', name: 'Megalion69' },
{ id: 'UCJjATWiGfOHoQYfutEwAIIg', name: 'WoTB Joe' },
]; ];
+10 -10
View File
@@ -1,10 +1,10 @@
export * from './blitz'; export * from "./blitz";
export * from './blitzkit'; export * from "./blitzkit";
export * from './imgur'; export * from "./imgur";
export * from './math'; export * from "./math";
export * from './patreon'; export * from "./patreon";
export * from './protobuf'; export * from "./protobuf";
export * from './protos'; export * from "./protos";
export * from './statistics'; export * from "./statistics";
export * from './streams'; export * from "./streams";
export * from './types'; export * from "./types";
+2 -2
View File
@@ -906,8 +906,8 @@
}, },
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess",
"button": "Guess away", "button": "What could it be?",
"description": "Silhouette mini-game", "description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz", "seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": { "search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "Randomize" "randomize": "Randomize"
}, },
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess",
"button": "Guess away", "button": "What could it be?",
"description": "Silhouette mini-game", "description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz", "seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": { "search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "Randomize" "randomize": "Randomize"
}, },
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess",
"button": "Guess away", "button": "What could it be?",
"description": "Silhouette mini-game", "description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz", "seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": { "search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "Randomize" "randomize": "Randomize"
}, },
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess",
"button": "Guess away", "button": "What could it be?",
"description": "Silhouette mini-game", "description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz", "seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": { "search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "Randomize" "randomize": "Randomize"
}, },
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess",
"button": "Guess away", "button": "What could it be?",
"description": "Silhouette mini-game", "description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz", "seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": { "search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "Randomize" "randomize": "Randomize"
}, },
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess",
"button": "Guess away", "button": "What could it be?",
"description": "Silhouette mini-game", "description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz", "seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": { "search": {
+4 -4
View File
@@ -661,7 +661,7 @@
"select_dialog_title": "Поиск танка", "select_dialog_title": "Поиск танка",
"swap_dialog_title": "Заменить танк", "swap_dialog_title": "Заменить танк",
"clear_filters": "Сбросить фильтры", "clear_filters": "Сбросить фильтры",
"login": "Войти, чтобы воспользоваться", "login": "Войдите, чтобы воспользоваться",
"search_bar_hint": "Поиск танков...", "search_bar_hint": "Поиск танков...",
"recent": "Недавние", "recent": "Недавние",
"select_all": "Выбрать всё", "select_all": "Выбрать всё",
@@ -803,8 +803,8 @@
"randomize": "Случайно" "randomize": "Случайно"
}, },
"guess": { "guess": {
"name": "Угадай танк", "name": "Угадать",
"button": "Угадать", "button": "Что это за танк?",
"description": "Мини-игра «Силуэт»", "description": "Мини-игра «Силуэт»",
"seo_description": "Играйте угадывая силуэт танка из World of Tanks Blitz", "seo_description": "Играйте угадывая силуэт танка из World of Tanks Blitz",
"search": { "search": {
@@ -908,7 +908,7 @@
}, },
"stats": { "stats": {
"thickness_and_angle": "{thickness}мм {angle}°", "thickness_and_angle": "{thickness}мм {angle}°",
"nominal": "{thickness}1мм номинала", "nominal": "{thickness}мм номинала",
"ricochet_loss": "{loss}% пробития" "ricochet_loss": "{loss}% пробития"
} }
} }
+42 -42
View File
@@ -1,19 +1,19 @@
{ {
"test": { "test": {
"default": "тест", "default": "тест",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.", "long": "{fruit}",
"fox": "Швидка бура лисиця перестрибує через лінивого собаку." "fox": "Швидка бура лисиця перестрибує через лінивого собаку."
}, },
"common": { "common": {
"name": "BlitzKit", "name": "BlitzKit",
"units": { "units": {
"m": "{value}m", "m": "{value}м",
"mm": "{value}mm", "mm": "{value}мм",
"deg": "{value}°", "deg": "{value}°",
"hp": "{value}hp", "hp": "{value}од",
"x": "x{value}", "x": "x{value}",
"percentage": "{value}%", "percentage": "{value}%",
"deg_s": "{value}°/s" "deg_s": "{value}град/c"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Магазинна система заряджання", "auto_loader": "Магазинна система заряджання",
@@ -80,7 +80,7 @@
"nations": { "nations": {
"ussr": "СРСР", "ussr": "СРСР",
"germany": "Німеччина", "germany": "Німеччина",
"usa": "США", "usa": "ЗША",
"china": "Китай", "china": "Китай",
"uk": "Велика Британія", "uk": "Велика Британія",
"france": "Франція", "france": "Франція",
@@ -186,11 +186,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.", "clan_not_found": "# Не вдалося знайти клан\nНе вдалося знайти \"{search}\". Спробуйте обрати клан з результату пошуку.",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.", "player_not_found": "# Не вдалося знайти користувача\nНе вдалося знайти користувача \"{search}\". Спробуйте вибрати користувача із результатів пошуку, ввівши правильне ім'я користувача, або скористайтеся командою `/link`.",
"player_not_linked": "# Використайте команду \"/link\" щоб почати\nЯ не знаю, хто ви, але ви можете сказати мені сьогодні!", "player_not_linked": "# Використайте команду \"/link\" щоб почати\nЯ не знаю, хто ви, але ви можете сказати мені сьогодні!",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".", "tank_name_not_found": "# Танк не знайдено\nНе вдалося знайти танк із назвою «{search}».",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.", "tank_id_not_found": "# Танк не знайдено\nНе вдалося знайти танк з ID «{search}». Спробуйте ввести назву танка замість номера.",
"no_tank_stats": "# Немає статистики для танка\nЦей гравець не має статистики.\nМожливо, це не той гравець, якого ви шукаєте.", "no_tank_stats": "# Немає статистики для танка\nЦей гравець не має статистики.\nМожливо, це не той гравець, якого ви шукаєте.",
"get_help": "Допомогти Discord серверу", "get_help": "Допомогти Discord серверу",
"uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).", "uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).",
@@ -284,19 +284,19 @@
"name": "рівень", "name": "рівень",
"description": "Рівень", "description": "Рівень",
"choices": { "choices": {
"tier": "Tier {tier_1} - {tier_2}" "tier": "Рівень {tier_1} - {tier_2}"
} }
} }
}, },
"periods": { "periods": {
"career": "Кар'єра", "career": "Кар'єра",
"today": "Сьогодні", "today": "Сьогодні",
"days": "{days} days", "days": "{days} днів",
"custom": "{day_1} - {day_2} days" "custom": "{day_1} - {day_2} днів"
}, },
"filters": { "filters": {
"none": "Немає фільтрів", "none": "Немає фільтрів",
"tier": "Tier {tier}" "tier": "Рівень {tier}"
}, },
"no_data": { "no_data": {
"battles_in_period": "За цей період боїв не було", "battles_in_period": "За цей період боїв не було",
@@ -339,10 +339,10 @@
"body": { "body": {
"subtitle": "Дослідження", "subtitle": "Дослідження",
"total": "Загалом", "total": "Загалом",
"research": "Research: {cost}", "research": "Дослідження: {cost}",
"upgrades": "Upgrades: {cost}", "upgrades": "Upgrades: {cost}",
"purchase": "Purchase: {cost}", "purchase": "Вартість: {cost}",
"equipment": "Equipment: {cost}", "equipment": "Спорядження: {cost}",
"estimation": "# Це оцінка!\n\nЦей список може включати вже досліджені танки через неточну інформацію для вантажу. Спробуйте використати \"початковий танк\" з рядка за допомогою команди." "estimation": "# Це оцінка!\n\nЦей список може включати вже досліджені танки через неточну інформацію для вантажу. Спробуйте використати \"початковий танк\" з рядка за допомогою команди."
}, },
"errors": { "errors": {
@@ -435,7 +435,7 @@
}, },
"body": { "body": {
"subtitle": "Inactive for {days}+ days", "subtitle": "Inactive for {days}+ days",
"listing": "{days} days" "listing": "{days} днів"
} }
}, },
"full_stats": { "full_stats": {
@@ -549,7 +549,7 @@
"top_players": "Top {count} players", "top_players": "Top {count} players",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.", "no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
"no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.", "no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
"deleted_player": "Deleted {id}", "deleted_player": "",
"neighbors": "Rating neighbors", "neighbors": "Rating neighbors",
"more": "+ {count} more" "more": "+ {count} more"
} }
@@ -643,9 +643,9 @@
"body": "This page has moved. Redirecting..." "body": "This page has moved. Redirecting..."
}, },
"settings": { "settings": {
"title": "Settings", "title": "Налаштування",
"description": "Change BlitzKit settings", "description": "Змінити налаштування BlitzKit ",
"dev_mode": "Development mode", "dev_mode": "Режим розробника",
"language": "Мова" "language": "Мова"
}, },
"common": { "common": {
@@ -658,16 +658,16 @@
"copied": "Скопійовано!" "copied": "Скопійовано!"
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Search for tanks", "select_dialog_title": "Знайти танк",
"swap_dialog_title": "Swap tank", "swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters", "clear_filters": "Очистити",
"login": "Login to use this", "login": "Увійдіть, щоб скористатися",
"search_bar_hint": "Шукайте техніку...", "search_bar_hint": "Шукайте техніку...",
"recent": "Недавно переглянуті", "recent": "Недавно переглянуті",
"select_all": "Вибрати все", "select_all": "Вибрати все",
"count_singular": "1 танк", "count_singular": "1 танк",
"count_plural": "{count} tanks", "count_plural": "Танків: {count} ",
"sorting_by": "Sorting by {name}", "sorting_by": "Відсортувати за {name}",
"no_tanks_found": { "no_tanks_found": {
"body": "Нічого не знайдено.", "body": "Нічого не знайдено.",
"search_again": "Спробуйте шукати заново.", "search_again": "Спробуйте шукати заново.",
@@ -731,7 +731,7 @@
"home": { "home": {
"pwa": { "pwa": {
"title": "Install the BlitzKit App", "title": "Install the BlitzKit App",
"new": "NEW", "new": "",
"checking": "Checking compatibility...", "checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.", "instructions": "Instructions for Apple devices.",
"install": "Install", "install": "Install",
@@ -763,20 +763,20 @@
}, },
"tools": { "tools": {
"playlist": { "playlist": {
"reset": "Reset", "reset": "Скинути",
"shuffle": "Shuffle", "shuffle": "Перемішати",
"remove": "Remove played", "remove": "Видалити зіграні",
"tracking": "BlitzKit is tracking your battles", "tracking": "BlitzKit is tracking your battles",
"sort": { "sort": {
"button": "Сортувати за", "button": "Сортувати за",
"last_played": "Last played", "last_played": "Остання гра",
"least_played": "Least played" "least_played": "Найменше зіграно"
}, },
"table": { "table": {
"index": "#", "index": "#",
"never": "Never", "never": "Ніколи",
"tank": "Танк", "tank": "Танк",
"last_played": "Last played", "last_played": "Остання гра",
"battles": "Career battles", "battles": "Career battles",
"unrecorded": "--", "unrecorded": "--",
"wins": "Перемоги" "wins": "Перемоги"
@@ -784,14 +784,14 @@
"large_warning": { "large_warning": {
"title": "That's a lot of tanks!", "title": "That's a lot of tanks!",
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!", "description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
"cancel": "Cancel", "cancel": "Скасувати",
"proceed": "Proceed" "proceed": "Продовжити"
}, },
"generate": "Generate playlist", "generate": "Згенерувати плейлист",
"name": "Playlist", "name": "Плейлист",
"description": "Tank playlist generator", "description": "Tank playlist generator",
"seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!", "seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!",
"button": "Generate", "button": "Створити",
"pool_size": "{count} possible tanks" "pool_size": "{count} possible tanks"
}, },
"mixer": { "mixer": {
@@ -803,8 +803,8 @@
"randomize": "Randomize" "randomize": "Randomize"
}, },
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess",
"button": "Guess away", "button": "What could it be?",
"description": "Silhouette mini-game", "description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz", "seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": { "search": {
+152 -152
View File
@@ -1,23 +1,23 @@
{ {
"test": { "test": {
"default": "测试", "default": "测试",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.", "long": "这是一个测试用长字符串。你绝对不会在用户界面中看见这些。我最喜欢的水果是{fruit}因为它的颜色是{color}的。我如此喜欢它,以至于我会把它的名字念3次!{fruit}!{fruit}!{fruit}!重申一次,它是{color}的而不是{not_color}的。",
"fox": "褐色的狐狸迅速地跳过那只懒狗" "fox": "褐色的狐狸迅速地跳过那只懒狗"
}, },
"common": { "common": {
"name": "BlitzKit", "name": "BlitzKit",
"units": { "units": {
"m": "{value}m", "m": "{value} 公尺",
"mm": "{value}mm", "mm": "{value} 毫米",
"deg": "{value}°", "deg": "{value} 度°",
"hp": "{value}hp", "hp": "{value} 生命值",
"x": "x{value}", "x": "x{value}",
"percentage": "{value}%", "percentage": "{value}%",
"deg_s": "{value}°/s" "deg_s": "{value}度°/秒"
}, },
"gun_types": { "gun_types": {
"auto_loader": "弹夹型战车", "auto_loader": "弹夹型坦克",
"auto_reloader": "自动装填型战车", "auto_reloader": "自动装填型坦克",
"regular": "单发" "regular": "单发"
}, },
"shells": { "shells": {
@@ -101,8 +101,8 @@
}, },
"tank_class_short": { "tank_class_short": {
"-1": "未识别", "-1": "未识别",
"0": "轻型战车", "0": "轻型坦克",
"1": "中型战车", "1": "中型坦克",
"2": "重型战车", "2": "重型战车",
"3": "驱逐战车" "3": "驱逐战车"
}, },
@@ -186,11 +186,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# 找不到军团\n找不到 \"{search}\". 请尝试选从搜索结果中择一个军团。", "clan_not_found": "# 找不到公会\n找不到\"{search}\"。请尝试从搜索结果中选择一个公会。",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.", "player_not_found": "# 找不到玩家\n无法找到玩家 “{search}”。 尝试从搜索结果中选择玩家,或输入一个有效的玩家名称, 或使用“/link”命令。",
"player_not_linked": "# 使用 `/link` 来开始\n我不知道你是谁,但你今天可以告诉我!", "player_not_linked": "# 使用 `/link` 来开始\n我不知道你是谁,但你今天可以告诉我!",
"tank_name_not_found": "# 找不到坦克\n找不到名称为 \"{search}\"的坦克。", "tank_name_not_found": "# 找不到坦克\n找不到名为 “{search}” 的坦克。",
"tank_id_not_found": "# 找不到坦克\n找不到ID为 \"{search}\"的坦克. 尝试输入坦克的名称。", "tank_id_not_found": "# 找不到坦克\n无法找到ID为 “{search}” 的坦克。 请尝试输入坦克的名称而不是数字。",
"no_tank_stats": "# 没有这辆战车的数据\n这个玩家没有可用资料。 这可能不是你要找的玩家。", "no_tank_stats": "# 没有这辆战车的数据\n这个玩家没有可用资料。 这可能不是你要找的玩家。",
"get_help": "Discord的协助服务器", "get_help": "Discord的协助服务器",
"uncaught_error": "# BlitzKit 发生错误!\n我们对此感到抱歉。 欢迎加入[官方Discord](https://discord.gg/nDt7AjGJQH),以方便帮助你解决问题。", "uncaught_error": "# BlitzKit 发生错误!\n我们对此感到抱歉。 欢迎加入[官方Discord](https://discord.gg/nDt7AjGJQH),以方便帮助你解决问题。",
@@ -284,19 +284,19 @@
"name": "阶级", "name": "阶级",
"description": "阶级", "description": "阶级",
"choices": { "choices": {
"tier": "坦克等级 {tier_1} - {tier_2}" "tier": "{tier_1} - {tier_2} 阶"
} }
} }
}, },
"periods": { "periods": {
"career": "玩家资料", "career": "玩家资料",
"today": "今日:", "today": "今日:",
"days": "{days} days", "days": "{days} 天",
"custom": "{day_1} - {day_2} days" "custom": "{day_1} - {day_2} 天"
}, },
"filters": { "filters": {
"none": "无筛选器", "none": "无筛选器",
"tier": "坦克等级 {tier}" "tier": "{tier} 阶"
}, },
"no_data": { "no_data": {
"battles_in_period": "在这段时间里没有参与任何战斗", "battles_in_period": "在这段时间里没有参与任何战斗",
@@ -339,19 +339,19 @@
"body": { "body": {
"subtitle": "研发", "subtitle": "研发",
"total": "全部", "total": "全部",
"research": "Research: {cost}", "research": "研发:{cost}",
"upgrades": "Upgrades: {cost}", "upgrades": "升级:{cost}",
"purchase": "Purchase: {cost}", "purchase": "购买:{cost}",
"equipment": "Equipment: {cost}", "equipment": "装备:{cost}",
"estimation": "# 这是一个估计值!\n\n这个列表可能包含已经研发过的战车,因为 Wargaming 提供了不正确的资料。 如果可以,请尝试在同一研发线路中提供一个 “起始战车” 。" "estimation": "# 这是一个估计值!\n\n这个列表可能包含已经研发过的战车,因为 Wargaming 提供了不正确的资料。 如果可以,请尝试在同一研发线路中提供一个 “起始战车” 。"
}, },
"errors": { "errors": {
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.", "non_tech_tree": "# \"{tank}\" 不在科技树里\n\n你不能研发一个 加值/收藏家 坦克。",
"tier_1": "# {tank} 是一级坦克\n\n一级坦克一直是对所有玩家免费解锁的。", "tier_1": "# {tank} 是一级坦克\n\n一级坦克一直是对所有玩家免费解锁的。",
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.", "already_researched": "# \"{tank}\" 已完成研发\n\n请尝试在同一研发线路上提供“起始坦克”, 以查看全部您已研发过的坦克。",
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.", "start_end_equal": "# \"{tank}\" 重复提供了\n\n请确认 \"初始坦克\" 和 \"目标坦克\" 是不同的。",
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.", "tanks_not_on_line": "# “{tank_1}” 和 “{tank_2}” 不在同一个研发线路上\n\n请确认“起始坦克” 和 “目标坦克” 在同一研发线路中。",
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank." "unordered": "# “{tank_1}” 阶级比 “{tank_2}” 高\n\n请以提供 {tank_2} 作为起始坦克,因为它的阶级更低。"
} }
}, },
"about": { "about": {
@@ -394,7 +394,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "胜率", "winrate": "胜率",
"damage": "伤害", "damage": "伤害",
"unknown_tank": "未知坦克{id}" "unknown_tank": "未知坦克 {id}"
} }
}, },
"debug": { "debug": {
@@ -430,12 +430,12 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "阈值", "name": "阈值",
"description": "The number of days inactive (default: {days} days)" "description": "未上线天数( 默认:{days} 天)"
} }
}, },
"body": { "body": {
"subtitle": "Inactive for {days}+ days", "subtitle": "{days}+ 天未上线",
"listing": "{days} days" "listing": "{days} 天"
} }
}, },
"full_stats": { "full_stats": {
@@ -478,7 +478,7 @@
"body": { "body": {
"accounts_linked": "帐号已绑定", "accounts_linked": "帐号已绑定",
"no_role_permissions": { "no_role_permissions": {
"title": "{name} failed to verify", "title": "{name} 验证失败",
"description": "我没有管理身份的权限。" "description": "我没有管理身份的权限。"
} }
}, },
@@ -493,11 +493,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "限制", "name": "限制",
"description": "The max number of clan results (default: {limit})" "description": "公会结果的最大显示数量 (默认:{limit})"
} }
}, },
"body": { "body": {
"title": "Clan search for {search} in region {region}", "title": "在地区 {region} 中搜索公会 {search}",
"no_results": "没有找到该公会" "no_results": "没有找到该公会"
} }
}, },
@@ -507,11 +507,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "限制", "name": "限制",
"description": "The max number of player results (default: {limit})" "description": "玩家结果的最大显示数量 (默认:{limit})"
} }
}, },
"body": { "body": {
"title": "Player search for {search} in region {region}", "title": "在地区 {region} 中搜索玩家 {search}",
"no_results": "未找到玩家" "no_results": "未找到玩家"
} }
}, },
@@ -531,13 +531,13 @@
"options": { "options": {
"limit": { "limit": {
"name": "限制", "name": "限制",
"description": "How many neighbors to display (default: {count})" "description": "显示附近有多少玩家(默认:{count})"
}, },
"season": { "season": {
"name": "赛季", "name": "赛季",
"description": "赛季数 (默认:当前)", "description": "赛季数 (默认:当前)",
"choices": { "choices": {
"current": "{season} (current)" "current": "{season} (当前)"
} }
}, },
"league": { "league": {
@@ -546,12 +546,12 @@
} }
}, },
"body": { "body": {
"top_players": "Top {count} players", "top_players": "前 {count} 玩家",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.", "no_participation": "# {player} 没有在赛季 {season} 中进行过评价战斗\n\n该玩家没有参加该赛季或没有完成校准。",
"no_ongoing_season": "# 没有进行中的赛季\n\n目前没有赛季。", "no_ongoing_season": "# 没有进行中的赛季\n\n目前没有赛季。",
"deleted_player": "删除{id}", "deleted_player": "已删除玩家 {id}",
"neighbors": "对邻居进行评分", "neighbors": "对邻居进行评分",
"more": "+ {count} more" "more": "+ {count} 更多"
} }
}, },
"today": { "today": {
@@ -566,7 +566,7 @@
"name": "已拥有的战车", "name": "已拥有的战车",
"description": "显示该阶级中所有您已拥有的战车", "description": "显示该阶级中所有您已拥有的战车",
"body": { "body": {
"title": "# {name}'s owned tier {tier} tanks\n\n" "title": "# {name} 所拥有的 {tier} 阶坦克\n\n"
} }
}, },
"permissions": { "permissions": {
@@ -585,7 +585,7 @@
"description": "检查机器人是否处于活动状态。", "description": "检查机器人是否处于活动状态。",
"body": { "body": {
"pong": "Pong 🏓", "pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - {time}ms" "pong_with_time": "Pong 🏓 - {time} 微秒"
}, },
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
@@ -602,7 +602,7 @@
"name": "玩家信息", "name": "玩家信息",
"description": "玩家基本信息", "description": "玩家基本信息",
"body": { "body": {
"title": "{name}的信息", "title": "{name} 的信息",
"nickname": "昵称", "nickname": "昵称",
"battles": "战斗场次", "battles": "战斗场次",
"winrate": "胜率", "winrate": "胜率",
@@ -631,7 +631,7 @@
} }
}, },
"errors": { "errors": {
"no_participation": "# {name}还没有玩过评级模式。", "no_participation": "# {name} 未曾游玩评价战斗\n\n该玩家未参与本赛季的评价战斗。",
"unrecorded_stats": "# 统计数据目前无法使用\n\nWargaming 可能暂停了服务器。 如果有连续的赛季,统计数据可能在24小时内提供。" "unrecorded_stats": "# 统计数据目前无法使用\n\nWargaming 可能暂停了服务器。 如果有连续的赛季,统计数据可能在24小时内提供。"
} }
} }
@@ -644,7 +644,7 @@
}, },
"settings": { "settings": {
"title": "设置", "title": "设置",
"description": "调整BlitzKit设置", "description": "更改 BlitzKit 设置",
"dev_mode": "开发者模式", "dev_mode": "开发者模式",
"language": "语言" "language": "语言"
}, },
@@ -661,13 +661,13 @@
"select_dialog_title": "搜索坦克", "select_dialog_title": "搜索坦克",
"swap_dialog_title": "交换坦克", "swap_dialog_title": "交换坦克",
"clear_filters": "清除筛选", "clear_filters": "清除筛选",
"login": "Login to use this", "login": "登录以使用它",
"search_bar_hint": "搜寻战车……", "search_bar_hint": "搜寻战车……",
"recent": "最近查看的", "recent": "最近查看的",
"select_all": "全选", "select_all": "全选",
"count_singular": "1辆战车", "count_singular": "1辆战车",
"count_plural": "{count} tanks", "count_plural": "{count} 辆坦克",
"sorting_by": "Sorting by {name}", "sorting_by": "按 {name} 排序",
"no_tanks_found": { "no_tanks_found": {
"body": "找不到战车。", "body": "找不到战车。",
"search_again": "请尝试重新搜索。", "search_again": "请尝试重新搜索。",
@@ -688,7 +688,7 @@
"fire.caliber": "口径", "fire.caliber": "口径",
"fire.standardPenetration": "标准炮弹穿透力", "fire.standardPenetration": "标准炮弹穿透力",
"fire.premiumPenetration": "加值炮弹穿透力", "fire.premiumPenetration": "加值炮弹穿透力",
"fire.tertiaryPenetration": "tertiary penetration", "fire.tertiaryPenetration": "第三种炮弹穿透力",
"fire.damage": "炮弹伤害值", "fire.damage": "炮弹伤害值",
"fire.moduleDamage": "模组伤害值", "fire.moduleDamage": "模组伤害值",
"fire.shellVelocity": "炮弹飞行速度", "fire.shellVelocity": "炮弹飞行速度",
@@ -730,12 +730,12 @@
}, },
"home": { "home": {
"pwa": { "pwa": {
"title": "安装Blitz Kit App", "title": "安装 BlitzKit 应用程序",
"new": "NEW", "new": "新增",
"checking": "正在检查兼容性……", "checking": "正在检查兼容性……",
"instructions": "Instructions for Apple devices.", "instructions": "Apple 设备的说明。",
"install": "安装", "install": "安装",
"dismiss": "忽略" "dismiss": "取消"
}, },
"discord": { "discord": {
"join_short": "加入超級酷的人群!", "join_short": "加入超級酷的人群!",
@@ -743,16 +743,16 @@
"action": "Discord" "action": "Discord"
}, },
"hero": { "hero": {
"credit": "By {name} ❤️" "credit": "由 {name} 制作 ❤️"
}, },
"hot_tanks": { "hot_tanks": {
"title": "受欢迎的坦克", "title": "受欢迎的坦克",
"subtitle": "Last {days} days" "subtitle": "过去 {days} 天"
}, },
"other_projects": { "other_projects": {
"title": "其他酷炫的项目" "title": "其他酷炫的项目"
}, },
"help_translate": "Please help translate {name} to {language}!", "help_translate": "请帮忙将 {name} 翻译成 {language}!",
"seo_description": "战车世界闪击战 全方位工具" "seo_description": "战车世界闪击战 全方位工具"
}, },
"docs": { "docs": {
@@ -764,49 +764,49 @@
"tools": { "tools": {
"playlist": { "playlist": {
"reset": "重置", "reset": "重置",
"shuffle": "Shuffle", "shuffle": "换一批",
"remove": "Remove played", "remove": "移除已游玩的",
"tracking": "BlitzKit is tracking your battles", "tracking": "BlitzKit 正在追踪您的战斗记录",
"sort": { "sort": {
"button": "排序依据", "button": "排序依据",
"last_played": "Last played", "last_played": "最后一次游玩",
"least_played": "Least played" "least_played": "最少游玩的"
}, },
"table": { "table": {
"index": "#", "index": "#",
"never": "Never", "never": "从不",
"tank": "战车", "tank": "坦克",
"last_played": "Last played", "last_played": "最后一次游玩",
"battles": "Career battles", "battles": "生涯战斗数",
"unrecorded": "--", "unrecorded": "--",
"wins": "胜利" "wins": "胜利"
}, },
"large_warning": { "large_warning": {
"title": "That's a lot of tanks!", "title": "坦克数量太多了!",
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!", "description": "您确定要创建包含 {count} 个坦克的游玩列表吗?以50%胜率计算,这需要 {time} 小时的游玩时间!",
"cancel": "取消", "cancel": "取消",
"proceed": "Proceed" "proceed": "继续操作"
}, },
"generate": "Generate playlist", "generate": "创建游玩列表",
"name": "Playlist", "name": "遊玩列表",
"description": "Tank playlist generator", "description": "坦克游玩列表生成器",
"seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!", "seo_description": "想不到在坦克世界闪击战玩什么? 使用BlitzKit 游玩列表来获得建议!",
"button": "Generate", "button": "生成",
"pool_size": "{count} possible tanks" "pool_size": "{count} 辆可生成的坦克"
}, },
"mixer": { "mixer": {
"name": "Mixer", "name": "组装",
"button": "Mix and match", "button": "混合搭配",
"description": "Mix tank modules", "description": "混合坦克模块",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz", "seo_description": "《坦克世界:闪击战》的车体, 炮塔, 主炮混合搭配",
"screenshot": "Screenshot", "screenshot": "屏幕截图",
"randomize": "Randomize" "randomize": "随机搭配"
}, },
"guess": { "guess": {
"name": "猜坦克", "name": "猜测",
"button": "猜猜看", "button": "What could it be?",
"description": "战车剪影小游戏", "description": "坦克剪影小游戏",
"seo_description": "看剪影猜战车", "seo_description": "看剪影猜坦克",
"search": { "search": {
"placeholder": "输入你的猜想……", "placeholder": "输入你的猜想……",
"skip": "跳过", "skip": "跳过",
@@ -814,22 +814,22 @@
"next": "下一个", "next": "下一个",
"no_results": "沒有結果" "no_results": "沒有結果"
}, },
"stats": "{correct} / {total} guessed correctly" "stats": "猜对了 {correct} / {total}"
}, },
"tankopedia": { "tankopedia": {
"search": "搜索属性……", "search": "搜索属性……",
"name": "战车百科", "name": "战车百科",
"button": "搜索战车", "button": "搜索战车",
"description": "闪击战战车百科全书", "description": "闪击战坦克百科全书",
"seo_description": "战车世界闪击战的所有坦克参数, 装甲, 等等...", "seo_description": "《坦克世界:闪击战》的所有坦克参数, 装甲, 等等...",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "{name} - {tier} 阶 {nationality} {class} | {website} 坦克百科 | 坦克世界闪击战 (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.", "tank_seo_description": "{name} 装甲,统计资料,装备,规定配给等。 {dpm} 每分钟伤害, {alpha} 伤害值, {penetration}毫米 穿透, {speed}公里/小時 最高速度, {health} 生命值。",
"visualizers": { "visualizers": {
"reload": { "reload": {
"skip": "跳过" "skip": "跳过"
}, },
"assault": { "assault": {
"title": "Assault shells" "title": "突击弹"
}, },
"rotator": { "rotator": {
"hull": "车体", "hull": "车体",
@@ -852,10 +852,10 @@
"dead_rail": "死亡铁道" "dead_rail": "死亡铁道"
}, },
"flexibility": { "flexibility": {
"yaw": "Yaw", "yaw": "水平角度",
"pitch": "Pitch", "pitch": "俯仰角度",
"min": "Min", "min": "最少俯角",
"max": "Max" "max": "最大仰角"
} }
}, },
"sandbox": { "sandbox": {
@@ -901,15 +901,15 @@
"track": "履带" "track": "履带"
}, },
"status": { "status": {
"penetration": "{damage}hp penetration", "penetration": "{damage}生命值 击穿",
"blocked": "抵挡伤害", "blocked": "抵挡伤害",
"ricochet": "跳弹", "ricochet": "跳弹",
"splash": "{damage}hp splash" "splash": "{damage}生命值 溅射"
}, },
"stats": { "stats": {
"thickness_and_angle": "{thickness}mm {angle}°", "thickness_and_angle": "{thickness}毫米 {angle}度°",
"nominal": "{thickness}mm nominal", "nominal": "{thickness}毫米 装甲厚度",
"ricochet_loss": "{loss}% penetration" "ricochet_loss": "{loss}% 击穿"
} }
} }
}, },
@@ -929,13 +929,13 @@
"compare": "比较", "compare": "比较",
"swap": { "swap": {
"button": "调换", "button": "调换",
"title": "调换战车" "title": "调换坦克"
}, },
"vanilla": "原版", "vanilla": "原版",
"opentest": "开放测试", "opentest": "开放测试",
"full_name": "全称", "full_name": "全称",
"nation": "国别", "nation": "国别",
"class": "战车车型", "class": "坦克车型",
"tier": "阶级", "tier": "阶级",
"type": "战车类型", "type": "战车类型",
"dev_id": "开发参数:ID", "dev_id": "开发参数:ID",
@@ -947,20 +947,20 @@
"tech_tree": { "tech_tree": {
"title": "科技树", "title": "科技树",
"battle": "一场比赛", "battle": "一场比赛",
"battles": "{count} battles", "battles": "{count} 战斗场次",
"route": "Route {index} of {count}" "route": "路线 {index} / {count}"
}, },
"game_modes": { "game_modes": {
"title": "游戏模式技能" "title": "游戏模式技能"
}, },
"review": { "review": {
"title": "回顾影片", "title": "回顾影片",
"join": "Apply to {name} YouTuber program" "join": "应用到 {name} YouTuber程序"
}, },
"configuration": { "configuration": {
"title": "配置设定", "title": "配置设定",
"modules": { "modules": {
"title": "战车模块", "title": "坦克模块",
"stock": "白板", "stock": "白板",
"upgrade": "顶级配置" "upgrade": "顶级配置"
}, },
@@ -975,7 +975,7 @@
"consumables": { "consumables": {
"title": "消耗品", "title": "消耗品",
"clear": "清除", "clear": "清除",
"info": "黄色高亮消耗品不影响战车参数。" "info": "黄色高亮消耗品不影响坦克参数。"
}, },
"skills": { "skills": {
"title": "乘员技能", "title": "乘员技能",
@@ -991,15 +991,15 @@
"title": "属性", "title": "属性",
"compare": { "compare": {
"label": "比较", "label": "比较",
"class": "Tier {tier} {class}", "class": "{tier}阶 {class}",
"tier": "All of tier {tier}", "tier": "所有 {tier} 阶",
"all": "所有战车" "all": "所有坦克"
}, },
"values": { "values": {
"shell_index": "Shell {index}", "shell_index": "炮弹 {index}",
"at_distance": "At {distance}m", "at_distance": "在 {distance} 公尺",
"at_distance_slider_label": "距离", "at_distance_slider_label": "距离",
"dispersion_at_distance": "Dispersion at {distance}m", "dispersion_at_distance": "在 {distance} 公尺的扩散度",
"gun_flexibility": "炮管转动界限", "gun_flexibility": "炮管转动界限",
"speed": "速度", "speed": "速度",
"terrain_coefficients": "地形系数", "terrain_coefficients": "地形系数",
@@ -1015,13 +1015,13 @@
"dpmEffective": "60秒内生效", "dpmEffective": "60秒内生效",
"shells": "炮弹 ", "shells": "炮弹 ",
"mostOptimalShellIndex": "最优炮弹", "mostOptimalShellIndex": "最优炮弹",
"shellReloads": "Reloads", "shellReloads": "装填时间",
"shellReload": "装填时间", "shellReload": "装填时间",
"intraClip": "炮弹装填", "intraClip": "炮弹装填",
"caliber": "口径", "caliber": "口径",
"penetration": "穿深", "penetration": "穿深",
"damage": "伤害", "damage": "伤害",
"damageWithoutAssault": "Damage without assault shells", "damageWithoutAssault": "无突击弹的伤害",
"clipDamage": "弹匣伤害期望值", "clipDamage": "弹匣伤害期望值",
"moduleDamage": "模组伤害", "moduleDamage": "模组伤害",
"explosionRadius": "溅射半径", "explosionRadius": "溅射半径",
@@ -1101,14 +1101,14 @@
}, },
"compare": { "compare": {
"name": "比较", "name": "比较",
"button": "战车比较", "button": "坦克比较",
"description": "比较战车参数", "description": "比较坦克参数",
"seo_description": "比较《战车世界:闪击战》中的战车参数和装备配置", "seo_description": "比较《坦克世界:闪击战》中的坦克参数和装备配置",
"actions": { "actions": {
"add": { "add": {
"button": "添加", "button": "添加",
"title": "添加战车", "title": "添加坦克",
"description": "选择要加入比较的战车" "description": "选择要加入比较的坦克"
}, },
"clear": "清除", "clear": "清除",
"deltas": { "deltas": {
@@ -1144,20 +1144,20 @@
}, },
"performance": { "performance": {
"name": "性能表现", "name": "性能表现",
"button": "查看战车", "button": "查看坦克",
"description": "战车性能统计", "description": "坦克性能统计",
"seo_description": "最好的战车及其在战车世界闪击战中的表现", "seo_description": "最好的坦克及其在《坦克世界:闪击战》中的表现",
"estimation": { "estimation": {
"body": "Estimated stats based on {count} players. Updated {age} ago.", "body": "基于 {count} 个玩家的估计数据。更新于 {age} 之前。",
"hours": "{count} hours", "hours": "{count} 小时",
"minutes": "{count} minutes" "minutes": "{count} 分钟"
}, },
"player_count": { "player_count": {
"label": "玩家数量" "label": "玩家数量"
}, },
"table": { "table": {
"tanks": { "tanks": {
"header": "战车", "header": "坦克",
"total": "全部" "total": "全部"
}, },
"stats": { "stats": {
@@ -1181,18 +1181,18 @@
"name": "图库", "name": "图库",
"button": "浏览全部", "button": "浏览全部",
"description": "Blitz玩家头像", "description": "Blitz玩家头像",
"seo_description": "所有战车世界闪击战玩家头像", "seo_description": "所有《坦克世界:闪击战》玩家头像",
"last_updated": "Last updated on {time}", "last_updated": "最后更新于 {time}",
"search": { "search": {
"hint": "搜索頭像……", "hint": "搜索頭像……",
"results": "{count} avatars" "results": "{count} 头像"
} }
}, },
"session": { "session": {
"name": "场次", "name": "场次",
"button": "开始追踪", "button": "开始追踪",
"description": "即时场次统计", "description": "即时场次统计",
"seo_description": "在游玩 战车世界闪击战 时,实时追踪你的统计数据", "seo_description": "在游玩《坦克世界:闪击战》时,实时追踪你的统计数据",
"search": { "search": {
"lookup": "查询玩家以开始使用", "lookup": "查询玩家以开始使用",
"hint": "搜索玩家……", "hint": "搜索玩家……",
@@ -1200,14 +1200,14 @@
"no_results": "找不到玩家" "no_results": "找不到玩家"
}, },
"controls": { "controls": {
"title": "Tracking {name}", "title": "正在追踪 {name}",
"subtitle": "Since {time}", "subtitle": "起始于 {time}",
"reset": "重置", "reset": "重置",
"change": "变更" "change": "变更"
}, },
"no_cc": { "no_cc": {
"title": "你正在尝试追踪一个 CC 帐戶", "title": "你正在尝试追踪一个 CC 帐戶",
"body": "CC(社群贡献者)账戶没有公开的战车统计数据。 我们正在为CC账戶添加嵌入功能,敬请期待。", "body": "CC(社群贡献者)账戶没有公开的坦克统计数据。 我们正在为CC账戶添加嵌入功能,敬请期待。",
"alright": "好的" "alright": "好的"
}, },
"no_battles": { "no_battles": {
@@ -1215,16 +1215,16 @@
"hint": "去玩一场游戏吧" "hint": "去玩一场游戏吧"
}, },
"table": { "table": {
"tank": "战车", "tank": "坦克",
"total": "全部", "total": "全部",
"remove": "移除" "remove": "移除"
} }
}, },
"tier_list": { "tier_list": {
"name": "阶级列表", "name": "阶级列表",
"button": "排名战车", "button": "排名坦克",
"description": "战车世界闪击战 战车等级排名", "description": "《坦克世界:闪击战》坦克等级排名",
"seo_description": "基于玩家评分和统计表现,从最佳到最差排名的《战车世界:闪击战》战车等级列表", "seo_description": "基于玩家评分和统计表现,从最佳到最差排名的《坦克世界:闪击战》坦克等级列表",
"buttons": { "buttons": {
"reset": "重置", "reset": "重置",
"share": "分享" "share": "分享"
@@ -1234,7 +1234,7 @@
"name": "嵌入", "name": "嵌入",
"button": "立即创建", "button": "立即创建",
"description": "用于串流的嵌入", "description": "用于串流的嵌入",
"seo_description": "创建具有极高自订化的《战车世界:闪击战》串流嵌入功能", "seo_description": "创建具有极高自订化的《坦克世界:闪击战》串流嵌入功能",
"coming_soon": "更多嵌入功能即将推出。", "coming_soon": "更多嵌入功能即将推出。",
"suggest": "建议与想法", "suggest": "建议与想法",
"preview": { "preview": {
@@ -1261,7 +1261,7 @@
"cancel": "取消", "cancel": "取消",
"success": { "success": {
"title": "拷贝 URL", "title": "拷贝 URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".", "info": "已为 {name} 生成 URL。 当 URL 粘贴后,嵌入功能将开始追踪。 您可以随时在 PC 上点击右键或手机上长按并选择「重置」来重置。",
"back": "返回", "back": "返回",
"copy": "拷贝 URL" "copy": "拷贝 URL"
} }
@@ -1285,13 +1285,13 @@
"height": "高度", "height": "高度",
"showTotal": "显示总计", "showTotal": "显示总计",
"listGap": "列表间距", "listGap": "列表间距",
"listMaxTanks": "最多战车数量", "listMaxTanks": "最多坦克数量",
"cardRadius": "卡片半径", "cardRadius": "卡片半径",
"cardHeaderBackgroundColor": "卡片标题背景颜色", "cardHeaderBackgroundColor": "卡片标题背景颜色",
"cardHeaderPadding": "卡片标题內间距", "cardHeaderPadding": "卡片标题內间距",
"cardTitle": "卡片标题", "cardTitle": "卡片标题",
"cardTitleTypeColor": "卡片标题(战车类型)颜色", "cardTitleTypeColor": "卡片标题(坦克类型)颜色",
"cardTitleClassIcon": "卡片标题(战车车型)颜色", "cardTitleClassIcon": "卡片标题(坦克车型)颜色",
"cardBodyBackgroundColor": "卡片內容背景颜色", "cardBodyBackgroundColor": "卡片內容背景颜色",
"cardBodyPadding": "卡片內容內間距", "cardBodyPadding": "卡片內容內間距",
"columnGap": "栏间距", "columnGap": "栏间距",
@@ -1312,13 +1312,13 @@
"name": "图表", "name": "图表",
"button": "瘋狂研究", "button": "瘋狂研究",
"description": "大量图标", "description": "大量图标",
"seo_description": "有关战车世界闪击战 战车、玩家与事件的各种图表与图形" "seo_description": "有关《坦克世界:闪击战》坦克、玩家与事件的各种图表与图形"
}, },
"rating": { "rating": {
"name": "评价战斗", "name": "评价战斗",
"button": "查看排行榜", "button": "查看排行榜",
"description": "完整评价战斗排行榜", "description": "完整评价战斗排行榜",
"seo_description": "《战车世界:闪击战》即时及已存档的完整评分排行榜" "seo_description": "《坦克世界:闪击战》即时及已存档的完整评分排行榜"
}, },
"discord": { "discord": {
"name": "Discord机器人", "name": "Discord机器人",
@@ -1336,7 +1336,7 @@
"name": "玩家", "name": "玩家",
"button": "查询玩家", "button": "查询玩家",
"description": "定期玩家统计", "description": "定期玩家统计",
"seo_description": "战车世界闪击战 定期玩家统计" "seo_description": "《坦克世界:闪击战》定期玩家统计"
}, },
"home": { "home": {
"name": "主页", "name": "主页",
@@ -1345,14 +1345,14 @@
"seo_description": "开始使用 BlitzKit" "seo_description": "开始使用 BlitzKit"
}, },
"api": { "api": {
"name": "API", "name": "应用程序接口(API)",
"button": "To the docs", "button": "前往文档",
"description": "BlitzKit hosted API", "description": "BlitzKit 已托管的应用程序接口(API)",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz" "seo_description": "BlitzKit 托管的《坦克世界:闪击战》静态应用程序接口(API)"
} }
}, },
"footer": { "footer": {
"motto": "战车世界闪击战 全方位工具", "motto": "《坦克世界:闪击战》全方位工具",
"copyright": "本网站与 Wargaming战游网 或任何服务无关", "copyright": "本网站与 Wargaming战游网 或任何服务无关",
"about": { "about": {
"title": "关于", "title": "关于",
+5 -1
View File
@@ -14,6 +14,7 @@
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1", "@types/lz4js": "^0.2.1",
"@types/progress": "^2.0.7", "@types/progress": "^2.0.7",
"@types/steam-user": "^5.1.0",
"@types/three": "^0.180.0", "@types/three": "^0.180.0",
"crc": "^4.3.2", "crc": "^4.3.2",
"csv-parse": "^6.1.0", "csv-parse": "^6.1.0",
@@ -27,6 +28,7 @@
"progress": "^2.0.3", "progress": "^2.0.3",
"puppeteer": "^24.22.3", "puppeteer": "^24.22.3",
"sharp": "^0.34.4", "sharp": "^0.34.4",
"steam-user": "^5.2.3",
"three": "^0.180.0", "three": "^0.180.0",
"ts-proto": "^2.7.7", "ts-proto": "^2.7.7",
"yaml": "^2.8.1" "yaml": "^2.8.1"
@@ -42,8 +44,10 @@
"build:bot": "bun src/buildBot.ts", "build:bot": "bun src/buildBot.ts",
"build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts", "build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts",
"build:proxyclient": "bun src/buildProxyClient.ts", "build:proxyclient": "bun src/buildProxyClient.ts",
"build:catalogitemaccesssor": "bun src/buildCatalogItemAccessor.ts",
"prunestrings": "bun src/pruneStrings.ts", "prunestrings": "bun src/pruneStrings.ts",
"build:protos": "bun src/protos.ts" "build:protos": "bun src/protos.ts",
"patchnotes": "bun src/patchNotes.ts"
}, },
"type": "module" "type": "module"
} }
+7 -9
View File
@@ -4,12 +4,10 @@ import { mkdir, writeFile } from "fs/promises";
import { parse as parsePath } from "path"; import { parse as parsePath } from "path";
import ProgressBar from "progress"; import ProgressBar from "progress";
import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
import { readStringDVPL } from "../src/core/blitz/readStringDVPL"; import { vfs } from "./buildAssets/constants";
import { DATA } from "./buildAssets/constants";
import { readYAMLDVPL } from "./core/blitz/readYAMLDVPL";
import { writeDVPL } from "./core/blitz/writeDVPL"; import { writeDVPL } from "./core/blitz/writeDVPL";
const versionTextFile = await readStringDVPL(`${DATA}/version.txt`); const versionTextFile = await vfs.text(`Data/version.txt`);
const currentVersion = versionTextFile const currentVersion = versionTextFile
.split(" ")[0] .split(" ")[0]
.split(".") .split(".")
@@ -51,7 +49,7 @@ while (true) {
const { dir } = parsePath(path); const { dir } = parsePath(path);
try { try {
await mkdir(`${DATA}/${dir}`, { recursive: true }); await mkdir(`Data/${dir}`, { recursive: true });
} catch (error) { } catch (error) {
console.warn(`Failed to make directory "${dir}"`); console.warn(`Failed to make directory "${dir}"`);
} }
@@ -60,7 +58,7 @@ while (true) {
const buffer = Buffer.from(data); const buffer = Buffer.from(data);
await writeFile( await writeFile(
`${DATA}/${path}${isDvpl ? "" : ".dvpl"}`, `Data/${path}${isDvpl ? "" : ".dvpl"}`,
new Uint8Array(isDvpl ? buffer : writeDVPL(buffer)) new Uint8Array(isDvpl ? buffer : writeDVPL(buffer))
); );
@@ -76,14 +74,14 @@ while (true) {
}/en.yaml` }/en.yaml`
); );
const newStrings = parseYaml(await localizationsResponse.text()); const newStrings = parseYaml(await localizationsResponse.text());
const oldStrings = await readYAMLDVPL<Record<string, string>>( const oldStrings = await vfs.yaml<Record<string, string>>(
`${DATA}/Strings/en.yaml` `Data/Strings/en.yaml`
); );
const patchedStrings = { ...oldStrings, ...newStrings }; const patchedStrings = { ...oldStrings, ...newStrings };
const patchedContent = stringifyYaml(patchedStrings); const patchedContent = stringifyYaml(patchedStrings);
await writeFile( await writeFile(
`${DATA}/Strings/en.yaml`, `Data/Strings/en.yaml`,
new Uint8Array(writeDVPL(Buffer.from(patchedContent))) new Uint8Array(writeDVPL(Buffer.from(patchedContent)))
); );
} }
+31 -30
View File
@@ -11,10 +11,10 @@ import {
getAccountInfo, getAccountInfo,
getTankStats, getTankStats,
idToRegion, idToRegion,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { chunk, times } from 'lodash-es'; import { chunk, times } from "lodash-es";
import { commitAssets } from './core/github/commitAssets'; import { AssetUploader } from "./core/github/assetUploader";
import { fetchPreDiscoveredIds } from './core/github/fetchPreDiscoveredIds'; import { fetchPreDiscoveredIds } from "./core/github/fetchPreDiscoveredIds";
interface DataPoint { interface DataPoint {
x: number; x: number;
@@ -23,7 +23,7 @@ interface DataPoint {
} }
type OptionalSecondLevel<T> = { type OptionalSecondLevel<T> = {
[P in keyof T]: P extends 'samples' [P in keyof T]: P extends "samples"
? T[P] ? T[P]
: { : {
[K in keyof T[P]]?: T[P][K]; [K in keyof T[P]]?: T[P][K];
@@ -46,16 +46,16 @@ const startTime = Date.now();
const preDiscoveredIds = await fetchPreDiscoveredIds(); const preDiscoveredIds = await fetchPreDiscoveredIds();
const playerIds: Record<Region, number[][]> = { const playerIds: Record<Region, number[][]> = {
asia: chunk( asia: chunk(
preDiscoveredIds.filter((id) => idToRegion(id) === 'asia'), preDiscoveredIds.filter((id) => idToRegion(id) === "asia"),
PLAYER_IDS_PER_CALL, PLAYER_IDS_PER_CALL
), ),
com: chunk( com: chunk(
preDiscoveredIds.filter((id) => idToRegion(id) === 'com'), preDiscoveredIds.filter((id) => idToRegion(id) === "com"),
PLAYER_IDS_PER_CALL, PLAYER_IDS_PER_CALL
), ),
eu: chunk( eu: chunk(
preDiscoveredIds.filter((id) => idToRegion(id) === 'eu'), preDiscoveredIds.filter((id) => idToRegion(id) === "eu"),
PLAYER_IDS_PER_CALL, PLAYER_IDS_PER_CALL
), ),
}; };
let regionIndex = 0; let regionIndex = 0;
@@ -71,10 +71,10 @@ times(THREADS, async () => {
const regionIds = playerIds[region]; const regionIds = playerIds[region];
const [ids] = regionIds.splice( const [ids] = regionIds.splice(
Math.floor(Math.random() * regionIds.length), Math.floor(Math.random() * regionIds.length),
1, 1
); );
const accountInfo = await getAccountInfo(region, ids, undefined, { const accountInfo = await getAccountInfo(region, ids, undefined, {
fields: 'last_battle_time,statistics.all.battles', fields: "last_battle_time,statistics.all.battles",
}); });
const filteredIds = ids.filter((_, index) => { const filteredIds = ids.filter((_, index) => {
@@ -103,9 +103,9 @@ times(THREADS, async () => {
const players = await Promise.all( const players = await Promise.all(
filteredIds.map((id) => filteredIds.map((id) =>
getTankStats(region, id, { getTankStats(region, id, {
fields: 'last_battle_time,battle_life_time,all,tank_id', fields: "last_battle_time,battle_life_time,all,tank_id",
}), })
), )
); );
players.forEach((tanks) => { players.forEach((tanks) => {
@@ -114,7 +114,7 @@ times(THREADS, async () => {
if (!tankIds.includes(tank.tank_id)) { if (!tankIds.includes(tank.tank_id)) {
console.log( console.log(
`Found new tank: ${tank.tank_id} (${tankIds.length + 1})`, `Found new tank: ${tank.tank_id} (${tankIds.length + 1})`
); );
tankIds.push(tank.tank_id); tankIds.push(tank.tank_id);
tanksSorted[tank.tank_id] = []; tanksSorted[tank.tank_id] = [];
@@ -140,9 +140,10 @@ async function postWork() {
console.log( console.log(
`Generating statistics based on ${samples.d_120.toLocaleString()} players (${samples.total.toLocaleString()} checked in total) and ${ `Generating statistics based on ${samples.d_120.toLocaleString()} players (${samples.total.toLocaleString()} checked in total) and ${
tankIds.length tankIds.length
} tanks...`, } tanks...`
); );
using uploader = new AssetUploader("averages");
const averages: Record<number, AverageDefinitionsEntry> = {}; const averages: Record<number, AverageDefinitionsEntry> = {};
tankIds.forEach((id) => { tankIds.forEach((id) => {
@@ -179,7 +180,7 @@ async function postWork() {
const data: DataPoint[] = dataWY.map(({ w, y }, index) => ({ const data: DataPoint[] = dataWY.map(({ w, y }, index) => ({
w, w,
x: x:
(key === 'battle_life_time' (key === "battle_life_time"
? tanks[index].battle_life_time ? tanks[index].battle_life_time
: tanks[index].all[key]) / w, : tanks[index].all[key]) / w,
y, y,
@@ -190,7 +191,7 @@ async function postWork() {
function sum(slicer: (data: DataPoint) => number) { function sum(slicer: (data: DataPoint) => number) {
return data.reduce( return data.reduce(
(accumulator, data) => accumulator + slicer(data), (accumulator, data) => accumulator + slicer(data),
0, 0
); );
} }
@@ -231,14 +232,14 @@ async function postWork() {
latest, latest,
}; };
commitAssets('averages', [ await uploader.add({
{ path: `averages/${latest}.pb`,
path: `averages/${latest}.pb`, content: AverageDefinitions.encode(averageDefinitions).finish(),
content: AverageDefinitions.encode(averageDefinitions).finish(), });
}, await uploader.add({
{ path: "averages/manifest.json",
path: 'averages/manifest.json', content: new TextEncoder().encode(JSON.stringify(manifest)),
content: new TextEncoder().encode(JSON.stringify(manifest)), });
},
]); await uploader.flush();
} }
@@ -1,49 +0,0 @@
import { BLITZKIT_TANK_ICON_SIZE, fetchTankDefinitions } from "@blitzkit/core";
import { launch } from "puppeteer";
import sharp from "sharp";
import { commitAssets } from "../core/github/commitAssets";
import { type FileChange } from "../core/github/commitMultipleFiles";
export async function blitzkitTankIcons() {
console.log("Building blitzkit tank icons...");
const browser = await launch();
const page = await browser.newPage();
const tanks = Object.values((await fetchTankDefinitions()).tanks);
const files: FileChange[] = [];
let index = 0;
for (const { id, name } of tanks) {
await page.setViewport({
width: BLITZKIT_TANK_ICON_SIZE.width - 1,
height: BLITZKIT_TANK_ICON_SIZE.height,
});
try {
await page.goto(`http://localhost:4321/api/tankopedia/tank-icon/${id}/`, {
waitUntil: "networkidle0",
});
} catch (error) {
console.error(`${id} ${name} time out`, error);
continue;
}
const screenshot = await page.screenshot({
type: "webp",
omitBackground: true,
});
const content = await sharp(screenshot).trim({ threshold: 100 }).toBuffer();
index++;
console.log(
`Rendered ${id} (${name}) ${((index / tanks.length) * 100).toFixed(2)}%`
);
files.push({ path: `icons/tanks/big/${id}.webp`, content });
}
await page.close();
await browser.close();
await commitAssets("tank icons", files);
}
@@ -1,48 +1,42 @@
import { readdir } from 'fs/promises'; import sharp from "sharp";
import sharp from 'sharp'; import { AssetUploader } from "../core/github/assetUploader";
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { vfs } from "./constants";
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
import { commitAssets } from '../core/github/commitAssets';
import { FileChange } from '../core/github/commitMultipleFiles';
import { DATA } from './constants';
export async function boosterIcons() { export async function boosterIcons() {
const boosterFiles = (await readdir(`${DATA}/Gfx/Shared/boosters`)).filter( console.log("Building booster icons...");
(file) => !file.includes('@2x.txt') && !file.startsWith('texture0'),
using uploader = new AssetUploader("booster icons");
const boosterFiles = (vfs.dir(`Data/Gfx/Shared/boosters`)).filter(
(file) => !file.includes("@2x.txt") && !file.startsWith("texture0")
); );
const image = sharp( const image = sharp(
await readDVPLFile(`${DATA}/Gfx/Shared/boosters/texture0.packed.webp`), await vfs.file(`Data/Gfx/Shared/boosters/texture0.packed.webp`)
);
const changes: FileChange[] = [];
await Promise.all(
boosterFiles.map(async (file) => {
const name = file.match(/booster_(.+).txt/)?.[1];
if (name === undefined) return;
const sizes = (
await readStringDVPL(`${DATA}/Gfx/Shared/boosters/${file}`)
)
.split('\n')[4]
.split(' ')
.map(Number);
const content = await image
.clone()
.extract({
left: sizes[0],
top: sizes[1],
width: sizes[2],
height: sizes[3],
})
.toBuffer();
changes.push({
path: `icons/boosters/${name}.webp`,
content,
});
}),
); );
await commitAssets('booster icons', changes); for (const file of boosterFiles) {
const name = file.match(/booster_(.+).txt/)?.[1];
if (name === undefined) continue;
const sizes = (await vfs.text(`Data/Gfx/Shared/boosters/${file}`))
.split("\n")[4]
.split(" ")
.map(Number);
const content = await image
.clone()
.extract({
left: sizes[0],
top: sizes[1],
width: sizes[2],
height: sizes[3],
})
.toBuffer();
await uploader.add({
path: `icons/boosters/${name}.webp`,
content,
});
}
await uploader.flush();
} }
@@ -1,19 +1,14 @@
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { AssetUploader } from "../core/github/assetUploader";
import { commitAssets } from '../core/github/commitAssets'; import { vfs } from "./constants";
import { FileChange } from '../core/github/commitMultipleFiles';
import { DATA } from './constants';
export async function camouflageIcons() { export async function camouflageIcons() {
console.log('Building camouflage icons...'); console.log("Building camouflage icons...");
const content = await readDVPLFile(
`${DATA}/Gfx/UI/Hangar/IconCamouflage.packed.webp`,
);
const changes: FileChange[] = [
{
content,
path: 'icons/camo.webp',
},
];
await commitAssets('camo icons', changes); using uploader = new AssetUploader("camo icons");
const content = await vfs.file(
`Data/Gfx/UI/Hangar/IconCamouflage.packed.webp`
);
await uploader.add({ content, path: "icons/camo.webp" });
await uploader.flush();
} }
+10 -35
View File
@@ -1,37 +1,12 @@
import { readdir } from 'fs/promises'; import { assertSecret } from "@blitzkit/core";
import { argv } from 'process'; import { SteamVFS } from "../core/steam/vfs";
const WINDOWS_APPS = 'C:/Program Files/WindowsApps'; const APP = 444200;
const wotbPackageRegex = /7458BE2C\..+_\d+(\.\d+)+_x64__.+/; const DEPOT = 444202;
const isDepot = argv.includes('--depot'); export const vfs = await new SteamVFS(
const isManual = argv.includes('--manual'); assertSecret(import.meta.env.STEAM_USERNAME),
assertSecret(import.meta.env.STEAM_PASSWORD),
let data: string; APP,
let compressed: boolean; DEPOT
).init();
if (isManual) {
data = '../../temp/game/Data';
compressed = true;
} else if (isDepot) {
const [installationVersion] = await readdir(`../../depots/444202`);
data = `../../depots/444202/${installationVersion}/Data`;
compressed = true;
} else {
const directories = await readdir(WINDOWS_APPS).then((files) =>
files.filter((file) => wotbPackageRegex.test(file)),
);
if (directories.length !== 1) {
throw new Error(
`Expected 1 directory for WoTB Windows App, got ${directories.length}.`,
);
}
const [windowsApp] = directories;
data = `${WINDOWS_APPS}/${windowsApp}/Data`;
compressed = false;
}
export const DATA = data;
export const DATA_COMPRESSED = compressed;
@@ -1,178 +1,176 @@
import { existsSync } from 'fs'; import sharp from "sharp";
import sharp from 'sharp'; import { AssetUploader } from "../core/github/assetUploader";
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { vfs } from "./constants";
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL'; import { ConsumablesCommon, ProvisionsCommon } from "./definitions";
import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
import { commitAssets } from '../core/github/commitAssets';
import { FileChange } from '../core/github/commitMultipleFiles';
import { DATA } from './constants';
import { ConsumablesCommon, ProvisionsCommon } from './definitions';
interface Mappings { interface Mappings {
Header: { version: number }; Header: { version: number };
StyleSheets: { StyleSheets: {
selectors: string[]; selectors: string[];
properties: { 'bg-sprite': string }; properties: { "bg-sprite": string };
}[]; }[];
} }
const listItemsPattern = /<items path="(.+)\.xml"\/>/g; const listItemsPattern = /<items path="(.+)\.xml"\/>/g;
export async function consumableProvisionIcons() { export async function consumableProvisionIcons() {
console.log('Building consumable and provision icons...'); console.log("Building consumable and provision icons...");
const changes: FileChange[] = [];
using uploader = new AssetUploader("consumable and provision icons");
const styles = [ const styles = [
'UI/Styles/Lobby/Inventory/Event/InventoryBigStyles.yaml', "UI/Styles/Lobby/Inventory/Event/InventoryBigStyles.yaml",
'UI/Styles/Lobby/Inventory/Event/InventoryNormalStyles.yaml', "UI/Styles/Lobby/Inventory/Event/InventoryNormalStyles.yaml",
'UI/Styles/Lobby/Inventory/InventoryNormalStyles.yaml', "UI/Styles/Lobby/Inventory/InventoryNormalStyles.yaml",
'UI/Screens3/Lobby/Inventory/Equipment/EquipmentItemImage.style.yaml', "UI/Screens3/Lobby/Inventory/Equipment/EquipmentItemImage.style.yaml",
'UI/Screens/Battle/Styles/BattleEquipmentStyles.yaml', "UI/Screens/Battle/Styles/BattleEquipmentStyles.yaml",
]; ];
const styleSheets = await Promise.all( const styleSheets = await Promise.all(
styles.map(async (path) => styles.map(async (path) =>
Object.values( Object.values(
(await readYAMLDVPL<Mappings>(`${DATA}/${path}`)).StyleSheets, (
), await vfs.yaml<Mappings>(`Data/${path}`)
), ).StyleSheets
)
)
).then((array) => array.flat()); ).then((array) => array.flat());
for (const match of ( for (const match of (
await readStringDVPL( await vfs.text(
`${DATA}/XML/item_defs/vehicles/common/consumables/list.xml`, `Data/XML/item_defs/vehicles/common/consumables/list.xml`
) )
).matchAll(listItemsPattern)) { ).matchAll(listItemsPattern)) {
const consumablesCommon = await readXMLDVPL<{ const consumablesCommon = await vfs.xml<{
root: ConsumablesCommon; root: ConsumablesCommon;
}>(`${DATA}/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`); }>(`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`);
await Promise.all( for (const consumable of Object.values(consumablesCommon.root)) {
Object.values(consumablesCommon.root).map(async (consumable) => { const styleSheet = styleSheets.find((styleSheet) =>
const styleSheet = styleSheets.find((styleSheet) => styleSheet.selectors.some((selector) =>
styleSheet.selectors.some((selector) => selector.includes(`${consumable.icon} `)
selector.includes(`${consumable.icon} `), )
), );
if (!styleSheet) {
console.warn(
`No style sheet found for consumable ${consumable.icon}; skipping...`
); );
continue;
}
if (!styleSheet) { const configPath = styleSheet.properties["bg-sprite"]
console.warn( .replace("~res:/", "")
`No style sheet found for consumable ${consumable.icon}; skipping...`, .replace(".psd", "")
); .replace(".txt", "");
return;
}
const configPath = styleSheet.properties['bg-sprite'] if (vfs.has(`Data/${configPath}.packed.webp`)) {
.replace('~res:/', '') const image = sharp(
.replace('.psd', '') await vfs.file(`Data/${configPath}.packed.webp`)
.replace('.txt', ''); );
const content = await image.trim({ threshold: 100 }).toBuffer();
if (existsSync(`${DATA}/${configPath}.packed.webp`)) { await uploader.add({
const image = sharp( path: `icons/consumables/${consumable.id}.webp`,
await readDVPLFile(`${DATA}/${configPath}.packed.webp`), content,
); });
const content = await image.trim({ threshold: 100 }).toBuffer(); } else {
const consumablesTexture = sharp(
await vfs.file(
`${`Data/${configPath}`
.split("/")
.slice(0, -1)
.join("/")}/texture0.packed.webp`
)
);
const sizes = (await vfs.text(`Data/${configPath}.txt`))
.split("\n")[4]
.split(" ")
.map(Number);
const content = await consumablesTexture
.clone()
.extract({
left: sizes[0],
top: sizes[1],
width: sizes[2],
height: sizes[3],
})
.toBuffer();
changes.push({ await uploader.add({
path: `icons/consumables/${consumable.id}.webp`, path: `icons/consumables/${consumable.id}.webp`,
content, content,
}); });
} else { }
const consumablesTexture = sharp( }
await readDVPLFile(
`${`${DATA}/${configPath}`.split('/').slice(0, -1).join('/')}/texture0.packed.webp`,
),
);
const sizes = (await readStringDVPL(`${DATA}/${configPath}.txt`))
.split('\n')[4]
.split(' ')
.map(Number);
const content = await consumablesTexture
.clone()
.extract({
left: sizes[0],
top: sizes[1],
width: sizes[2],
height: sizes[3],
})
.toBuffer();
changes.push({
path: `icons/consumables/${consumable.id}.webp`,
content,
});
}
}),
);
} }
for (const match of ( for (const match of (
await readStringDVPL( await vfs.text(
`${DATA}/XML/item_defs/vehicles/common/provisions/list.xml`, `Data/XML/item_defs/vehicles/common/provisions/list.xml`
) )
).matchAll(listItemsPattern)) { ).matchAll(listItemsPattern)) {
const provisionsCommon = await readXMLDVPL<{ const provisionsCommon = await vfs.xml<{
root: ProvisionsCommon; root: ProvisionsCommon;
}>(`${DATA}/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`); }>(`Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`);
await Promise.all(
Object.values(provisionsCommon.root).map(async (provision) => { for (const provision of Object.values(provisionsCommon.root)) {
const styleSheet = styleSheets.find((styleSheet) => const styleSheet = styleSheets.find((styleSheet) =>
styleSheet.selectors.some((selector) => styleSheet.selectors.some((selector) =>
selector.includes(`${provision.icon} `), selector.includes(`${provision.icon} `)
), )
);
if (!styleSheet) {
console.warn(
`No style sheet found for provision ${provision.icon}; skipping...`
); );
continue;
}
if (!styleSheet) { const configPath = styleSheet.properties["bg-sprite"]
console.warn( .replace("~res:/", "")
`No style sheet found for provision ${provision.icon}; skipping...`, .replace(".psd", "")
); .replace(".txt", "");
return;
}
const configPath = styleSheet.properties['bg-sprite'] if (vfs.has(`Data/${configPath}.packed.webp`)) {
.replace('~res:/', '') const image = sharp(
.replace('.psd', '') await vfs.file(`Data/${configPath}.packed.webp`)
.replace('.txt', ''); );
const content = await image.trim({ threshold: 100 }).toBuffer();
if (existsSync(`${DATA}/${configPath}.packed.webp`)) { await uploader.add({
const image = sharp( path: `icons/provisions/${provision.id}.webp`,
await readDVPLFile(`${DATA}/${configPath}.packed.webp`), content,
); });
const content = await image.trim({ threshold: 100 }).toBuffer(); } else {
const provisionsTexture = sharp(
await vfs.file(
`${`Data/${configPath}`
.split("/")
.slice(0, -1)
.join("/")}/texture0.packed.webp`
)
);
const sizes = (await vfs.text(`Data/${configPath}.txt`))
.split("\n")[4]
.split(" ")
.map(Number);
const content = await provisionsTexture
.clone()
.extract({
left: sizes[0],
top: sizes[1],
width: sizes[2],
height: sizes[3],
})
.toBuffer();
changes.push({ await uploader.add({
path: `icons/provisions/${provision.id}.webp`, path: `icons/provisions/${provision.id}.webp`,
content, content,
}); });
} else { }
const provisionsTexture = sharp( }
await readDVPLFile(
`${`${DATA}/${configPath}`.split('/').slice(0, -1).join('/')}/texture0.packed.webp`,
),
);
const sizes = (await readStringDVPL(`${DATA}/${configPath}.txt`))
.split('\n')[4]
.split(' ')
.map(Number);
const content = await provisionsTexture
.clone()
.extract({
left: sizes[0],
top: sizes[1],
width: sizes[2],
height: sizes[3],
})
.toBuffer();
changes.push({
path: `icons/provisions/${provision.id}.webp`,
content,
});
}
}),
);
} }
await commitAssets('consumable and provision icons', changes); await uploader.flush();
} }
+48 -52
View File
@@ -1,66 +1,62 @@
import { assertSecret, BlitzGlossary } from '@blitzkit/core'; import { assertSecret, BlitzGlossary } from "@blitzkit/core";
import sharp from 'sharp'; import sharp from "sharp";
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { AssetUploader } from "../core/github/assetUploader";
import { commitAssets } from '../core/github/commitAssets'; import { vfs } from "./constants";
import { FileChange } from '../core/github/commitMultipleFiles';
import { DATA } from './constants';
const ICONS = [ const ICONS = [
['currency_silver_m.packed.webp', 'silver'], ["currency_silver_m.packed.webp", "silver"],
['currency_premium_xl.packed.webp', 'premium'], ["currency_premium_xl.packed.webp", "premium"],
['currency_gold_m.packed.webp', 'gold'], ["currency_gold_m.packed.webp", "gold"],
['currency_free-xp_xl.packed.webp', 'free-xp'], ["currency_free-xp_xl.packed.webp", "free-xp"],
['currency_elite-xp_xl.packed.webp', 'elite-xp'], ["currency_elite-xp_xl.packed.webp", "elite-xp"],
['currency_crew-xp_xl.packed.webp', 'crew-xp'], ["currency_crew-xp_xl.packed.webp", "crew-xp"],
['currency_battle-xp_xl.packed.webp', 'xp'], ["currency_battle-xp_xl.packed.webp", "xp"],
]; ];
export async function currencies() { export async function currencies() {
console.log('Building currency icons...'); console.log("Building currency icons...");
const changes: FileChange[] = []; using uploader = new AssetUploader("currency icons");
await Promise.all( for (const [file, name] of ICONS) {
ICONS.map(async ([file, name]) => { const content = await sharp(
const content = await sharp( await vfs.file(`Data/Gfx/Lobby/currency/${file}`)
await readDVPLFile(`${DATA}/Gfx/Lobby/currency/${file}`), )
) .trim()
.trim() .toBuffer();
.toBuffer();
changes.push({ await uploader.add({
content, content,
path: `icons/currencies/${name}.webp`, path: `icons/currencies/${name}.webp`,
}); });
}), }
);
const glossary = await fetch( const glossary = await fetch(assertSecret(import.meta.env.WOTB_GLOSSARY))
assertSecret(import.meta.env.WOTB_GLOSSARY), .then((response) => response.json() as Promise<BlitzGlossary>)
).then((response) => response.json() as Promise<BlitzGlossary>); .then((glossary) =>
Object.values(glossary)
.entries()
.filter(([key]) => /^prx_season_\d+$/.test(`${key}`))
);
await Promise.all( for (const [key, item] of glossary) {
Object.entries(glossary) if (item.image_url === null) throw new Error(`No image_url for ${key}`);
.filter(([key]) => /^prx_season_\d+$/.test(key))
.map(async ([key, item]) => {
if (item.image_url === null) throw new Error(`No image_url for ${key}`);
const imageRaw = await fetch(item.image_url).then((response) => const imageRaw = await fetch(item.image_url).then((response) =>
response.arrayBuffer(), response.arrayBuffer()
); );
const content = await sharp(imageRaw) const content = await sharp(imageRaw)
.trim({ .trim({
threshold: 100, threshold: 100,
background: { r: 0, g: 0, b: 0, alpha: 0 }, background: { r: 0, g: 0, b: 0, alpha: 0 },
}) })
.toBuffer(); .toBuffer();
changes.push({ await uploader.add({
content, content,
path: `icons/currencies/${key}.webp`, path: `icons/currencies/${key}.webp`,
}); });
}), }
);
await commitAssets('currency icons', changes); await uploader.flush();
} }
+115 -118
View File
@@ -32,16 +32,12 @@ import {
} from "@blitzkit/core"; } from "@blitzkit/core";
import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n"; import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json"; import locales from "@blitzkit/i18n/locales.json";
import { readdir } from "fs/promises";
import { deburr } from "lodash-es"; import { deburr } from "lodash-es";
import { parse as parsePath } from "path"; import { parse as parsePath } from "path";
import type { Vector3Tuple } from "three"; import type { Vector3Tuple } from "three";
import { parse as parseYaml } from "yaml"; import { parse as parseYaml } from "yaml";
import { readStringDVPL } from "../core/blitz/readStringDVPL"; import { AssetUploader } from "../core/github/assetUploader";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL"; import { vfs } from "./constants";
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
import { commitAssets } from "../core/github/commitAssets";
import { DATA } from "./constants";
import type { Avatar } from "./skillIcons"; import type { Avatar } from "./skillIcons";
import type { TankParameters } from "./tankIcons"; import type { TankParameters } from "./tankIcons";
@@ -302,7 +298,7 @@ interface VehicleDefinitions {
type: "default"; type: "default";
enabled: false; enabled: false;
factorsModifiers: ""; factorsModifiers: "";
}, }
]; ];
}; };
}; };
@@ -538,10 +534,11 @@ const blitzModuleTypeToBlitzkit: Record<keyof BlitzModuleType, ModuleType> = {
export async function definitions() { export async function definitions() {
console.log("Building definitions..."); console.log("Building definitions...");
using uploader = new AssetUploader("definitions");
const gameDefinitions: GameDefinitions = { const gameDefinitions: GameDefinitions = {
version: (await readStringDVPL(`${DATA}/version.txt`)).split(" ")[0], version: (await vfs.text('Data/version.txt')).split(" ")[0],
nations: ( nations: (
await readYAMLDVPL<AvailableNationsYaml>(`${DATA}/available_nations.yaml`) await vfs.yaml<AvailableNationsYaml>('Data/available_nations.yaml')
).available_nations, ).available_nations,
gameModes: {}, gameModes: {},
roles: {}, roles: {},
@@ -564,12 +561,10 @@ export async function definitions() {
[TankClass.TANK_DESTROYER]: { skills: [] }, [TankClass.TANK_DESTROYER]: { skills: [] },
}, },
}; };
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then( const nations = vfs.dir('Data/XML/item_defs/vehicles').filter((nation) => nation !== "common")
(nations) => nations.filter((nation) => nation !== "common")
);
const tankStringIdMap: Record<string, number> = {}; const tankStringIdMap: Record<string, number> = {};
const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>( const optionalDevices = await vfs.xml<{ root: OptionalDevices }>(
`${DATA}/XML/item_defs/vehicles/common/optional_devices.xml` 'Data/XML/item_defs/vehicles/common/optional_devices.xml'
); );
const stringsI18n: Record<string, Record<string, string>> = {}; const stringsI18n: Record<string, Record<string, string>> = {};
@@ -581,8 +576,8 @@ export async function definitions() {
) )
.then((response) => response.text()) .then((response) => response.text())
.then((string) => parseYaml(string) as BlitzStrings); .then((string) => parseYaml(string) as BlitzStrings);
const preInstalled = await readYAMLDVPL<BlitzStrings>( const preInstalled = await vfs.yaml<BlitzStrings>(
`${DATA}/Strings/${blitzLocale}.yaml` `Data/Strings/${blitzLocale}.yaml`
); );
stringsI18n[locale] = { stringsI18n[locale] = {
@@ -615,15 +610,15 @@ export async function definitions() {
return { locales: collection } satisfies I18nString; return { locales: collection } satisfies I18nString;
} }
const optionalDeviceSlots = await readXMLDVPL<{ const optionalDeviceSlots = await vfs.xml<{
root: OptionalDeviceSlots; root: OptionalDeviceSlots;
}>(`${DATA}/XML/item_defs/vehicles/common/optional_device_slots.xml`); }>(`Data/XML/item_defs/vehicles/common/optional_device_slots.xml`);
const consumables: ConsumablesCommon = {}; const consumables: ConsumablesCommon = {};
const provisions: ProvisionsCommon = {}; const provisions: ProvisionsCommon = {};
for (const match of ( for (const match of (
await readStringDVPL( await vfs.text(
`${DATA}/XML/item_defs/vehicles/common/consumables/list.xml` `Data/XML/item_defs/vehicles/common/consumables/list.xml`
) )
).matchAll(/<items path="(.+)\.xml"\/>/g)) { ).matchAll(/<items path="(.+)\.xml"\/>/g)) {
if (match[1] === "prototypes") continue; if (match[1] === "prototypes") continue;
@@ -631,16 +626,16 @@ export async function definitions() {
Object.assign( Object.assign(
consumables, consumables,
( (
await readXMLDVPL<{ root: ConsumablesCommon }>( await vfs.xml<{ root: ConsumablesCommon }>(
`${DATA}/XML/item_defs/vehicles/common/consumables/${match[1]}.xml` `Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`
) )
).root ).root
); );
} }
for (const match of ( for (const match of (
await readStringDVPL( await vfs.text(
`${DATA}/XML/item_defs/vehicles/common/provisions/list.xml` `Data/XML/item_defs/vehicles/common/provisions/list.xml`
) )
).matchAll(/<items path="(.+)\.xml"\/>/g)) { ).matchAll(/<items path="(.+)\.xml"\/>/g)) {
if (match[1] === "prototypes") continue; if (match[1] === "prototypes") continue;
@@ -648,34 +643,34 @@ export async function definitions() {
Object.assign( Object.assign(
provisions, provisions,
( (
await readXMLDVPL<{ root: ConsumablesCommon }>( await vfs.xml<{ root: ConsumablesCommon }>(
`${DATA}/XML/item_defs/vehicles/common/provisions/${match[1]}.xml` `Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`
) )
).root ).root
); );
} }
const avatar = await readXMLDVPL<{ root: Avatar }>( const avatar = await vfs.xml<{ root: Avatar }>(
`${DATA}/XML/item_defs/tankmen/avatar.xml` `Data/XML/item_defs/tankmen/avatar.xml`
); );
const maps = await readYAMLDVPL<Maps>(`${DATA}/maps.yaml`); const maps = await vfs.yaml<Maps>(`Data/maps.yaml`);
const tankXps = new Map<number, ResearchCost>(); const tankXps = new Map<number, ResearchCost>();
const camouflagesXml = await readXMLDVPL<{ root: CamouflagesXML }>( const camouflagesXml = await vfs.xml<{ root: CamouflagesXML }>(
`${DATA}/XML/item_defs/vehicles/common/camouflages.xml` `Data/XML/item_defs/vehicles/common/camouflages.xml`
); );
const camouflagesYaml = await readYAMLDVPL<CamouflagesYaml>( const camouflagesYaml = await vfs.yaml<CamouflagesYaml>(
`${DATA}/camouflages.yaml` `Data/camouflages.yaml`
); );
const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages); const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages);
const squadBattleTypeStyles = await readYAMLDVPL<SquadBattleTypeStyles>( const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStyles>(
`${DATA}/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml` `Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`
); );
const gameTypeSelectorStyles = await readYAMLDVPL<SquadBattleTypeStyles>( const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStyles>(
`${DATA}/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml` `Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`
); );
const gameModeNativeNames: Record<string, number> = {}; const gameModeNativeNames: Record<string, number> = {};
const combatRoles = await readYAMLDVPL<CombatRolesYaml>( const combatRoles = await vfs.yaml<CombatRolesYaml>(
`${DATA}/XML/item_defs/vehicles/common/combat_roles.yaml` `Data/XML/item_defs/vehicles/common/combat_roles.yaml`
); );
const consumableNativeNames: Record<string, number> = {}; const consumableNativeNames: Record<string, number> = {};
const provisionNativeNames: Record<string, number> = {}; const provisionNativeNames: Record<string, number> = {};
@@ -726,8 +721,8 @@ export async function definitions() {
const idToNation: Record<number, string> = {}; const idToNation: Record<number, string> = {};
for (const nation of nations) { for (const nation of nations) {
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>( const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml` `Data/XML/item_defs/vehicles/${nation}/list.xml`
); );
for (const tankKey in tankList.root) { for (const tankKey in tankList.root) {
@@ -766,7 +761,9 @@ export async function definitions() {
} }
console.warn( console.warn(
`Multiple tanks share slug ${slug}: ${requesters.map(({ key }) => key).join(", ")}` `Multiple tanks share slug ${slug}: ${requesters
.map(({ key }) => key)
.join(", ")}`
); );
if (requesters.length !== 2) { if (requesters.length !== 2) {
@@ -808,24 +805,24 @@ export async function definitions() {
}); });
for (const nation of nations) { for (const nation of nations) {
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>( const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml` `Data/XML/item_defs/vehicles/${nation}/list.xml`
); );
const turretList = await readXMLDVPL<{ const turretList = await vfs.xml<{
root: TurretDefinitionsList; root: TurretDefinitionsList;
}>(`${DATA}/XML/item_defs/vehicles/${nation}/components/turrets.xml`); }>(`Data/XML/item_defs/vehicles/${nation}/components/turrets.xml`);
const gunList = await readXMLDVPL<{ const gunList = await vfs.xml<{
root: GunDefinitionsList; root: GunDefinitionsList;
}>(`${DATA}/XML/item_defs/vehicles/${nation}/components/guns.xml`); }>(`Data/XML/item_defs/vehicles/${nation}/components/guns.xml`);
const shellList = await readXMLDVPL<{ const shellList = await vfs.xml<{
root: ShellDefinitionsList; root: ShellDefinitionsList;
}>(`${DATA}/XML/item_defs/vehicles/${nation}/components/shells.xml`); }>(`Data/XML/item_defs/vehicles/${nation}/components/shells.xml`);
const enginesList = await readXMLDVPL<{ const enginesList = await vfs.xml<{
root: EngineDefinitionsList; root: EngineDefinitionsList;
}>(`${DATA}/XML/item_defs/vehicles/${nation}/components/engines.xml`); }>(`Data/XML/item_defs/vehicles/${nation}/components/engines.xml`);
const chassisList = await readXMLDVPL<{ const chassisList = await vfs.xml<{
root: ChassisDefinitionsList; root: ChassisDefinitionsList;
}>(`${DATA}/XML/item_defs/vehicles/${nation}/components/chassis.xml`); }>(`Data/XML/item_defs/vehicles/${nation}/components/chassis.xml`);
function resolveUnlocks(unlocks?: BlitzModuleType) { function resolveUnlocks(unlocks?: BlitzModuleType) {
if (!unlocks) return []; if (!unlocks) return [];
@@ -876,11 +873,11 @@ export async function definitions() {
const trackXps = new Map<number, ResearchCost>(); const trackXps = new Map<number, ResearchCost>();
const tank = tankList.root[tankKey]; const tank = tankList.root[tankKey];
let tankPrice: TankPrice; let tankPrice: TankPrice;
const tankDefinition = await readXMLDVPL<{ root: VehicleDefinitions }>( const tankDefinition = await vfs.xml<{ root: VehicleDefinitions }>(
`${DATA}/XML/item_defs/vehicles/${nation}/${tankKey}.xml` `Data/XML/item_defs/vehicles/${nation}/${tankKey}.xml`
); );
const tankParameters = await readYAMLDVPL<TankParameters>( const tankParameters = await vfs.yaml<TankParameters>(
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml` `Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
); );
const turretOrigin = tankDefinition.root.hull.turretPositions.turret const turretOrigin = tankDefinition.root.hull.turretPositions.turret
.split(" ") .split(" ")
@@ -1018,8 +1015,8 @@ export async function definitions() {
type: tankTags.includes("collectible") type: tankTags.includes("collectible")
? TankType.COLLECTOR ? TankType.COLLECTOR
: (typeof tank.price === "number" ? false : "gold" in tank.price) : (typeof tank.price === "number" ? false : "gold" in tank.price)
? TankType.PREMIUM ? TankType.PREMIUM
: TankType.RESEARCHABLE, : TankType.RESEARCHABLE,
tier: tank.level, tier: tank.level,
class: blitzTankClassToBlitzkit[tankTags[0] as BlitzTankClass], class: blitzTankClassToBlitzkit[tankTags[0] as BlitzTankClass],
testing: tankTags.includes("testTank"), testing: tankTags.includes("testTank"),
@@ -1337,24 +1334,24 @@ export async function definitions() {
}, },
} }
: gunType === "autoReloader" : gunType === "autoReloader"
? { ? {
$case: "auto_reloader", $case: "auto_reloader",
value: { value: {
intra_clip: 60 / gun.clip!.rate, intra_clip: 60 / gun.clip!.rate,
shell_count: gunClipCount, shell_count: gunClipCount,
shell_reloads: gun shell_reloads: gun
.pumpGunReloadTimes!.split(" ") .pumpGunReloadTimes!.split(" ")
.map(Number), .map(Number),
},
}
: {
$case: "auto_loader",
value: {
intra_clip: 60 / gun.clip!.rate,
clip_reload: gun.reloadTime,
shell_count: gunClipCount,
},
}, },
}
: {
$case: "auto_loader",
value: {
intra_clip: 60 / gun.clip!.rate,
clip_reload: gun.reloadTime,
shell_count: gunClipCount,
},
},
} satisfies GunDefinition); } satisfies GunDefinition);
modelDefinitions.models[tankId].turrets[turretId].guns[gunId] = { modelDefinitions.models[tankId].turrets[turretId].guns[gunId] = {
@@ -1365,8 +1362,8 @@ export async function definitions() {
gun.armor.gun === undefined gun.armor.gun === undefined
? 0 ? 0
: typeof gun.armor.gun === "number" : typeof gun.armor.gun === "number"
? gun.armor.gun ? gun.armor.gun
: gun.armor.gun["#text"], : gun.armor.gun["#text"],
pitch: { pitch: {
min: gunPitch[0], min: gunPitch[0],
max: gunPitch[1], max: gunPitch[1],
@@ -1413,7 +1410,7 @@ export async function definitions() {
type: blitzShellKindToBlitzkit[shell.kind], type: blitzShellKindToBlitzkit[shell.kind],
explosion_radius: explosion_radius:
shell.kind === "HIGH_EXPLOSIVE" shell.kind === "HIGH_EXPLOSIVE"
? (shell.explosionRadius ?? 0) ? shell.explosionRadius ?? 0
: undefined, : undefined,
icon: shell.icon, icon: shell.icon,
penetration: { penetration: {
@@ -1783,42 +1780,42 @@ export async function definitions() {
}; };
}); });
await commitAssets("definitions", [ await uploader.add({
{ content: GameDefinitions.encode(gameDefinitions).finish(),
content: GameDefinitions.encode(gameDefinitions).finish(), path: "definitions/game.pb",
path: "definitions/game.pb", });
}, await uploader.add({
{ content: TankDefinitions.encode(tankDefinitions).finish(),
content: TankDefinitions.encode(tankDefinitions).finish(), path: "definitions/tanks.pb",
path: "definitions/tanks.pb", });
}, await uploader.add({
{ content: ModelDefinitions.encode(modelDefinitions).finish(),
content: ModelDefinitions.encode(modelDefinitions).finish(), path: "definitions/models.pb",
path: "definitions/models.pb", });
}, await uploader.add({
{ content: EquipmentDefinitions.encode(equipmentDefinitions).finish(),
content: EquipmentDefinitions.encode(equipmentDefinitions).finish(), path: "definitions/equipment.pb",
path: "definitions/equipment.pb", });
}, await uploader.add({
{ content: ConsumableDefinitions.encode(consumableDefinitions).finish(),
content: ConsumableDefinitions.encode(consumableDefinitions).finish(), path: "definitions/consumables.pb",
path: "definitions/consumables.pb", });
}, await uploader.add({
{ content: ProvisionDefinitions.encode(provisionDefinitions).finish(),
content: ProvisionDefinitions.encode(provisionDefinitions).finish(), path: "definitions/provisions.pb",
path: "definitions/provisions.pb", });
}, await uploader.add({
{ content: SkillDefinitions.encode(skillDefinitions).finish(),
content: SkillDefinitions.encode(skillDefinitions).finish(), path: "definitions/skills.pb",
path: "definitions/skills.pb", });
}, await uploader.add({
{ content: MapDefinitions.encode(mapDefinitions).finish(),
content: MapDefinitions.encode(mapDefinitions).finish(), path: "definitions/maps.pb",
path: "definitions/maps.pb", });
}, await uploader.add({
{ content: CamouflageDefinitions.encode(camouflageDefinitions).finish(),
content: CamouflageDefinitions.encode(camouflageDefinitions).finish(), path: "definitions/camouflages.pb",
path: "definitions/camouflages.pb", });
},
]); await uploader.flush();
} }
@@ -1,91 +1,82 @@
import sharp from 'sharp'; import sharp from "sharp";
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { AssetUploader } from "../core/github/assetUploader";
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL'; import { vfs } from "./constants";
import { readXMLDVPL } from '../core/blitz/readXMLDVPL'; import { OptionalDevices } from "./definitions";
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
import { commitAssets } from '../core/github/commitAssets';
import { FileChange } from '../core/github/commitMultipleFiles';
import { DATA } from './constants';
import { OptionalDevices } from './definitions';
interface Mappings { interface Mappings {
Header: { version: number }; Header: { version: number };
StyleSheets: { StyleSheets: {
selectors: string[]; selectors: string[];
properties: { 'bg-sprite': string }; properties: { "bg-sprite": string };
}[]; }[];
} }
export async function equipmentIcons() { export async function equipmentIcons() {
console.log('Building equipment icons...'); console.log("Building equipment icons...");
const changes: FileChange[] = []; using uploader = new AssetUploader("equipment icons");
const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>( const optionalDevices = await vfs.xml<{ root: OptionalDevices }>(
`${DATA}/XML/item_defs/vehicles/common/optional_devices.xml`, `Data/XML/item_defs/vehicles/common/optional_devices.xml`
); );
const mappings = await readYAMLDVPL<Mappings>( const mappings = await vfs.yaml<Mappings>(
`${DATA}/UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml`, `Data/UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml`
); );
const image = sharp( const image = sharp(
await readDVPLFile( await vfs.file(
`${DATA}/Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp`, `Data/Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp`
), )
); );
await Promise.all( for (const optionalDeviceKey in optionalDevices.root) {
Object.entries(optionalDevices.root).map( const optionalDevice = optionalDevices.root[optionalDeviceKey];
async ([optionalDeviceKey, optionalDevice]) => {
if (optionalDeviceKey === 'nextAvailableId') return;
const mapping = mappings.StyleSheets.find((mapping) => if (optionalDeviceKey === "nextAvailableId") continue;
mapping.selectors.includes(
`.optional_device_item.${optionalDevice.icon} #Img`,
),
);
if (!mapping) { const mapping = mappings.StyleSheets.find((mapping) =>
console.warn( mapping.selectors.includes(
`No mapping found for ${optionalDevice.icon}; skipping...`, `.optional_device_item.${optionalDevice.icon} #Img`
); )
return; );
}
const configPathRaw = mapping.properties['bg-sprite']; if (!mapping) {
const configPath = configPathRaw.replace('~res:/', ''); console.warn(`No mapping found for ${optionalDevice.icon}; skipping...`);
continue;
}
if (configPath.startsWith('Gfx/Lobby')) { const configPathRaw = mapping.properties["bg-sprite"];
const configPathWebp = configPath.replace('.txt', ''); const configPath = configPathRaw.replace("~res:/", "");
const image = sharp(
await readDVPLFile(`${DATA}/${configPathWebp}.packed.webp`),
);
const content = await image.trim().toBuffer();
changes.push({ if (configPath.startsWith("Gfx/Lobby")) {
path: `icons/equipment/${optionalDevice.id}.webp`, const configPathWebp = configPath.replace(".txt", "");
content, const image = sharp(
}); await vfs.file(`Data/${configPathWebp}.packed.webp`)
} else { );
const config = await readStringDVPL(`${DATA}/${configPath}`); const content = await image.trim().toBuffer();
const sizes = config.split('\n')[4].split(' ').map(Number);
const content = await image
.clone()
.extract({
left: sizes[0],
top: sizes[1],
width: sizes[2],
height: sizes[3],
})
.toBuffer();
changes.push({ await uploader.add({
path: `icons/equipment/${optionalDevice.id}.webp`, path: `icons/equipment/${optionalDevice.id}.webp`,
content, content,
}); });
} } else {
}, const config = await vfs.text(`Data/${configPath}`);
), const sizes = config.split("\n")[4].split(" ").map(Number);
); const content = await image
.clone()
.extract({
left: sizes[0],
top: sizes[1],
width: sizes[2],
height: sizes[3],
})
.toBuffer();
await commitAssets('equipment icons', changes); await uploader.add({
path: `icons/equipment/${optionalDevice.id}.webp`,
content,
});
}
}
await uploader.flush();
} }
+55 -67
View File
@@ -1,75 +1,63 @@
import { readdir } from 'fs/promises'; import sharp from "sharp";
import sharp from 'sharp'; import { AssetUploader } from "../core/github/assetUploader";
import { readDVPLFile } from '../core/blitz/readDVPLFile'; import { vfs } from "./constants";
import { commitAssets } from '../core/github/commitAssets';
import { FileChange } from '../core/github/commitMultipleFiles';
import { DATA } from './constants';
export async function flags() { export async function flags() {
console.log('Building flags...'); console.log("Building flags...");
const changes = [ using uploader = new AssetUploader("flags");
...(await Promise.all( const circleFiles = vfs.dir(`Data/Gfx/Lobby/flags`).filter(
(await readdir(`${DATA}/Gfx/Lobby/flags`)) (flag) =>
.filter( flag.startsWith("flag_profile-stat_") &&
(flag) => !flag.endsWith("@2x.packed.webp")
flag.startsWith('flag_profile-stat_') && );
!flag.endsWith('@2x.packed.webp'), const scratchedFiles = vfs.dir(`Data/Gfx/Lobby/flags`).filter(
) (flag) =>
.map(async (flag) => { flag.startsWith("flag_tutor-tank_") &&
const image = sharp( !flag.endsWith("@2x.packed.webp")
await readDVPLFile(`${DATA}/Gfx/Lobby/flags/${flag}`), )
); const fadedFiles = vfs.dir(`Data/Gfx/Lobby/flags`).filter(
const content = await image.trim({ threshold: 100 }).toBuffer(); (flag) =>
const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1]; flag.startsWith("flag_filter_") && flag.endsWith("@2x.packed.webp")
)
return { for (const flag of circleFiles) {
content, const image = sharp(await vfs.file(`Data/Gfx/Lobby/flags/${flag}`));
path: `flags/circle/${name}.webp`, const content = await image.trim({ threshold: 100 }).toBuffer();
} satisfies FileChange; const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1];
}),
)),
...(await Promise.all(
(await readdir(`${DATA}/Gfx/Lobby/flags`))
.filter(
(flag) =>
flag.startsWith('flag_tutor-tank_') &&
!flag.endsWith('@2x.packed.webp'),
)
.map(async (flag) => {
const content = await readDVPLFile(`${DATA}/Gfx/Lobby/flags/${flag}`);
const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1];
return { uploader.add({
path: `flags/scratched/${name}.webp`, content,
content, path: `flags/circle/${name}.webp`,
} satisfies FileChange; });
}), }
)),
...(await Promise.all(
(await readdir(`${DATA}/Gfx/Lobby/flags`))
.filter(
(flag) =>
flag.startsWith('flag_filter_') && flag.endsWith('@2x.packed.webp'),
)
.map(async (flag) => {
const content = await sharp(
await readDVPLFile(`${DATA}/Gfx/Lobby/flags/${flag}`),
)
.trim({
threshold: 100,
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.toBuffer();
const name = flag.match(/flag_filter_(.+)@2x\.packed\.webp/)![1];
return { for (const flag of scratchedFiles) {
content, const content = await vfs.file(`Data/Gfx/Lobby/flags/${flag}`);
path: `flags/fade_small/${name}.webp`, const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1];
} satisfies FileChange;
}),
)),
];
await commitAssets('flags', changes); uploader.add({
path: `flags/scratched/${name}.webp`,
content,
});
}
for (const flag of fadedFiles) {
const content = await sharp(
await vfs.file(`Data/Gfx/Lobby/flags/${flag}`)
)
.trim({
threshold: 100,
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.toBuffer();
const name = flag.match(/flag_filter_(.+)@2x\.packed\.webp/)![1];
uploader.add({
content,
path: `flags/fade_small/${name}.webp`,
});
}
await uploader.flush();
} }
@@ -1,36 +1,33 @@
import sharp from 'sharp'; import sharp from "sharp";
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { AssetUploader } from "../core/github/assetUploader";
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL'; import { vfs } from "./constants";
import { commitAssets } from '../core/github/commitAssets'; import { SquadBattleTypeStyles } from "./definitions";
import { FileChange } from '../core/github/commitMultipleFiles';
import { DATA } from './constants';
import { SquadBattleTypeStyles } from './definitions';
export async function gameModeBanners() { export async function gameModeBanners() {
console.log('Building game mode banners...'); console.log("Building game mode banners...");
const changes: FileChange[] = []; using uploader = new AssetUploader("game mode banners");
const gameTypeSelectorStyles = await readYAMLDVPL<SquadBattleTypeStyles>( const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStyles>(
`${DATA}/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`, `Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`
); );
const squadBattleTypeStyles = await readYAMLDVPL<SquadBattleTypeStyles>( const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStyles>(
`${DATA}/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`, `Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`
); );
const bannerMatches: { name: string; path: string }[] = []; const bannerMatches: { name: string; path: string }[] = [];
for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/eGameMode\.([a-zA-Z]+) -> "~res:([^"]+)"/g, /eGameMode\.([a-zA-Z]+) -> "~res:([^"]+)"/g
)) { )) {
bannerMatches.push({ name: match[1], path: match[2] }); bannerMatches.push({ name: match[1], path: match[2] });
} }
for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/"(\d+)" -> "battleType\/([a-zA-Z]+)"/g, /"(\d+)" -> "battleType\/([a-zA-Z]+)"/g
)) { )) {
const id = Number(match[1]); const id = Number(match[1]);
const name = match[2]; const name = match[2];
let path = bannerMatches.find( let path = bannerMatches.find(
(bannerMatch) => bannerMatch.name.toLowerCase() === name.toLowerCase(), (bannerMatch) => bannerMatch.name.toLowerCase() === name.toLowerCase()
)?.path; )?.path;
if (path === undefined) { if (path === undefined) {
@@ -38,16 +35,16 @@ export async function gameModeBanners() {
} }
const content = await sharp( const content = await sharp(
await readDVPLFile(`${DATA}${path}.packed.webp`), await vfs.file(`Data${path}.packed.webp`)
) )
.trim({ background: { r: 0, g: 0, b: 0, alpha: 0 } }) .trim({ background: { r: 0, g: 0, b: 0, alpha: 0 } })
.toBuffer(); .toBuffer();
changes.push({ await uploader.add({
path: `icons/game_mode_banners/${id}.webp`, path: `icons/game_mode_banners/${id}.webp`,
content, content,
}); });
} }
await commitAssets('game mode banners', changes); await uploader.flush();
} }
+17 -18
View File
@@ -1,13 +1,13 @@
import { Avatar, fetchGlossary, Gallery } from '@blitzkit/core'; import { Avatar, fetchGlossary, Gallery } from "@blitzkit/core";
import locales from '@blitzkit/i18n/locales.json'; import locales from "@blitzkit/i18n/locales.json";
import { extname } from 'path'; import { extname } from "path";
import ProgressBar from 'progress'; import ProgressBar from "progress";
import { commitAssets } from '../core/github/commitAssets'; import { AssetUploader } from "../core/github/assetUploader";
import { FileChange } from '../core/github/commitMultipleFiles';
export async function glossary() { export async function glossary() {
console.log('Building glossary...'); console.log("Building glossary...");
using uploader = new AssetUploader("glossary");
const avatars: Record<string, { avatar: Avatar; url: string }> = {}; const avatars: Record<string, { avatar: Avatar; url: string }> = {};
await Promise.all( await Promise.all(
@@ -15,15 +15,15 @@ export async function glossary() {
const glossary = await fetchGlossary(supported.blitz ?? supported.locale); const glossary = await fetchGlossary(supported.blitz ?? supported.locale);
console.log( console.log(
`Found ${Object.keys(glossary).length} things for ${supported.locale}`, `Found ${Object.keys(glossary).length} things for ${supported.locale}`
); );
for (const key in glossary) { for (const key in glossary) {
const glossaryEntry = glossary[key]; const glossaryEntry = glossary[key];
if ( if (
!key.startsWith('avatar') || !key.startsWith("avatar") ||
key.endsWith('_part') || key.endsWith("_part") ||
glossaryEntry.image_url === null glossaryEntry.image_url === null
) { ) {
continue; continue;
@@ -45,16 +45,15 @@ export async function glossary() {
}; };
} }
} }
}), })
); );
const changes: FileChange[] = [];
const gallery: Gallery = { avatars: [] }; const gallery: Gallery = { avatars: [] };
const totalCount = Object.keys(avatars).length; const totalCount = Object.keys(avatars).length;
const bar = new ProgressBar( const bar = new ProgressBar(
`Fetching avatars of ${totalCount} things :bar`, `Fetching avatars of ${totalCount} things :bar`,
totalCount, totalCount
); );
await Promise.all( await Promise.all(
@@ -72,19 +71,19 @@ export async function glossary() {
.then((response) => response.arrayBuffer()) .then((response) => response.arrayBuffer())
.then((buffer) => new Uint8Array(buffer)); .then((buffer) => new Uint8Array(buffer));
changes.push({ await uploader.add({
path: `gallery/avatars/${key}${avatar.extension}`, path: `gallery/avatars/${key}${avatar.extension}`,
content, content,
}); });
bar.tick(); bar.tick();
}), })
); );
changes.push({ await uploader.add({
path: 'definitions/gallery.pb', path: "definitions/gallery.pb",
content: Gallery.encode(gallery).finish(), content: Gallery.encode(gallery).finish(),
}); });
await commitAssets('glossary', changes); await uploader.flush();
} }
+16 -13
View File
@@ -1,16 +1,8 @@
import { config } from "dotenv"; import { config } from "dotenv";
import { argv } from "process"; import { argv } from "process";
config();
const allTargets = argv.includes("--all-targets");
const targets = argv
.find((argument) => argument.startsWith("--target"))
?.split("=")[1]
.split(",");
import { boosterIcons } from "./boosterIcons"; import { boosterIcons } from "./boosterIcons";
import { camouflageIcons } from "./camouflageIcons"; import { camouflageIcons } from "./camouflageIcons";
import { vfs } from "./constants";
import { consumableProvisionIcons } from "./consumableProvisionIcons"; import { consumableProvisionIcons } from "./consumableProvisionIcons";
import { currencies } from "./currencies"; import { currencies } from "./currencies";
import { definitions } from "./definitions"; import { definitions } from "./definitions";
@@ -20,17 +12,26 @@ import { gameModeBanners } from "./gameModeBanners";
import { glossary } from "./glossary"; import { glossary } from "./glossary";
import { migration } from "./migration"; import { migration } from "./migration";
import { moduleIcons } from "./moduleIcons"; import { moduleIcons } from "./moduleIcons";
import { popularTanks } from "./popularTanks";
import { shellIcons } from "./shellIcons"; import { shellIcons } from "./shellIcons";
import { skillIcons } from "./skillIcons"; import { skillIcons } from "./skillIcons";
import { tankArmors } from "./tankArmors"; import { tankArmors } from "./tankArmors";
import { tankIcons } from "./tankIcons"; import { tankIcons } from "./tankIcons";
import { tankModels } from "./tankModels"; import { tankModels } from "./tankModels";
config();
const allTargets = argv.includes("--all-targets");
const targets = argv
.find((argument) => argument.startsWith("--target"))
?.split("=")[1]
.split(",");
if (!targets && !allTargets) throw new Error("No target(s) specified"); if (!targets && !allTargets) throw new Error("No target(s) specified");
const methods = [ const methods = [
migration,
definitions, definitions,
tankIcons,
tankModels, tankModels,
tankArmors, tankArmors,
gameModeBanners, gameModeBanners,
@@ -43,10 +44,9 @@ const methods = [
consumableProvisionIcons, consumableProvisionIcons,
camouflageIcons, camouflageIcons,
boosterIcons, boosterIcons,
tankIcons,
// blitzkitTankIcons,
// popularTanks,
glossary, glossary,
migration,
popularTanks,
]; ];
for (const method of methods) { for (const method of methods) {
@@ -59,3 +59,6 @@ for (const method of methods) {
} }
} }
} }
vfs.dispose();
process.exit(0);
+23 -29
View File
@@ -1,38 +1,32 @@
import { toUniqueId } from '@blitzkit/core'; import { toUniqueId } from "@blitzkit/core";
import { readdir } from 'fs/promises'; import { AssetUploader } from "../core/github/assetUploader";
import { readXMLDVPL } from '../core/blitz/readXMLDVPL'; import { vfs } from "./constants";
import { commitAssets } from '../core/github/commitAssets'; import { VehicleDefinitionList } from "./definitions";
import { DATA } from './constants';
import { VehicleDefinitionList } from './definitions';
export async function migration() { export async function migration() {
console.log('Building migration map...'); console.log("Building migration map...");
using uploader = new AssetUploader("migration map");
const map: Record<string, number> = {}; const map: Record<string, number> = {};
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then( const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common");
(nations) => nations.filter((nation) => nation !== 'common'),
);
await Promise.all( for (const nation of nations) {
nations.map(async (nation) => { const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>( `Data/XML/item_defs/vehicles/${nation}/list.xml`
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`, );
);
for (const tankKey in tankList.root) { for (const tankKey in tankList.root) {
const tank = tankList.root[tankKey]; const tank = tankList.root[tankKey];
const tankId = toUniqueId(nation, tank.id); const tankId = toUniqueId(nation, tank.id);
const davaKey = `${nation}:${tankKey}`; const davaKey = `${nation}:${tankKey}`;
map[davaKey] = tankId; map[davaKey] = tankId;
} }
}), }
);
await commitAssets('migration', [ await uploader.add({
{ path: "definitions/migration.json",
path: 'definitions/migration.json', content: new TextEncoder().encode(JSON.stringify(map)),
content: new TextEncoder().encode(JSON.stringify(map)), });
}, await uploader.flush();
]);
} }
+23 -28
View File
@@ -1,34 +1,29 @@
import { readdir } from 'fs/promises'; import sharp from "sharp";
import sharp from 'sharp'; import { AssetUploader } from "../core/github/assetUploader";
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { vfs } from "./constants";
import { commitAssets } from '../core/github/commitAssets';
import { FileChange } from '../core/github/commitMultipleFiles';
import { DATA } from './constants';
export async function moduleIcons() { export async function moduleIcons() {
console.log('Building module icons...'); console.log("Building module icons...");
const changes = await Promise.all( using uploader = new AssetUploader("module icons");
(await readdir(`${DATA}/Gfx/UI/ModulesTechTree`)) const files = vfs.dir(`Data/Gfx/UI/ModulesTechTree`).filter(
.filter( (file) => !file.endsWith("@2x.packed.webp") && file.startsWith("vehicle")
(file) => )
!file.endsWith('@2x.packed.webp') && file.startsWith('vehicle'),
)
.map(async (file) => {
const nameRaw = file.match(/vehicle(.+)\.packed\.webp/)![1];
const name = nameRaw[0].toLowerCase() + nameRaw.slice(1);
const content = await sharp(
await readDVPLFile(`${DATA}/Gfx/UI/ModulesTechTree/${file}`),
)
.trim()
.toBuffer();
return { for (const file of files) {
content, const nameRaw = file.match(/vehicle(.+)\.packed\.webp/)![1];
path: `icons/modules/${name}.webp`, const name = nameRaw[0].toLowerCase() + nameRaw.slice(1);
} satisfies FileChange; const content = await sharp(
}), await vfs.file(`Data/Gfx/UI/ModulesTechTree/${file}`)
); )
.trim()
.toBuffer();
await commitAssets('module icons', changes); await uploader.add({
content,
path: `icons/modules/${name}.webp`,
});
}
await uploader.flush();
} }
@@ -2,31 +2,34 @@ import {
assertSecret, assertSecret,
fetchTankDefinitions, fetchTankDefinitions,
PopularTanks, PopularTanks,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { google } from 'googleapis'; import { google } from "googleapis";
import { commitAssets } from '../core/github/commitAssets'; import { AssetUploader } from "../core/github/assetUploader";
export async function popularTanks() { export async function popularTanks() {
using uploader = new AssetUploader("popular tanks");
const tankDefinitions = await fetchTankDefinitions(); const tankDefinitions = await fetchTankDefinitions();
const auth = await google.auth.getClient({ const auth = await google.auth.getClient({
keyFile: import.meta.env.GOOGLE_APPLICATION_CREDENTIALS, keyFile: import.meta.env.GOOGLE_APPLICATION_CREDENTIALS,
scopes: ['https://www.googleapis.com/auth/analytics.readonly'], scopes: ["https://www.googleapis.com/auth/analytics.readonly"],
}); });
const analytics = google.analyticsdata({ version: 'v1beta', auth }); const analytics = google.analyticsdata({ version: "v1beta", auth });
const report = await analytics.properties.runReport({ const report = await analytics.properties.runReport({
property: `properties/${assertSecret(import.meta.env.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID)}`, property: `properties/${assertSecret(
import.meta.env.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
)}`,
requestBody: { requestBody: {
dateRanges: [{ startDate: '7daysAgo', endDate: 'today' }], dateRanges: [{ startDate: "7daysAgo", endDate: "today" }],
dimensions: [{ name: 'pagePath' }], dimensions: [{ name: "pagePath" }],
metrics: [{ name: 'screenPageViews' }], metrics: [{ name: "screenPageViews" }],
orderBys: [{ metric: { metricName: 'screenPageViews' }, desc: true }], orderBys: [{ metric: { metricName: "screenPageViews" }, desc: true }],
dimensionFilter: { dimensionFilter: {
filter: { filter: {
fieldName: 'pagePath', fieldName: "pagePath",
stringFilter: { stringFilter: {
matchType: 'BEGINS_WITH', matchType: "BEGINS_WITH",
value: '/tanks/', value: "/tanks/",
}, },
}, },
}, },
@@ -34,7 +37,7 @@ export async function popularTanks() {
}); });
if (!report.data.rows) { if (!report.data.rows) {
throw new Error('No rows in report'); throw new Error("No rows in report");
} }
const tanks = report.data.rows const tanks = report.data.rows
@@ -42,21 +45,21 @@ export async function popularTanks() {
(row) => (row) =>
row.dimensionValues && row.dimensionValues &&
row.dimensionValues[0].value && row.dimensionValues[0].value &&
row.dimensionValues[0].value !== '/tanks/' && row.dimensionValues[0].value !== "/tanks/" &&
row.metricValues && row.metricValues &&
row.metricValues[0].value, row.metricValues[0].value
) )
.map((row) => ({ .map((row) => ({
id: Number( id: Number(
row.dimensionValues![0].value!.match( row.dimensionValues![0].value!.match(
/\/tools\/tankopedia\/(\d+)\/?/, /\/tools\/tankopedia\/(\d+)\/?/
)?.[1], )?.[1]
), ),
views: Number(row.metricValues![0].value!), views: Number(row.metricValues![0].value!),
})) }))
.filter((row) => { .filter((row) => {
if (!(row.id in tankDefinitions.tanks)) { if (!(row.id in tankDefinitions.tanks)) {
console.warn('Skipping unknown popular tank', row.id); console.warn("Skipping unknown popular tank", row.id);
return false; return false;
} }
@@ -66,10 +69,9 @@ export async function popularTanks() {
.map(({ id, views }) => ({ id, views })); .map(({ id, views }) => ({ id, views }));
const popularTanks: PopularTanks = { tanks }; const popularTanks: PopularTanks = { tanks };
await commitAssets('popular tanks', [ await uploader.add({
{ path: "definitions/popular-tanks.pb",
path: 'definitions/popular-tanks.pb', content: PopularTanks.encode(popularTanks).finish(),
content: PopularTanks.encode(popularTanks).finish(), });
}, await uploader.flush();
]);
} }
+35 -39
View File
@@ -1,48 +1,44 @@
import { readdir } from 'fs/promises'; import sharp from "sharp";
import sharp from 'sharp'; import { AssetUploader } from "../core/github/assetUploader";
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { vfs } from "./constants";
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
import { commitAssets } from '../core/github/commitAssets';
import { FileChange } from '../core/github/commitMultipleFiles';
import { DATA } from './constants';
export async function shellIcons() { export async function shellIcons() {
console.log('Building shell icons...'); console.log("Building shell icons...");
using uploader = new AssetUploader("shell icons");
const image = sharp( const image = sharp(
await readDVPLFile( await vfs.file(
`${DATA}/Gfx/Shared/tank-supply/ammunition/big/texture0.packed.webp`, `Data/Gfx/Shared/tank-supply/ammunition/big/texture0.packed.webp`
), )
); );
const files = vfs.dir(
`Data/Gfx/Shared/tank-supply/ammunition/big`
).filter((file) => file.endsWith("_l.txt"))
const changes = await Promise.all( for (const file of files) {
(await readdir(`${DATA}/Gfx/Shared/tank-supply/ammunition/big`)) const name = file.match(/(.+)_l\.txt/)![1];
.filter((file) => file.endsWith('_l.txt')) const sizes = (
.map(async (file) => { await vfs.text(
const name = file.match(/(.+)_l\.txt/)![1]; `Data/Gfx/Shared/tank-supply/ammunition/big/${file}`
const sizes = ( )
await readStringDVPL( )
`${DATA}/Gfx/Shared/tank-supply/ammunition/big/${file}`, .split("\n")[4]
) .split(" ")
) .map(Number);
.split('\n')[4]
.split(' ')
.map(Number);
return { await uploader.add({
content: await image content: await image
.clone() .clone()
.extract({ .extract({
left: sizes[0], left: sizes[0],
top: sizes[1], top: sizes[1],
width: sizes[2], width: sizes[2],
height: sizes[3], height: sizes[3],
}) })
.toBuffer(), .toBuffer(),
path: `icons/shells/${name}.webp`, path: `icons/shells/${name}.webp`,
} satisfies FileChange; });
}), }
);
await commitAssets('shell icons', changes); await uploader.flush();
} }
+16 -21
View File
@@ -1,10 +1,7 @@
import { TankClass } from "@blitzkit/core"; import { TankClass } from "@blitzkit/core";
import sharp from "sharp"; import sharp from "sharp";
import { readDVPLFile } from "../core/blitz/readDVPLFile"; import { AssetUploader } from "../core/github/assetUploader";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL"; import { vfs } from "./constants";
import { commitAssets } from "../core/github/commitAssets";
import type { FileChange } from "../core/github/commitMultipleFiles";
import { DATA } from "./constants";
interface SkillIcon { interface SkillIcon {
name: string; name: string;
@@ -28,23 +25,21 @@ export interface Avatar {
export async function skillIcons() { export async function skillIcons() {
console.log("Building skill icons..."); console.log("Building skill icons...");
const avatar = await readXMLDVPL<{ root: Avatar }>( const avatar = await vfs.xml<{ root: Avatar }>(
`${DATA}/XML/item_defs/tankmen/avatar.xml` `Data/XML/item_defs/tankmen/avatar.xml`
); );
const changes = await Promise.all( using uploader = new AssetUploader("skill icons");
Object.values(avatar.root.skills).map(async (skill) => {
const icon = Array.isArray(skill.icon) ? skill.icon[0] : skill.icon;
const name = icon.name.split("/").at(-1)!.replace(/_\d$/, "");
const path = `${DATA}${icon.name.replace("~res:", "")}.packed.webp`;
const image = sharp(await readDVPLFile(path)).trim();
const content = await image.toBuffer();
return { for (const key in avatar.root.skills) {
content, const skill = avatar.root.skills[key];
path: `icons/skills/${name}.webp`, const icon = Array.isArray(skill.icon) ? skill.icon[0] : skill.icon;
} satisfies FileChange; const name = icon.name.split("/").at(-1)!.replace(/_\d$/, "");
}) const path = `Data${icon.name.replace("~res:", "")}.packed.webp`;
); const image = sharp(await vfs.file(path)).trim();
const content = await image.toBuffer();
commitAssets("skill icons", changes); await uploader.add({ content, path: `icons/skills/${name}.webp` });
}
await uploader.flush();
} }
+24 -34
View File
@@ -1,44 +1,34 @@
import { toUniqueId } from '@blitzkit/core'; import { toUniqueId } from "@blitzkit/core";
import { NodeIO } from '@gltf-transform/core'; import { NodeIO } from "@gltf-transform/core";
import { readdir } from 'fs/promises'; import { extractArmor } from "../core/blitz/extractArmor";
import { extractArmor } from '../core/blitz/extractArmor'; import { AssetUploader } from "../core/github/assetUploader";
import { readXMLDVPL } from '../core/blitz/readXMLDVPL'; import { vfs } from "./constants";
import { commitAssets } from '../core/github/commitAssets'; import { VehicleDefinitionList } from "./definitions";
import { FileChange } from '../core/github/commitMultipleFiles';
import { DATA } from './constants';
import { VehicleDefinitionList } from './definitions';
export async function tankArmors() { export async function tankArmors() {
console.log('Building tank armors...'); console.log("Building tank armors...");
const changes: FileChange[] = []; using uploader = new AssetUploader("tank armors");
const nodeIO = new NodeIO(); const nodeIO = new NodeIO();
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then( const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common");
(nations) => nations.filter((nation) => nation !== 'common'),
);
await Promise.all( for (const nation of nations) {
nations.map(async (nation) => { const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( `Data/XML/item_defs/vehicles/${nation}/list.xml`
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`, );
);
await Promise.all( for (const tankKey in tanks.root) {
Object.entries(tanks.root).map(async ([tankKey, tank]) => { const tank = tanks.root[tankKey];
if (tankKey.includes('tutorial_bot')) return;
const id = toUniqueId(nation, tank.id); if (tankKey.includes("tutorial_bot")) continue;
const model = await extractArmor(DATA, `${nation}-${tankKey}`);
const content = await nodeIO.writeBinary(model);
changes.push({ const id = toUniqueId(nation, tank.id);
path: `3d/tanks/armor/${id}.glb`, const model = await extractArmor(vfs, `${nation}-${tankKey}`);
content, const content = await nodeIO.writeBinary(model);
});
}),
);
}),
);
await commitAssets('tank armor', changes); await uploader.add({ path: `3d/tanks/armor/${id}.glb`, content });
}
}
await uploader.flush();
} }
+46 -55
View File
@@ -1,13 +1,8 @@
import { NATION_IDS } from "@blitzkit/core"; import { NATION_IDS } from "@blitzkit/core";
import { readdir } from "fs/promises";
import sharp from "sharp"; import sharp from "sharp";
import type { Vector3Tuple } from "three"; import type { Vector3Tuple } from "three";
import { readDVPLFile } from "../core/blitz/readDVPLFile"; import { AssetUploader } from "../core/github/assetUploader";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL"; import { vfs } from "./constants";
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
import { commitAssets } from "../core/github/commitAssets";
import type { FileChange } from "../core/github/commitMultipleFiles";
import { DATA } from "./constants";
import type { VehicleDefinitionList } from "./definitions"; import type { VehicleDefinitionList } from "./definitions";
export interface TankParameters { export interface TankParameters {
@@ -45,56 +40,52 @@ export interface TankParameters {
export async function tankIcons() { export async function tankIcons() {
console.log("Building tank icons..."); console.log("Building tank icons...");
const changes: FileChange[] = []; using uploader = new AssetUploader("tank icons");
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then( const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common")
(nations) => nations.filter((nation) => nation !== "common")
);
await Promise.all( for (const nation of nations) {
nations.map(async (nation) => { const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( `Data/XML/item_defs/vehicles/${nation}/list.xml`
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml` );
for (const tankKey in tanks.root) {
const tank = tanks.root[tankKey];
if (tankKey.includes("tutorial_bot")) continue;
const nationVehicleId = tank.id;
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
const parameters = await vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
); );
const smallPath = `Data/${parameters.resourcesPath.smallIconPath
.replace(/~res:\//, "")
.replace(/\..+/, "")}.packed.webp`;
const bigPath = `Data/${parameters.resourcesPath.bigIconPath
.replace(/~res:\//, "")
.replace(/\..+/, "")}.packed.webp`;
const big = await sharp(await vfs.file(bigPath))
.trim()
.toBuffer();
const small = await sharp(await vfs.file(smallPath))
.trim()
.toBuffer();
await Promise.all( if (big) {
Object.entries(tanks.root).map(async ([tankKey, tank]) => { await uploader.add({
if (tankKey.includes("tutorial_bot")) return; content: big,
path: `icons/tanks/big/${id}.webp`,
});
}
if (small) {
await uploader.add({
content: small,
path: `icons/tanks/small/${id}.webp`,
});
}
}
}
const nationVehicleId = tank.id; await uploader.flush();
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
);
const smallPath = `${DATA}/${parameters.resourcesPath.smallIconPath
.replace(/~res:\//, "")
.replace(/\..+/, "")}.packed.webp`;
const bigPath = `${DATA}/${parameters.resourcesPath.bigIconPath
.replace(/~res:\//, "")
.replace(/\..+/, "")}.packed.webp`;
const big = await sharp(await readDVPLFile(bigPath))
.trim()
.toBuffer();
const small = await sharp(await readDVPLFile(smallPath))
.trim()
.toBuffer();
if (big) {
changes.push({
content: big,
path: `icons/tanks/big/${id}.webp`,
});
}
if (small) {
changes.push({
content: small,
path: `icons/tanks/small/${id}.webp`,
});
}
})
);
})
);
await commitAssets("tank icons", changes);
} }
+12 -24
View File
@@ -1,54 +1,42 @@
import { toUniqueId } from "@blitzkit/core"; import { toUniqueId } from "@blitzkit/core";
import { NodeIO } from "@gltf-transform/core"; import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { readdir } from "fs/promises";
import { extractModel } from "../core/blitz/extractModel"; import { extractModel } from "../core/blitz/extractModel";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL"; import { AssetUploader } from "../core/github/assetUploader";
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL"; import { vfs } from "./constants";
import { commitAssets } from "../core/github/commitAssets";
import { FileChange } from "../core/github/commitMultipleFiles";
import { DATA } from "./constants";
import { VehicleDefinitionList } from "./definitions"; import { VehicleDefinitionList } from "./definitions";
import { TankParameters } from "./tankIcons"; import { TankParameters } from "./tankIcons";
export async function tankModels() { export async function tankModels() {
console.log("Building tank models..."); console.log("Building tank models...");
using uploader = new AssetUploader("tank models");
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS); const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then( const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common")
(nations) => nations.filter((nation) => nation !== "common")
);
for (const nationIndex in nations) { for (const nationIndex in nations) {
const nation = nations[nationIndex]; const nation = nations[nationIndex];
const changes: FileChange[] = []; const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( `Data/XML/item_defs/vehicles/${nation}/list.xml`
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`
); );
const entries = Object.entries(tanks.root); const entries = Object.entries(tanks.root);
console.log(`Building models for ${nation}`);
let index = 0;
for (const [tankKey, tank] of entries) { for (const [tankKey, tank] of entries) {
if (tankKey.includes("tutorial_bot")) continue; if (tankKey.includes("tutorial_bot")) continue;
const id = toUniqueId(nation, tank.id); const id = toUniqueId(nation, tank.id);
const parameters = await vfs.yaml<TankParameters>(
console.log(` ${++index} / ${entries.length} : ${tankKey}`); `Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
); );
const model = await extractModel( const model = await extractModel(
DATA, vfs,
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, "") parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, "")
); );
const content = Buffer.from(await nodeIO.writeBinary(model)); const content = Buffer.from(await nodeIO.writeBinary(model));
changes.push({ path: `3d/tanks/models/${id}.glb`, content }); await uploader.add({ path: `3d/tanks/models/${id}.glb`, content });
} }
await commitAssets(`tank models ${nation}`, changes);
} }
await uploader.flush();
} }
+1 -1
View File
@@ -5,7 +5,7 @@ import { argv } from "process";
const projectDir = "../.."; const projectDir = "../..";
const srcRoot = `${projectDir}/packages/bot`; const srcRoot = `${projectDir}/packages/bot`;
const distRoot = `${projectDir}/dist/bot`; const distRoot = `${projectDir}/dist/bot`;
const modulesRoot = `${projectDir}/packages/bot/node_modules`; const modulesRoot = "../../node_modules";
const workers = await readdir(`${srcRoot}/src/workers`); const workers = await readdir(`${srcRoot}/src/workers`);
const dev = argv.includes("--dev"); const dev = argv.includes("--dev");
@@ -0,0 +1,62 @@
import { readdir, readFile, writeFile } from "fs/promises";
const SOURCES = "../../packages/closed/protos";
const TARGET = "../../packages/closed/src/unreal/catalogItemAccessor.ts";
const componentPattern = /^message (\w+)Component {/gm;
const files = await readdir(SOURCES).then((files) =>
files.filter(
(file) =>
file.startsWith("blitz_static_") && file.endsWith("_component.proto")
)
);
const imports: { name: string; file: string }[] = [];
for (const file of files) {
const content = await readFile(`${SOURCES}/${file}`).then((buffer) =>
buffer.toString()
);
const components = content.matchAll(componentPattern);
for (const match of components) {
const name = match[1];
const duplicate = imports.find((_import) => _import.name === name);
if (duplicate) {
console.warn(
`Duplicate component ${name} across ${duplicate.file}.proto and ${file}`
);
continue;
}
const importFile = file.replace(".proto", "");
imports.push({ name, file: importFile });
}
}
let content = "";
for (const _import of imports) {
content += `import { ${_import.name}Component } from "@protos/${_import.file}";\n`;
}
content +=
'import { BaseCatalogItemAccessor } from "./baseCatalogItemAccessor";\n\n';
content +=
"export class CatalogItemAccessor extends BaseCatalogItemAccessor {\n";
for (const _import of imports) {
const strippedName = _import.name.replace("BlitzStatic", "");
const defaultName = strippedName[0].toLowerCase() + strippedName.slice(1);
content += ` ${strippedName}(name = "${defaultName}Component") {\n`;
content += ` return this.component(name, ${_import.name}Component);\n`;
content += " }\n";
}
content += "}\n";
await writeFile(TARGET, content);
+6 -9
View File
@@ -4,21 +4,18 @@ const IMPORT = "@protos/auto_legacy_proxy_client";
const SOURCE = "../../packages/closed/protos/auto_legacy_proxy_client.proto"; const SOURCE = "../../packages/closed/protos/auto_legacy_proxy_client.proto";
const TARGET = "../../packages/closed/src/unreal/methodPopulatedProxyClient.ts"; const TARGET = "../../packages/closed/src/unreal/methodPopulatedProxyClient.ts";
const requestPattern = /message (\w+)Request {/g; const requestPattern = /^message (\w+)Request {/gm;
const source = await readFile(SOURCE).then((buffer) => buffer.toString()); const source = await readFile(SOURCE).then((buffer) => buffer.toString());
const requests = source.matchAll(requestPattern); const requests = source.matchAll(requestPattern);
const imports: string[] = []; const imports: string[] = [];
const methods: { name: string; method: string }[] = []; const names: string[] = [];
for (const match of requests) { for (const match of requests) {
const name = match[1]; const name = match[1];
// const method =
// name.charAt(0).toLowerCase() + name.slice(1).replace(/Async$/, "");
const method = name;
imports.push(`${name}Request`, `${name}Response`); imports.push(`${name}Request`, `${name}Response`);
methods.push({ name, method }); names.push(name);
} }
let content = ""; let content = "";
@@ -31,9 +28,9 @@ content += 'import { BaseProxyClient } from "./baseProxyClient";\n\n';
content += content +=
"export abstract class MethodPopulatedProxyClient extends BaseProxyClient {\n"; "export abstract class MethodPopulatedProxyClient extends BaseProxyClient {\n";
content += methods content += names
.map(({ name, method }) => { .map((name) => {
return ` ${method}(data: ${name}Request) { return ` ${name}(data: ${name}Request) {
return this.request("${name}Request", ${name}Request, data, ${name}Response); return this.request("${name}Request", ${name}Request, data, ${name}Response);
}\n`; }\n`;
}) })
+8 -10
View File
@@ -13,8 +13,7 @@ import {
} from "@blitzkit/core"; } from "@blitzkit/core";
import { chunk } from "lodash-es"; import { chunk } from "lodash-es";
import { argv } from "process"; import { argv } from "process";
import { commitAssets } from "./core/github/commitAssets"; import { AssetUploader } from "./core/github/assetUploader";
import { FileChange } from "./core/github/commitMultipleFiles";
const region = argv const region = argv
.find((arg) => arg.startsWith("--region=")) .find((arg) => arg.startsWith("--region="))
@@ -156,16 +155,15 @@ const info = await patientFetchJSON<RatingInfo & { detail: undefined }>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/` `https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`
); );
const normalizedServer = regionSubdomain === "na" ? "com" : regionSubdomain; const normalizedServer = regionSubdomain === "na" ? "com" : regionSubdomain;
const content = RatingLeaderboard.encode({ const content = RatingLeaderboard.encode({
version: { $case: "v2", value: { entries } }, version: { $case: "v2", value: { entries } },
}).finish(); }).finish();
const changes: FileChange[] = [ using uploader = new AssetUploader(`rating leaderboard ${regionSubdomain}`);
{
path: `regions/${normalizedServer}/rating/${info.current_season}.pb`,
content,
},
];
commitAssets(`rating leaderboard ${regionSubdomain}`, changes); await uploader.add({
path: `regions/${normalizedServer}/rating/${info.current_season}.pb`,
content,
});
await uploader.flush();
+29 -29
View File
@@ -3,23 +3,23 @@ import {
Sc2ReadStream, Sc2ReadStream,
ScgReadStream, ScgReadStream,
VertexAttribute, VertexAttribute,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { Accessor, Document, Node, Scene } from '@gltf-transform/core'; import { Accessor, Document, Node, Scene } from "@gltf-transform/core";
import { times } from 'lodash-es'; import { times } from "lodash-es";
import { SteamVFS } from "../steam/vfs";
import { import {
vertexAttributeGLTFName, vertexAttributeGLTFName,
vertexAttributeGltfVectorSizes, vertexAttributeGltfVectorSizes,
} from './extractModel/constants'; } from "./extractModel/constants";
import { readDVPLFile } from './readDVPLFile';
export async function extractArmor(data: string, fileName: string) { export async function extractArmor(vfs: SteamVFS, fileName: string) {
const sc2Path = `${data}/3d/Tanks/CollisionMeshes/${fileName}.sc2`; const sc2Path = `Data/3d/Tanks/CollisionMeshes/${fileName}.sc2`;
const scgPath = `${data}/3d/Tanks/CollisionMeshes/${fileName}.scg`; const scgPath = `Data/3d/Tanks/CollisionMeshes/${fileName}.scg`;
const sc2 = new Sc2ReadStream( const sc2 = new Sc2ReadStream(
(await readDVPLFile(sc2Path)).buffer as ArrayBuffer, (await vfs.file(sc2Path)).buffer as ArrayBuffer
).sc2(); ).sc2();
const scg = new ScgReadStream( const scg = new ScgReadStream(
(await readDVPLFile(scgPath)).buffer as ArrayBuffer, (await vfs.file(scgPath)).buffer as ArrayBuffer
).scg(); ).scg();
const document = new Document(); const document = new Document();
const scene = document.createScene(); const scene = document.createScene();
@@ -29,21 +29,21 @@ export async function extractArmor(data: string, fileName: string) {
hierarchies.forEach((hierarchy) => { hierarchies.forEach((hierarchy) => {
const components = times( const components = times(
hierarchy.components.count, hierarchy.components.count,
(index) => hierarchy.components[index.toString().padStart(4, '0')], (index) => hierarchy.components[index.toString().padStart(4, "0")]
); );
components.forEach((component) => { components.forEach((component) => {
switch (component['comp.typename']) { switch (component["comp.typename"]) {
case 'TransformComponent': case "TransformComponent":
break; break;
case 'RenderComponent': { case "RenderComponent": {
const batch = component['rc.renderObj']['ro.batches']['0000']; const batch = component["rc.renderObj"]["ro.batches"]["0000"];
const polygonGroup = scg.get(batch['rb.datasource']); const polygonGroup = scg.get(batch["rb.datasource"]);
if (!polygonGroup) { if (!polygonGroup) {
console.warn( console.warn(
`Missing polygon group ${batch['rb.datasource']} (${hierarchy.name}); skipping...`, `Missing polygon group ${batch["rb.datasource"]} (${hierarchy.name}); skipping...`
); );
break; break;
@@ -73,7 +73,7 @@ export async function extractArmor(data: string, fileName: string) {
VertexAttribute.TANGENT, VertexAttribute.TANGENT,
attributes attributes
.get(VertexAttribute.TANGENT)! .get(VertexAttribute.TANGENT)!
.map((tangent) => [...tangent, 1]), .map((tangent) => [...tangent, 1])
); );
} }
@@ -85,7 +85,7 @@ export async function extractArmor(data: string, fileName: string) {
const vertexSize = vertexAttributeGltfVectorSizes[attribute]; const vertexSize = vertexAttributeGltfVectorSizes[attribute];
const accessor = document const accessor = document
.createAccessor(name) .createAccessor(name)
.setType(vertexSize === 1 ? 'SCALAR' : `VEC${vertexSize}`) .setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
.setArray(new Float32Array(value.flat())) .setArray(new Float32Array(value.flat()))
.setBuffer(buffer); .setBuffer(buffer);
@@ -94,19 +94,19 @@ export async function extractArmor(data: string, fileName: string) {
hardJointIndices.forEach((hardJointIndex) => { hardJointIndices.forEach((hardJointIndex) => {
const node = document.createNode( const node = document.createNode(
`${hierarchy.name}_armor_${hardJointIndex}`, `${hierarchy.name}_armor_${hardJointIndex}`
); );
const mesh = document.createMesh(batch['##name']); const mesh = document.createMesh(batch["##name"]);
const indicesAccessor = document const indicesAccessor = document
.createAccessor() .createAccessor()
.setType('SCALAR') .setType("SCALAR")
.setArray( .setArray(
new Uint16Array( new Uint16Array(
polygonGroup.indices.filter( polygonGroup.indices.filter(
(index) => (index) =>
vertexHardJointIndices.get(index) === hardJointIndex, vertexHardJointIndices.get(index) === hardJointIndex
), )
), )
) )
.setBuffer(buffer); .setBuffer(buffer);
const primitive = document const primitive = document
@@ -127,18 +127,18 @@ export async function extractArmor(data: string, fileName: string) {
default: default:
throw new TypeError( throw new TypeError(
`Unhandled component type: ${component['comp.typename']}`, `Unhandled component type: ${component["comp.typename"]}`
); );
} }
}); });
if (hierarchy['#hierarchy']) { if (hierarchy["#hierarchy"]) {
parseHierarchies(hierarchy['#hierarchy'], parent); parseHierarchies(hierarchy["#hierarchy"], parent);
} }
}); });
} }
parseHierarchies(sc2['#hierarchy'], scene); parseHierarchies(sc2["#hierarchy"], scene);
return document; return document;
} }
@@ -9,8 +9,8 @@ import { Document, Material, Node, Scene } from "@gltf-transform/core";
import { dedup, prune } from "@gltf-transform/functions"; import { dedup, prune } from "@gltf-transform/functions";
import { times } from "lodash-es"; import { times } from "lodash-es";
import { dirname } from "path"; import { dirname } from "path";
import { SteamVFS } from "../../steam/vfs";
import { readBaseColor } from "../readBaseColor"; import { readBaseColor } from "../readBaseColor";
import { readDVPLFile } from "../readDVPLFile";
import { readNormal } from "../readNormal"; import { readNormal } from "../readNormal";
import { readRoughnessMetallic } from "../readRoughnessMetallic"; import { readRoughnessMetallic } from "../readRoughnessMetallic";
import { import {
@@ -25,14 +25,14 @@ const omitMeshNames = {
end: ["_POINT"], end: ["_POINT"],
}; };
export async function extractModel(data: string, path: string) { export async function extractModel(vfs: SteamVFS, path: string) {
const sc2Path = `${data}/3d/${path}.sc2`; const sc2Path = `Data/3d/${path}.sc2`;
const scgPath = `${data}/3d/${path}.scg`; const scgPath = `Data/3d/${path}.scg`;
const sc2 = new Sc2ReadStream( const sc2 = new Sc2ReadStream(
(await readDVPLFile(sc2Path)).buffer as ArrayBuffer (await vfs.file(sc2Path)).buffer as ArrayBuffer
).sc2(); ).sc2();
const scg = new ScgReadStream( const scg = new ScgReadStream(
(await readDVPLFile(scgPath)).buffer as ArrayBuffer (await vfs.file(scgPath)).buffer as ArrayBuffer
).scg(); ).scg();
const document = new Document(); const document = new Document();
const scene = document.createScene(); const scene = document.createScene();
@@ -67,9 +67,11 @@ export async function extractModel(data: string, path: string) {
.setMimeType("image/webp") .setMimeType("image/webp")
.setImage( .setImage(
await readBaseColor( await readBaseColor(
`${data}/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`, `Data/3d/${dirname(path)}/${
textures.baseColorMap ?? textures.albedo
}`,
textures.miscMap textures.miscMap
? `${data}/3d/${dirname(path)}/${textures.miscMap}` ? `Data/3d/${dirname(path)}/${textures.miscMap}`
: undefined : undefined
) )
) )
@@ -101,7 +103,7 @@ export async function extractModel(data: string, path: string) {
.setMimeType("image/webp") .setMimeType("image/webp")
.setImage( .setImage(
await readRoughnessMetallic( await readRoughnessMetallic(
`${data}/3d/${dirname(path)}/${textures.baseRMMap}` `Data/3d/${dirname(path)}/${textures.baseRMMap}`
) )
) )
); );
@@ -116,7 +118,7 @@ export async function extractModel(data: string, path: string) {
.setMimeType("image/webp") .setMimeType("image/webp")
.setImage( .setImage(
await readNormal( await readNormal(
`${data}/3d/${dirname(path)}/${ `Data/3d/${dirname(path)}/${
textures.baseNormalMap ?? textures.normalmap textures.baseNormalMap ?? textures.normalmap
}`, }`,
isBase isBase
@@ -1,6 +0,0 @@
import { readDVPLFile } from './readDVPLFile';
export async function readBase64DVPL(file: string) {
const buffer = await readDVPLFile(file);
return buffer.toString('base64');
}
@@ -1,21 +0,0 @@
import { readFile } from 'fs/promises';
import { DATA_COMPRESSED } from '../../buildAssets/constants';
import { readDVPL } from './readDVPL';
export async function readDVPLFile(file: string) {
let path: string;
if (DATA_COMPRESSED) {
path = file.endsWith('.dvpl') ? file : `${file}.dvpl`;
} else {
path = file.replace('.dvpl', '');
}
const content = await readFile(path);
if (!DATA_COMPRESSED) return content;
const dvpl = readDVPL(content);
return dvpl;
}
@@ -1,6 +0,0 @@
import { readDVPLFile } from './readDVPLFile';
export async function readStringDVPL(file: string) {
const buffer = await readDVPLFile(file);
return buffer.toString();
}
@@ -1,81 +1,17 @@
import { DdsReadStream, PvrReadStream } from "@blitzkit/core"; import { DdsReadStream, PvrReadStream } from "@blitzkit/core";
import { existsSync } from "fs"; import { vfs } from "../../../buildAssets/constants";
import { DATA_COMPRESSED } from "../../../buildAssets/constants";
import { readDVPLFile } from "../readDVPLFile";
export async function readTexture(path: string) { export async function readTexture(path: string) {
const ddsTexturePath = path.replace(".tex", ".dx11.dds"); const ddsTexturePath = path.replace(".tex", ".dx11.dds");
const isDds = existsSync( const isDds = vfs.has(ddsTexturePath);
`${ddsTexturePath}${DATA_COMPRESSED ? ".dvpl" : ""}`
);
const resolvedTexturePath = isDds const resolvedTexturePath = isDds
? ddsTexturePath ? ddsTexturePath
: ddsTexturePath.replace(".dds", ".pvr"); : ddsTexturePath.replace(".dds", ".pvr");
const decompressedDvpl = await readDVPLFile(`${resolvedTexturePath}.dvpl`); const file = await vfs.file(resolvedTexturePath);
const raw = isDds const raw = isDds
? await new DdsReadStream(decompressedDvpl.buffer as ArrayBuffer).dds() ? await new DdsReadStream(file.buffer as ArrayBuffer).dds()
: new PvrReadStream(decompressedDvpl.buffer as ArrayBuffer).pvr(); : new PvrReadStream(file.buffer as ArrayBuffer).pvr();
return raw; return raw;
// switch (mutation) {
// case TextureMutation.Normal: {
// const bytes = 4 * raw.width * raw.height;
// for (let index = 0; index < bytes; index += 4) {
// /**
// * Red is always 255 and blue is always 0. Only alpha and green contain any
// * sort of information.
// */
// let x = raw.data[index + 3] * (2 / 255) - 1;
// let y = raw.data[index + 1] * (2 / 255) - 1;
// const underSqrt = 1 - x ** 2 - y ** 2;
// let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt);
// raw.data[index] = Math.round((x + 1) * (255 / 2));
// raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
// raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
// raw.data[index + 3] = 255;
// }
// return await sharp(raw.data, { raw }).jpeg().toBuffer();
// }
// case TextureMutation.RoughnessMetallicness: {
// const newImage = sharp(Buffer.alloc(raw.width * raw.height), {
// raw: { ...raw, channels: 1 },
// });
// const metallicness = sharp(raw.data, { raw })
// .extractChannel("green")
// .jpeg()
// .toBuffer();
// const roughness = sharp(raw.data, { raw })
// .extractChannel("alpha")
// .jpeg()
// .toBuffer();
// return await newImage
// .joinChannel(await Promise.all([roughness, metallicness]))
// .jpeg()
// .toBuffer();
// }
// case TextureMutation.Miscellaneous: {
// /**
// * Alpha is ambient occlusion and green is emissive. But very few tanks
// * use emissive so I am ignoring it for now.
// */
// return await sharp(raw.data, { raw })
// .extractChannel("alpha")
// .jpeg()
// .toBuffer();
// }
// case TextureMutation.Albedo: {
// return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
// }
// default:
// return await sharp(raw.data, { raw }).jpeg().toBuffer();
// }
} }
@@ -1,9 +0,0 @@
import { XMLParser } from 'fast-xml-parser';
import { readStringDVPL } from './readStringDVPL';
export async function readXMLDVPL<Type>(file: string) {
const parser = new XMLParser();
const xml = await readStringDVPL(file);
return parser.parse(xml) as Type;
}
@@ -1,7 +0,0 @@
import { readFile } from 'fs/promises';
import { parse } from 'yaml';
export async function readYAML<Type>(file: string) {
const yaml = (await readFile(file)).toString();
return parse(yaml) as Type;
}
@@ -1,8 +0,0 @@
import { parse } from 'yaml';
import { readStringDVPL } from './readStringDVPL';
export async function readYAMLDVPL<Type>(file: string) {
const yaml = await readStringDVPL(file);
return parse(yaml) as Type;
}
@@ -0,0 +1,216 @@
import { assertSecret } from "@blitzkit/core";
import { Octokit } from "@octokit/rest";
import ProgressBar from "progress";
import { compareUint8Arrays } from "./compareUint8Arrays";
export interface FileChange {
path: string;
content: Uint8Array;
}
interface GithubChangeBlob {
sha: string;
path: string;
mode: "100644";
type: "blob";
}
enum DiffStatus {
New,
Changed,
Unchanged,
}
export class AssetUploader {
time_per_blob = 2 ** 4 * 1000;
time_between_blobs = (60 * 60 * 1000) / 5000 / 0.9;
max_tree_size = 7_000_000;
max_file_count = 128;
heuristicFormats = ["glb", "webp", "jpg", "png"];
owner: string;
repo: string;
branch: string;
changes: FileChange[] = [];
changesSize = 0;
private octokit = new Octokit({
auth: assertSecret(import.meta.env.GH_TOKEN),
});
constructor(public message: string) {
[this.owner, this.repo] = assertSecret(
import.meta.env.PUBLIC_ASSET_REPO
).split("/");
this.branch = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
}
async add(change: FileChange) {
const blobPath = `${this.owner}/${this.repo}/${this.branch}/${change.path}`;
const response = await fetch(
`https://raw.githubusercontent.com/${blobPath}`,
{ headers: { "Accept-Encoding": "identity" } }
);
let diff: { change: number; status: DiffStatus };
if (response.status === 404) {
diff = { change: change.content.length, status: DiffStatus.New };
} else if (
response.headers.has("Content-Length") &&
Number(response.headers.get("Content-Length")) !== change.content.length
) {
diff = {
change:
change.content.length -
Number(response.headers.get("Content-Length")),
status: DiffStatus.Changed,
};
} else if (response.status === 200) {
if (
this.heuristicFormats.some((format) =>
change.path.endsWith(`.${format}`)
)
) {
/**
* Heuristic: if it's a large blob like a .glb, a diff = 0 might end
* up being a false positive with isDifferent = true. So, it's okay
* to assume it's unchanged.
*/
diff = { change: 0, status: DiffStatus.Unchanged };
} else {
const buffer = new Uint8Array(await response.arrayBuffer());
if (compareUint8Arrays(buffer, change.content)) {
diff = { change: 0, status: DiffStatus.Unchanged };
} else {
diff = {
change: change.content.length - buffer.length,
status: DiffStatus.Changed,
};
}
}
} else {
throw new Error(
`Unexpected status code ${response.status} for ${change.path}`
);
}
if (diff.status === DiffStatus.Unchanged) {
// console.log(`🔵 ${change.path}`);
} else {
console.log(
`${diff.status === DiffStatus.New ? "🟢" : "🟡"} (${
diff.change > 0 ? "+" : ""
}${diff.change.toLocaleString()}B) ${change.path}`
);
// flush BEFORE going over limit
if (
this.changesSize + change.content.length > this.max_tree_size ||
this.changes.length + 1 > this.max_file_count
) {
console.log(`Flushing ${this.changes.length} changes...`);
await this.flush();
}
this.changes.push(change);
this.changesSize += change.content.length;
}
}
async flush() {
if (this.changes.length == 0) return;
const latestCommitSha = (
await this.octokit.git.getRef({
owner: this.owner,
repo: this.repo,
ref: `heads/${this.branch}`,
})
).data.object.sha;
const treeSha = (
await this.octokit.git.getCommit({
owner: this.owner,
repo: this.repo,
commit_sha: latestCommitSha,
})
).data.tree.sha;
const blobs: GithubChangeBlob[] = [];
const bar = new ProgressBar("Blobs :bar", this.changes.length);
for (const change of this.changes) {
while (true) {
try {
const start = Date.now();
var { sha } = await this.octokit.git
.createBlob({
owner: this.owner,
repo: this.repo,
encoding: "base64",
content: Buffer.from(change.content).toString("base64"),
})
.then((response) => response.data);
const end = Date.now();
console.log(` blobbed ${change.path} ${end - start}ms`);
await new Promise((resolve) =>
setTimeout(
resolve,
Math.max(0, this.time_between_blobs + (start - end))
)
);
blobs.push({ sha, path: change.path, mode: "100644", type: "blob" });
bar.tick();
break;
} catch {
console.warn(
` failed ${change.path}; retrying in ${this.time_per_blob}ms...`
);
await new Promise((resolve) =>
setTimeout(resolve, this.time_per_blob)
);
}
}
}
const { data: treeData } = await this.octokit.git.createTree({
owner: this.owner,
repo: this.repo,
base_tree: treeSha,
tree: blobs,
});
const { data: commitData } = await this.octokit.git.createCommit({
owner: this.owner,
repo: this.repo,
message: this.message,
tree: treeData.sha,
parents: [latestCommitSha],
});
await this.octokit.git.updateRef({
owner: this.owner,
repo: this.repo,
ref: `heads/${this.branch}`,
sha: commitData.sha,
force: true,
});
this.changes = [];
this.changesSize = 0;
}
[Symbol.dispose]() {
if (this.changes.length > 0) {
throw new Error(
`${this.changes.length} uncommitted changes. Did you forget to flush?`
);
}
}
}
@@ -1,25 +0,0 @@
import { assertSecret } from '@blitzkit/core';
import { chunk } from 'lodash-es';
import { commitMultipleFiles, FileChange } from './commitMultipleFiles';
export async function commitAssets(message: string, changes: FileChange[]) {
console.log(`Committing ${message}...`);
if (changes.length === 0) return;
const chunks = chunk(changes, 64);
let chunkIndex = 0;
for (const chunk of chunks) {
console.log(`Committing chunk ${chunkIndex + 1}/${chunks.length}`);
await commitMultipleFiles(
assertSecret(import.meta.env.PUBLIC_ASSET_REPO),
assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH),
`${message} - ${new Date().toDateString()} (${chunkIndex + 1}/${chunks.length})`,
chunk,
);
chunkIndex++;
}
}
@@ -1,135 +0,0 @@
import { asset } from "@blitzkit/core";
import ProgressBar from "progress";
import { compareUint8Arrays } from "./compareUint8Arrays";
import { type GithubChangeBlob, createBlob } from "./createBlob";
import { octokit } from "./octokit";
export interface FileChange {
path: string;
content: Uint8Array;
}
enum DiffStatus {
New,
Changed,
Unchanged,
}
const heuristicFormats = ["glb", "webp", "jpg", "png"];
export async function commitMultipleFiles(
repoRaw: string,
branch: string,
message: string,
changesRaw: FileChange[]
) {
const changes: FileChange[] = [];
await Promise.all(
changesRaw.map(async (change) => {
const response = await fetch(asset(change.path), {
headers: { "Accept-Encoding": "identity" },
});
let diff: { change: number; status: DiffStatus };
if (response.status === 404) {
diff = { change: change.content.length, status: DiffStatus.New };
} else if (
response.headers.has("Content-Length") &&
Number(response.headers.get("Content-Length")) !== change.content.length
) {
diff = {
change:
change.content.length -
Number(response.headers.get("Content-Length")),
status: DiffStatus.Changed,
};
} else if (response.status === 200) {
if (
heuristicFormats.some((format) => change.path.endsWith(`.${format}`))
) {
/**
* Heuristic: if it's a large blob like a .glb, a diff = 0 might end
* up being a false positive with isDifferent = true. So, it's okay
* to assume it's unchanged.
*/
diff = { change: 0, status: DiffStatus.Unchanged };
} else {
const buffer = new Uint8Array(await response.arrayBuffer());
if (compareUint8Arrays(buffer, change.content)) {
diff = { change: 0, status: DiffStatus.Unchanged };
} else {
diff = {
change: change.content.length - buffer.length,
status: DiffStatus.Changed,
};
}
}
} else {
throw new Error(
`Unexpected status code ${response.status} for ${change.path}`
);
}
if (diff.status === DiffStatus.Unchanged) {
// just don't bother
// console.log(`🔵 (${diff.change.toLocaleString()}B) ${change.path}`);
} else {
console.log(
`${
diff.status === DiffStatus.New ? "🟢" : "🟡"
} (${diff.change > 0 ? "+" : ""}${diff.change.toLocaleString()}B) ${change.path}`
);
changes.push(change);
}
})
);
if (changes.length === 0) return;
const [owner, repo] = repoRaw.split("/");
const latestCommitSha = (
await octokit.git.getRef({
owner,
repo,
ref: `heads/${branch}`,
})
).data.object.sha;
const treeSha = (
await octokit.git.getCommit({
owner,
repo,
commit_sha: latestCommitSha,
})
).data.tree.sha;
const blobs: GithubChangeBlob[] = [];
const bar = new ProgressBar("Blobs :bar", changes.length);
for (const change of changes) {
const createdBlob = await createBlob(owner, repo, change);
blobs.push(createdBlob!);
bar.tick();
}
const { data: treeData } = await octokit.git.createTree({
owner,
repo,
base_tree: treeSha,
tree: blobs,
});
const { data: newCommitData } = await octokit.git.createCommit({
owner,
repo,
message,
tree: treeData.sha,
parents: [latestCommitSha],
});
await octokit.git.updateRef({
owner,
repo,
ref: `heads/${branch}`,
sha: newCommitData.sha,
force: true,
});
}
@@ -1,5 +1,5 @@
import { throttle } from "lodash-es"; import { throttle } from "lodash-es";
import type { FileChange } from "./commitMultipleFiles"; import { FileChange } from "./assetUploader";
import { octokit } from "./octokit"; import { octokit } from "./octokit";
export interface GithubChangeBlob { export interface GithubChangeBlob {
+3 -3
View File
@@ -1,6 +1,6 @@
import { assertSecret } from '@blitzkit/core'; import { assertSecret } from "@blitzkit/core";
import { throttling } from '@octokit/plugin-throttling'; import { throttling } from "@octokit/plugin-throttling";
import { Octokit } from '@octokit/rest'; import { Octokit } from "@octokit/rest";
Octokit.plugin(throttling); Octokit.plugin(throttling);
+155
View File
@@ -0,0 +1,155 @@
import { readDVPL } from "@blitzkit/core/src/blitz";
import { XMLParser } from "fast-xml-parser";
import { normalize } from "path/posix";
import SteamUser, { EConnectionProtocol } from "steam-user";
import { parse } from "yaml";
interface SteamManifestFile {
chunks: {
sha: string;
crc: number;
offset: string;
cb_original: number;
cb_compressed: number;
}[];
filename: string;
size: string;
flags: number;
sha_filename: string;
sha_content: string;
linktarget: unknown;
}
interface SteamManifest {
files: SteamManifestFile[];
depot_id: number;
gid_manifest: string;
creation_time: number;
filenames_encrypted: boolean;
cb_disk_original: string;
cb_disk_compressed: string;
unique_chunks: number;
crc_encrypted: number;
crc_clear: number;
}
export class SteamVFS {
private steam = new SteamUser({ protocol: EConnectionProtocol.TCP });
private textDecoder = new TextDecoder();
private xmlParser = new XMLParser();
private manifest: Map<string, SteamManifestFile> = new Map();
constructor(
private username: string,
private password: string,
private app: number,
private depot: number
) {}
async init() {
this.steam.logOn({
accountName: this.username,
password: this.password,
});
await new Promise((resolve) => this.steam.once("loggedOn", resolve));
const productInfo = await this.steam.getProductInfo([this.app], []);
const manifest: SteamManifest = await new Promise((resolve) => {
// @ts-expect-error
this.steam.getManifest(
this.app,
this.depot,
productInfo.apps[this.app].appinfo.depots[this.depot].manifests.public
.gid,
"public",
// @ts-expect-error
(_, response) => {
resolve(response);
}
);
});
const paths: string[] = [];
for (const file of manifest.files) {
const path = file.filename.replaceAll("\\", "/");
paths.push(path);
this.manifest.set(path, file);
}
return this;
}
has(path: string) {
const normalized = normalize(path);
const dvplPath = `${normalized}.dvpl`;
return (
(this.manifest.has(dvplPath) && dvplPath) ||
(this.manifest.has(normalized) && normalized)
);
}
assert(requested: string) {
const path = this.has(requested);
if (!path) throw new Error(`File not found: ${requested}`);
return path;
}
dir(path: string) {
const parentSegments = path.split("/").length;
const children: string[] = [];
for (const child of this.manifest.keys()) {
const childSegments = child.split("/");
if (
child.startsWith(path) &&
parentSegments + 1 === childSegments.length
) {
children.push(childSegments.at(-1)!);
}
}
return children;
}
async file(requested: string) {
const path = this.assert(requested);
const fileManifest = this.manifest.get(path)!;
const downloaded: { type: "complete"; file: Buffer } =
// @ts-expect-error
await this.steam.downloadFile(this.app, this.depot, fileManifest);
let buffer = downloaded.file;
if (path.endsWith(".dvpl")) buffer = readDVPL(downloaded.file);
return new Uint8Array(buffer);
}
async text(path: string) {
const file = await this.file(path);
return this.textDecoder.decode(file);
}
async yaml<Type>(path: string) {
const file = await this.text(path);
return parse(file) as Type;
}
async xml<Type>(path: string) {
const file = await this.text(path);
return this.xmlParser.parse(file) as Type;
}
dispose() {
this.steam.logOff();
}
[Symbol.dispose]() {
this.dispose();
}
}
+40 -37
View File
@@ -9,11 +9,10 @@ import {
getAccountInfo, getAccountInfo,
idToRegion, idToRegion,
retryAbleBlitzFetchEvent, retryAbleBlitzFetchEvent,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { chunk, times, uniq } from 'lodash-es'; import { chunk, times, uniq } from "lodash-es";
import { compress, decompress } from 'lz4js'; import { compress, decompress } from "lz4js";
import { commitAssets } from './core/github/commitAssets'; import { AssetUploader } from "./core/github/assetUploader";
import { FileChange } from './core/github/commitMultipleFiles';
const CHUNK_SIZE = 2 ** 21; const CHUNK_SIZE = 2 ** 21;
const RUN_TIME = 1000 * 60 * 60 * 5.5; const RUN_TIME = 1000 * 60 * 60 * 5.5;
@@ -24,12 +23,12 @@ const TERMINATION_THRESHOLD = 1000;
const startTime = Date.now(); const startTime = Date.now();
const indexableRegions = [...REGIONS]; const indexableRegions = [...REGIONS];
let ids: number[] = []; let ids: number[] = [];
const preDiscoveredManifest = (await fetch(asset('ids/manifest.json')).then( const preDiscoveredManifest = (await fetch(asset("ids/manifest.json")).then(
(response) => response.json(), (response) => response.json()
)) as DiscoveredIdsDefinitions; )) as DiscoveredIdsDefinitions;
console.log( console.log(
`Fetching ${preDiscoveredManifest.chunks} pre-discovered chunks...`, `Fetching ${preDiscoveredManifest.chunks} pre-discovered chunks...`
); );
let chunkIndex = 0; let chunkIndex = 0;
@@ -39,23 +38,23 @@ while (chunkIndex < preDiscoveredManifest.chunks) {
const buffer = await response.arrayBuffer(); const buffer = await response.arrayBuffer();
const decompressed = decompress(new Uint8Array(buffer)).buffer; const decompressed = decompress(new Uint8Array(buffer)).buffer;
return new DidsReadStream(decompressed as ArrayBuffer).dids(); return new DidsReadStream(decompressed as ArrayBuffer).dids();
}, }
); );
// no spread syntax: https://github.com/oven-sh/bun/issues/11734 // no spread syntax: https://github.com/oven-sh/bun/issues/11734
ids = ids.concat(preDiscovered); ids = ids.concat(preDiscovered);
console.log( console.log(
`Pre-discovered ${preDiscovered.length} ids (chunk ${chunkIndex})`, `Pre-discovered ${preDiscovered.length} ids (chunk ${chunkIndex})`
); );
chunkIndex++; chunkIndex++;
} }
const regionalIdIndex: Record<Region, number> = { const regionalIdIndex: Record<Region, number> = {
asia: ids.findLast((id) => idToRegion(id) === 'asia') ?? MIN_REGION_IDS.asia, asia: ids.findLast((id) => idToRegion(id) === "asia") ?? MIN_REGION_IDS.asia,
com: ids.findLast((id) => idToRegion(id) === 'com') ?? MIN_REGION_IDS.com, com: ids.findLast((id) => idToRegion(id) === "com") ?? MIN_REGION_IDS.com,
eu: ids.findLast((id) => idToRegion(id) === 'eu') ?? MIN_REGION_IDS.eu, eu: ids.findLast((id) => idToRegion(id) === "eu") ?? MIN_REGION_IDS.eu,
}; };
const zeroStreak: Record<Region, number> = { asia: 0, com: 0, eu: 0 }; const zeroStreak: Record<Region, number> = { asia: 0, com: 0, eu: 0 };
let regionIndex = 0; let regionIndex = 0;
@@ -64,7 +63,7 @@ let outOfTimeFlagged = false;
async function verify(region: Region, ids: number[]) { async function verify(region: Region, ids: number[]) {
const infos = await getAccountInfo(region, ids, undefined, { const infos = await getAccountInfo(region, ids, undefined, {
// cheeky way of requesting nothing // cheeky way of requesting nothing
fields: 'account_id,-account_id', fields: "account_id,-account_id",
}); });
const filtered = ids.filter((_, index) => { const filtered = ids.filter((_, index) => {
const info = infos[index]; const info = infos[index];
@@ -85,7 +84,7 @@ const interval = setInterval(async () => {
const region = indexableRegions[regionIndex]; const region = indexableRegions[regionIndex];
const idsToVerify = times( const idsToVerify = times(
ACCOUNTS_PER_CALL, ACCOUNTS_PER_CALL,
(index) => regionalIdIndex[region] + index, (index) => regionalIdIndex[region] + index
); );
const verified = await verify(region, idsToVerify); const verified = await verify(region, idsToVerify);
@@ -105,8 +104,8 @@ const interval = setInterval(async () => {
indexableRegions.splice(sliceableIndex, 1); indexableRegions.splice(sliceableIndex, 1);
if (indexableRegions.length === 0) { if (indexableRegions.length === 0) {
console.log('All players discovered exhaustively!'); console.log("All players discovered exhaustively!");
post(); await post();
} }
} }
} }
@@ -117,46 +116,50 @@ const interval = setInterval(async () => {
if (Date.now() - startTime > RUN_TIME && !outOfTimeFlagged) { if (Date.now() - startTime > RUN_TIME && !outOfTimeFlagged) {
outOfTimeFlagged = true; outOfTimeFlagged = true;
console.log('Out of time'); console.log("Out of time");
post(); await post();
} }
regionIndex = (regionIndex + 1) % indexableRegions.length; regionIndex = (regionIndex + 1) % indexableRegions.length;
regionalIdIndex[region] += ACCOUNTS_PER_CALL; regionalIdIndex[region] += ACCOUNTS_PER_CALL;
}, 1000 / MAX_REQUESTS); }, 1000 / MAX_REQUESTS);
function post() { async function post() {
clearInterval(interval); clearInterval(interval);
ids = uniq(ids).sort((a, b) => a - b); ids = uniq(ids).sort((a, b) => a - b);
console.log(`Uploading ${ids.length} ids...`); console.log(`Uploading ${ids.length} ids...`);
using uploader = new AssetUploader("discovered ids");
const idsChunked = chunk(ids, CHUNK_SIZE); const idsChunked = chunk(ids, CHUNK_SIZE);
const files = idsChunked.map((chunk, index) => {
let index = 0;
for (const chunk of idsChunked) {
const didsWriteStream = new DidsWriteStream().dids(chunk); const didsWriteStream = new DidsWriteStream().dids(chunk);
const buffer = didsWriteStream.uint8Array; const buffer = didsWriteStream.uint8Array;
const content = compress(buffer); const content = compress(buffer);
console.log( console.log(
`Chunk ${index} with ${chunk.length} ids (compressed: ${content.length}; uncompressed: ${buffer.length})`, `Chunk ${index} with ${chunk.length} ids (compressed: ${content.length}; uncompressed: ${buffer.length})`
); );
return { await uploader.add({
content, content,
path: `ids/${index}.dids.lz4`, path: `ids/${index}.dids.lz4`,
} satisfies FileChange; });
index++;
}
await uploader.add({
content: new TextEncoder().encode(
JSON.stringify({
chunks: idsChunked.length,
count: ids.length,
time: Date.now(),
} satisfies DiscoveredIdsDefinitions)
),
path: "ids/manifest.json",
}); });
commitAssets('discovered ids', [ await uploader.flush();
...files,
{
content: new TextEncoder().encode(
JSON.stringify({
chunks: idsChunked.length,
count: ids.length,
time: Date.now(),
} satisfies DiscoveredIdsDefinitions),
),
path: 'ids/manifest.json',
},
]);
} }
+314
View File
@@ -0,0 +1,314 @@
import {
availableProvisions,
createDefaultProvisions,
createDefaultSkills,
fetchConsumableDefinitions,
fetchEquipmentDefinitions,
fetchModelDefinitions,
fetchProvisionDefinitions,
fetchSkillDefinitions,
fetchTankDefinitions,
TankDefinition,
tankIcon,
} from "@blitzkit/core";
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
import strings from "@blitzkit/i18n/strings/en.json";
import { writeFile } from "fs/promises";
import { isEqual } from "lodash-es";
import { tankCharacteristics } from "../../website/src/core/blitzkit/tankCharacteristics";
import { genericDefaultEquipmentMatrix } from "../../website/src/stores/duel/constants";
const domain = "https://opentest.blitzkit.app";
import.meta.env.PUBLIC_ASSET_BASE = "https://blitzkit.github.io/assets";
const currentTankDefinitions = await fetchTankDefinitions();
const currentConsumableDefinitions = await fetchConsumableDefinitions();
const currentProvisionDefinitions = await fetchProvisionDefinitions();
const currentEquipmentDefinitions = await fetchEquipmentDefinitions();
const currentSkillDefinitions = await fetchSkillDefinitions();
const currentModelDefinitions = await fetchModelDefinitions();
import.meta.env.PUBLIC_ASSET_BASE = "https://blitzkit.github.io/assets-preview";
const previewTankDefinitions = await fetchTankDefinitions();
const previewConsumableDefinitions = await fetchConsumableDefinitions();
const previewProvisionDefinitions = await fetchProvisionDefinitions();
const previewEquipmentDefinitions = await fetchEquipmentDefinitions();
const previewSkillDefinitions = await fetchSkillDefinitions();
const previewModelDefinitions = await fetchModelDefinitions();
let notes = "# BlitzKit OpenTest Patch Notes\n\n";
const newTanks: TankDefinition[] = [];
for (const id in previewTankDefinitions.tanks) {
if (!(id in currentTankDefinitions.tanks)) {
newTanks.push(previewTankDefinitions.tanks[id]);
}
}
newTanks.sort((a, b) => b.tier - a.tier);
import.meta.env.PUBLIC_ASSET_BASE = "https://blitzkit.github.io/assets-preview";
if (newTanks.length > 0) {
notes += "## New Tanks\n\n";
for (const tank of newTanks) {
notes += `### [${tank.name.locales.en}](${domain}/tanks/${tank.slug})\n`;
notes += `![](${tankIcon(tank.id)})\n\n`;
}
notes += "\n";
}
const changedTanks: [TankDefinition, TankDefinition][] = [];
for (const stringId in previewTankDefinitions.tanks) {
const id = Number(stringId);
if (newTanks.some((tank) => tank.id === id)) continue;
const current = currentTankDefinitions.tanks[id];
const preview = previewTankDefinitions.tanks[id];
if (isEqual(current, preview)) continue;
changedTanks.push([current, preview]);
}
changedTanks.sort((a, b) => b[1].tier - a[1].tier);
import.meta.env.PUBLIC_ASSET_BASE = "https://blitzkit.github.io/assets";
if (changedTanks.length > 0) {
notes += "## Changed Tanks\n\n";
for (const [current, preview] of changedTanks) {
const currentTurret = current.turrets.at(-1)!;
const previewTurret = preview.turrets.at(-1)!;
const currentGun = currentTurret.guns.at(-1)!;
const previewGun = previewTurret.guns.at(-1)!;
const currentShell = currentGun.shells[0];
const previewShell = previewGun.shells[0];
const currentEngine = current.engines.at(-1)!;
const previewEngine = preview.engines.at(-1)!;
const currentTracks = current.tracks.at(-1)!;
const previewTracks = preview.tracks.at(-1)!;
const currentTankModel = currentModelDefinitions.models[current.id];
const previewTankModel = previewModelDefinitions.models[preview.id];
const currentConsumables = Object.values(
currentConsumableDefinitions.consumables
).filter((consumable) =>
checkConsumableProvisionInclusivity(consumable, current, currentGun)
);
const previewConsumables = Object.values(
previewConsumableDefinitions.consumables
).filter((consumable) =>
checkConsumableProvisionInclusivity(consumable, preview, previewGun)
);
const addedConsumables = previewConsumables.filter(
(previewConsumable) =>
!currentConsumables.some(
(currentConsumable) => currentConsumable.id === previewConsumable.id
)
);
const removedConsumables = currentConsumables.filter(
(currentConsumable) =>
!previewConsumables.some(
(previewConsumable) => previewConsumable.id === currentConsumable.id
)
);
const currentProvisions = availableProvisions(
current,
currentGun,
currentProvisionDefinitions
);
const previewProvisions = availableProvisions(
preview,
previewGun,
previewProvisionDefinitions
);
const addedProvisions = previewProvisions.filter(
(previewProvision) =>
!currentProvisions.some(
(currentProvision) => currentProvision.id === previewProvision.id
)
);
const removedProvisions = currentProvisions.filter(
(currentProvision) =>
!previewProvisions.some(
(previewProvision) => previewProvision.id === currentProvision.id
)
);
const currentEquipment = currentEquipmentDefinitions.presets[
current.equipment_preset
].slots
.map(({ left, right }) => [left, right])
.flat();
const previewEquipment = previewEquipmentDefinitions.presets[
preview.equipment_preset
].slots
.map(({ left, right }) => [left, right])
.flat();
const addedEquipment = previewEquipment
.filter(
(previewEquipment) =>
!currentEquipment.some(
(currentEquipment) => currentEquipment === previewEquipment
)
)
.map((id) => previewEquipmentDefinitions.equipments[id]);
const removedEquipment = currentEquipment
.filter(
(currentEquipment) =>
!previewEquipment.some(
(previewEquipment) => previewEquipment === currentEquipment
)
)
.map((id) => currentEquipmentDefinitions.equipments[id]);
const currentCharacteristics = tankCharacteristics(
{
tank: current,
gun: currentGun,
shell: currentShell,
applyDynamicArmor: false,
applyReactiveArmor: false,
applySpallLiner: false,
assaultDistance: 0,
crewSkills: createDefaultSkills(currentSkillDefinitions),
camouflage: true,
consumables: [],
engine: currentEngine,
provisions: createDefaultProvisions(
current,
currentGun,
currentProvisionDefinitions
),
equipmentMatrix: genericDefaultEquipmentMatrix,
stockEngine: current.engines[0],
stockGun: current.turrets[0].guns[0],
stockTrack: current.tracks[0],
stockTurret: current.turrets[0],
track: currentTracks,
turret: currentTurret,
},
{
equipmentDefinitions: currentEquipmentDefinitions,
provisionDefinitions: currentProvisionDefinitions,
tankModelDefinition: currentTankModel,
}
);
const previewCharacteristics = tankCharacteristics(
{
tank: preview,
gun: previewGun,
shell: previewShell,
applyDynamicArmor: false,
applyReactiveArmor: false,
applySpallLiner: false,
assaultDistance: 0,
crewSkills: createDefaultSkills(previewSkillDefinitions),
camouflage: true,
consumables: [],
engine: previewEngine,
provisions: createDefaultProvisions(
preview,
previewGun,
previewProvisionDefinitions
),
equipmentMatrix: genericDefaultEquipmentMatrix,
stockEngine: preview.engines[0],
stockGun: preview.turrets[0].guns[0],
stockTrack: preview.tracks[0],
stockTurret: preview.turrets[0],
track: previewTracks,
turret: previewTurret,
},
{
equipmentDefinitions: previewEquipmentDefinitions,
provisionDefinitions: previewProvisionDefinitions,
tankModelDefinition: previewTankModel,
}
);
const changedCharacteristics: string[] = [];
for (const key in currentCharacteristics) {
const characteristicKey = key as keyof typeof currentCharacteristics;
if (
currentCharacteristics[characteristicKey] !==
previewCharacteristics[characteristicKey]
) {
changedCharacteristics.push(characteristicKey);
}
}
notes += `### [${preview.name.locales.en}](${domain}/tanks/${preview.slug})\n`;
notes += `![](${tankIcon(preview.id)})\n\n`;
if (addedConsumables.length + removedConsumables.length > 0) {
notes += `Consumables: `;
notes += [
...addedConsumables.map(
(consumable) => `🟢 ${consumable.name.locales.en}`
),
...removedConsumables.map(
(consumable) => `🔴 ${consumable.name.locales.en}`
),
].join(", ");
notes += `\n`;
}
if (addedProvisions.length + removedProvisions.length > 0) {
notes += `Provisions: `;
notes += [
...addedProvisions.map(
(provision) => `🟢 ${provision.name.locales.en}`
),
...removedProvisions.map(
(provision) => `🔴 ${provision.name.locales.en}`
),
].join(", ");
notes += `\n`;
}
if (addedEquipment.length + removedEquipment.length > 0) {
notes += `-Equipment: `;
notes += [
...addedEquipment.map((equipment) => `🟢 ${equipment.name.locales.en}`),
...removedEquipment.map(
(equipment) => `🔴 ${equipment.name.locales.en}`
),
].join(", ");
notes += `\n`;
}
if (changedCharacteristics.length > 0) {
notes += `Characteristics:\n`;
for (const characteristicUntyped of changedCharacteristics) {
const characteristic =
characteristicUntyped as keyof typeof currentCharacteristics;
const name =
strings.website.tools.tankopedia.characteristics.values[
characteristic
];
const before = currentCharacteristics[characteristic];
const after = previewCharacteristics[characteristic];
notes += `- ${
typeof before === "number" && typeof after === "number"
? after > before
? "⬆️"
: "🔻"
: "✏️"
} ${name}: \`${
typeof before === "number" ? before.toFixed(2) : before
}\` → \`${typeof after === "number" ? after.toFixed(2) : after}\`\n`;
}
}
}
}
await writeFile("../../temp/patch-notes.md", notes);
+22 -6
View File
@@ -4,6 +4,10 @@ import { readdir, rm } from "fs/promises";
import { promisify } from "util"; import { promisify } from "util";
const MAX_COMMAND_LENGTH = 2 ** 11; const MAX_COMMAND_LENGTH = 2 ** 11;
const TS_PROTO_EXECUTABLE_LOCATIONS = [
"./node_modules/.bin/protoc-gen-ts_proto",
"../../node_modules/.bin/protoc-gen-ts_proto",
];
const roots = [ const roots = [
// "../../packages/core/src/protos", // "../../packages/core/src/protos",
@@ -12,12 +16,24 @@ const roots = [
const exec = promisify(execSync); const exec = promisify(execSync);
let executableFileExtension: string; let tsProtoExecutableLocation: string | undefined = undefined;
if (existsSync("node_modules/.bin/protoc-gen-ts_proto.exe")) { for (const location of TS_PROTO_EXECUTABLE_LOCATIONS) {
executableFileExtension = ".exe"; if (existsSync(location)) {
} else { tsProtoExecutableLocation = location;
executableFileExtension = ""; break;
} else if (existsSync(`${location}.exe`)) {
tsProtoExecutableLocation = `${location}.exe`;
break;
}
}
if (!tsProtoExecutableLocation) {
throw new Error(
`Could not find ts-proto executable. Checked locations: ${TS_PROTO_EXECUTABLE_LOCATIONS.join(
", "
)}`
);
} }
for (const root of roots) { for (const root of roots) {
@@ -32,7 +48,7 @@ for (const root of roots) {
while (files.length > 0) { while (files.length > 0) {
let command = [ let command = [
"protoc", "protoc",
`--plugin=./node_modules/.bin/protoc-gen-ts_proto${executableFileExtension}`, `--plugin=${tsProtoExecutableLocation}`,
"--ts_proto_opt=esModuleInterop=true", "--ts_proto_opt=esModuleInterop=true",
"--ts_proto_opt=oneof=unions-value", "--ts_proto_opt=oneof=unions-value",
// "--ts_proto_opt=removeEnumPrefix=true", // "--ts_proto_opt=removeEnumPrefix=true",
+21 -19
View File
@@ -4,22 +4,22 @@ import {
Reviews, Reviews,
Video, Video,
youtubers, youtubers,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { google } from 'googleapis'; import { google } from "googleapis";
import { cloneDeep, uniqBy } from 'lodash-es'; import { cloneDeep, uniqBy } from "lodash-es";
import { commitAssets } from './core/github/commitAssets'; import { AssetUploader } from "./core/github/assetUploader";
const MAX_QUERIES = 64; const MAX_QUERIES = 64;
console.log('Finding reviews...'); console.log("Finding reviews...");
const currentReviews = await fetch(asset('definitions/reviews.pb')) const currentReviews = await fetch(asset("definitions/reviews.pb"))
.then((response) => response.arrayBuffer()) .then((response) => response.arrayBuffer())
.then((buffer) => Reviews.decode(new Uint8Array(buffer))); .then((buffer) => Reviews.decode(new Uint8Array(buffer)));
const auth = await google.auth.getClient({ const auth = await google.auth.getClient({
scopes: ['https://www.googleapis.com/auth/youtube.readonly'], scopes: ["https://www.googleapis.com/auth/youtube.readonly"],
}); });
const youtube = google.youtube({ version: 'v3', auth }); const youtube = google.youtube({ version: "v3", auth });
const tankDefinitions = await fetchTankDefinitions(); const tankDefinitions = await fetchTankDefinitions();
const tanks = Object.values(tankDefinitions.tanks); const tanks = Object.values(tankDefinitions.tanks);
@@ -43,7 +43,7 @@ for (const tank of tanksSanitized) {
try { try {
const results = await youtube.search.list({ const results = await youtube.search.list({
part: ['snippet'], part: ["snippet"],
q: `World of Tanks Blitz ${tank.name}`, q: `World of Tanks Blitz ${tank.name}`,
}); });
@@ -54,9 +54,9 @@ for (const tank of tanksSanitized) {
(item) => (item) =>
item.snippet?.channelId && item.snippet?.channelId &&
item.id?.videoId && item.id?.videoId &&
youtubers.some(({ id }) => id === item.snippet?.channelId), youtubers.some(({ id }) => id === item.snippet?.channelId)
), ),
(item) => item.snippet!.channelId, (item) => item.snippet!.channelId
); );
reviews.reviews[tank.id] = { reviews.reviews[tank.id] = {
@@ -66,11 +66,11 @@ for (const tank of tanksSanitized) {
({ ({
id: item.id!.videoId!, id: item.id!.videoId!,
author: item.snippet!.channelId!, author: item.snippet!.channelId!,
}) satisfies Video, } satisfies Video)
), ),
}; };
console.log(`${++done} done; found ${items.length} for ${tank.name}`); console.log(`${++done} done; found ${items.length} for ${tank.name.locales.en}`);
await new Promise((resolve) => setTimeout(resolve, 1000 / 5)); await new Promise((resolve) => setTimeout(resolve, 1000 / 5));
} catch (error) { } catch (error) {
@@ -78,9 +78,11 @@ for (const tank of tanksSanitized) {
break; break;
} }
} }
await commitAssets('reviews', [
{ using uploader = new AssetUploader("reviews");
content: Reviews.encode(reviews).finish(),
path: 'definitions/reviews.pb', await uploader.add({
}, content: Reviews.encode(reviews).finish(),
]); path: "definitions/reviews.pb",
});
await uploader.flush();
+17 -17
View File
@@ -1,39 +1,39 @@
import decompress from 'decompress'; import decompress from "decompress";
import { existsSync } from 'fs'; import { existsSync } from "fs";
import { mkdir, readdir, writeFile } from 'fs/promises'; import { mkdir, readdir, writeFile } from "fs/promises";
if (!existsSync('../../temp')) { if (!existsSync("../../temp")) {
console.log('Creating temp directory...'); console.log("Creating temp directory...");
await mkdir('../../temp'); await mkdir("../../temp");
} }
console.log('Downloading depot downloader...'); console.log("Downloading depot downloader...");
await fetch( await fetch(
'https://api.github.com/repos/SteamRE/DepotDownloader/releases/latest', "https://api.github.com/repos/SteamRE/DepotDownloader/releases/latest"
) )
.then( .then(
(response) => (response) =>
response.json() as Promise<{ response.json() as Promise<{
assets: { browser_download_url: string; name: string }[]; assets: { browser_download_url: string; name: string }[];
}>, }>
) )
.then(({ assets }) => { .then(({ assets }) => {
const asset = assets.find(({ name }) => name.includes('linux-x64')); const asset = assets.find(({ name }) => name.includes("linux-x64"));
if (!asset) throw new Error('No asset found'); if (!asset) throw new Error("No asset found");
return fetch(asset.browser_download_url); return fetch(asset.browser_download_url);
}) })
.then((response) => response.arrayBuffer()) .then((response) => response.arrayBuffer())
.then((arrayBuffer) => .then((arrayBuffer) =>
writeFile('../../temp/depotDownloader.zip', new Uint8Array(arrayBuffer)), writeFile("../../temp/depotDownloader.zip", new Uint8Array(arrayBuffer))
); );
console.log('Unzipping...'); console.log("Unzipping...");
await decompress( await decompress(
'../../temp/depotDownloader.zip', "../../temp/depotDownloader.zip",
'../../temp/depotDownloader', "../../temp/depotDownloader"
); );
console.log( console.log(
'Download complete; files:', "Download complete; files:",
await readdir('../../temp/depotDownloader'), await readdir("../../temp/depotDownloader")
); );
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"include": ["src/**/*"] "include": ["src/**/*", "../core/src/blitz/readDVPL.ts"]
} }
Binary file not shown.
@@ -67,7 +67,9 @@ export const PrimaryArmorScene = memo(() => {
<group ref={turretContainer}> <group ref={turretContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith( const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}` `turret_${turretModelDefinition.model_id
.toString()
.padStart(2, "0")}`
); );
const isVisible = isCurrentTurret; const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
@@ -4,6 +4,7 @@ import {
resolvePenetrationCoefficient, resolvePenetrationCoefficient,
} from "@blitzkit/core"; } from "@blitzkit/core";
import { invalidate, useFrame } from "@react-three/fiber"; import { invalidate, useFrame } from "@react-three/fiber";
import type { QuicklimeEvent } from "quicklime";
import { useEffect } from "react"; import { useEffect } from "react";
import { import {
MeshBasicMaterial, MeshBasicMaterial,
@@ -21,6 +22,7 @@ import { jsxTree } from "../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from "../../../../stores/duel"; import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
import { Tankopedia } from "../../../../stores/tankopedia"; import { Tankopedia } from "../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent"; import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
import { transitionEvent } from "../../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import fragmentShader from "./shaders/fragment.glsl?raw"; import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from "./shaders/vertex.glsl?raw"; import vertexShader from "./shaders/vertex.glsl?raw";
import { spacedArmorRenderTarget } from "./target"; import { spacedArmorRenderTarget } from "./target";
@@ -72,6 +74,8 @@ export function PrimaryArmorSceneComponent({
resolution: { value: new Vector2() }, resolution: { value: new Vector2() },
spacedArmorBuffer: { value: null }, spacedArmorBuffer: { value: null },
spacedArmorDepth: { value: null }, spacedArmorDepth: { value: null },
opacity: { value: 0 },
}, },
]), ]),
}); });
@@ -154,6 +158,10 @@ export function PrimaryArmorSceneComponent({
if (!noInvalidate) invalidate(); if (!noInvalidate) invalidate();
} }
function handleTransitionEvent(event: QuicklimeEvent<number>) {
material.uniforms.opacity.value = event.data;
}
handleShellChange(); handleShellChange();
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration); handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
handleAdvancedHighlightingChange( handleAdvancedHighlightingChange(
@@ -164,6 +172,8 @@ export function PrimaryArmorSceneComponent({
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix); handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix); handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
transitionEvent.on(handleTransitionEvent);
const unsubscribes = [ const unsubscribes = [
Duel.on((state) => state.antagonist.shell, handleShellChange), Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange), Tankopedia.on((state) => state.customShell, handleShellChange),
@@ -184,12 +194,13 @@ export function PrimaryArmorSceneComponent({
(state) => state.antagonist.equipmentMatrix, (state) => state.antagonist.equipmentMatrix,
(equipment) => handleAntagonistEquipmentChange(equipment) (equipment) => handleAntagonistEquipmentChange(equipment)
), ),
() => transitionEvent.off(handleTransitionEvent),
]; ];
return () => { return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe()); unsubscribes.forEach((unsubscribe) => unsubscribe());
}; };
}); }, []);
useFrame(({ gl, camera }) => { useFrame(({ gl, camera }) => {
gl.getSize(material.uniforms.resolution.value).multiplyScalar( gl.getSize(material.uniforms.resolution.value).multiplyScalar(
@@ -1,12 +1,9 @@
precision mediump float; precision mediump float;
#define USE_FOG
#include <fog_pars_fragment>
varying vec3 vNormal; varying vec3 vNormal;
varying vec3 vViewPosition; varying vec3 vViewPosition;
uniform float opacity;
uniform float thickness; uniform float thickness;
uniform float penetration; uniform float penetration;
uniform float caliber; uniform float caliber;
@@ -119,5 +116,5 @@ void main() {
gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha); gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha);
} }
#include <fog_fragment> gl_FragColor.a *= opacity;
} }
@@ -1,9 +1,5 @@
precision mediump float; precision mediump float;
#define USE_FOG
#include <fog_pars_vertex>
varying vec3 vNormal; varying vec3 vNormal;
varying vec3 vViewPosition; varying vec3 vViewPosition;
@@ -14,6 +10,4 @@ void main() {
vNormal = normalMatrix * normal; vNormal = normalMatrix * normal;
gl_Position = projectionMatrix * mvPosition; gl_Position = projectionMatrix * mvPosition;
#include <fog_vertex>
} }
@@ -24,6 +24,7 @@ import {
} from "../../../core/blitzkit/modelTransform"; } from "../../../core/blitzkit/modelTransform";
import { discardClippingPlane } from "../../../core/three/discardClippingPlane"; import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
import { Tankopedia } from "../../../stores/tankopedia"; import { Tankopedia } from "../../../stores/tankopedia";
import { transitionEvent } from "../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import { ArmorType } from "./SpacedArmorScene"; import { ArmorType } from "./SpacedArmorScene";
import type { import type {
ArmorUserData, ArmorUserData,
@@ -108,10 +109,11 @@ export function StaticArmorSceneComponent({
() => () =>
new MeshBasicMaterial({ new MeshBasicMaterial({
color, color,
opacity, transparent: true,
transparent: opacity < 1,
depthWrite, depthWrite,
...(props.clip ? { clippingPlanes: [props.clip] } : {}), ...(props.clip ? { clippingPlanes: [props.clip] } : {}),
opacity: 0,
userData: { opacity0: opacity },
}), }),
[thickness] [thickness]
); );
@@ -121,6 +123,8 @@ export function StaticArmorSceneComponent({
color: color color: color
.clone() .clone()
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1), .multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
opacity: 0,
transparent: true,
}), }),
[thickness] [thickness]
); );
@@ -163,11 +167,22 @@ export function StaticArmorSceneComponent({
surfaceMaterial.needsUpdate = true; surfaceMaterial.needsUpdate = true;
} }
function handleTransitionEvent(event: QuicklimeEvent<number>) {
surfaceMaterial.opacity = surfaceMaterial.userData.opacity0 * event.data;
outlineMaterial.opacity = event.data;
surfaceMaterial.transparent = surfaceMaterial.opacity < 1;
outlineMaterial.transparent = outlineMaterial.opacity < 1;
}
transitionEvent.on(handleTransitionEvent);
const unsubscribes = [ const unsubscribes = [
Tankopedia.on( Tankopedia.on(
(state) => state.highlightArmor?.name, (state) => state.highlightArmor?.name,
handleHighlightArmor handleHighlightArmor
), ),
() => transitionEvent.off(handleTransitionEvent),
]; ];
if (props.clip) { if (props.clip) {
@@ -3,6 +3,7 @@ import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics"; import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
import { useLocale } from "../../hooks/useLocale"; import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral"; import { CompareEphemeral } from "../../stores/compareEphemeral";
import { CompareCellDirection } from "../CompareCell";
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell"; import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
import { CompareRow } from "./CompareRow"; import { CompareRow } from "./CompareRow";
import { CompareSectionTitle } from "./CompareSectionTitle"; import { CompareSectionTitle } from "./CompareSectionTitle";
@@ -79,7 +80,7 @@ export function Body({ stats }: BodyProps) {
<CompareRow <CompareRow
stats={stats} stats={stats}
name={strings.website.tools.tankopedia.characteristics.values.reload} name={strings.website.tools.tankopedia.characteristics.values.reload}
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
value={(stats) => value={(stats) =>
stats.shellReload ?? stats.shellReload ??
stats.shellReloads!.reduce((a, b) => a + b, 0) / stats.shellReloads!.reduce((a, b) => a + b, 0) /
@@ -95,7 +96,7 @@ export function Body({ stats }: BodyProps) {
<> <>
<CompareRow <CompareRow
stats={stats} stats={stats}
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
value="intraClip" value="intraClip"
decimals={2} decimals={2}
/> />
@@ -107,7 +108,7 @@ export function Body({ stats }: BodyProps) {
<CompareRow <CompareRow
stats={stats} stats={stats}
value="shellRicochet" value="shellRicochet"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={0} decimals={0}
/> />
<CompareRow stats={stats} value="shellVelocity" decimals={0} /> <CompareRow stats={stats} value="shellVelocity" decimals={0} />
@@ -116,48 +117,48 @@ export function Body({ stats }: BodyProps) {
<CompareRow <CompareRow
stats={stats} stats={stats}
value="aimTime" value="aimTime"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={2} decimals={2}
/> />
<CompareRow <CompareRow
stats={stats} stats={stats}
value="dispersion" value="dispersion"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3} decimals={3}
/> />
<CompareRow <CompareRow
stats={stats} stats={stats}
indent indent
value="dispersionMoving" value="dispersionMoving"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3} decimals={3}
/> />
<CompareRow <CompareRow
stats={stats} stats={stats}
indent indent
value="dispersionHullTraversing" value="dispersionHullTraversing"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3} decimals={3}
/> />
<CompareRow <CompareRow
stats={stats} stats={stats}
indent indent
value="dispersionTurretTraversing" value="dispersionTurretTraversing"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3} decimals={3}
/> />
<CompareRow <CompareRow
stats={stats} stats={stats}
indent indent
value="dispersionShooting" value="dispersionShooting"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3} decimals={3}
/> />
<CompareRow <CompareRow
stats={stats} stats={stats}
indent indent
value="dispersionGunDamaged" value="dispersionGunDamaged"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3} decimals={3}
/> />
<CompareRow stats={stats} value="gunDepression" decimals={1} /> <CompareRow stats={stats} value="gunDepression" decimals={1} />
@@ -186,21 +187,21 @@ export function Body({ stats }: BodyProps) {
<CompareRow <CompareRow
stats={stats} stats={stats}
value="hardTerrainCoefficientRaw" value="hardTerrainCoefficientRaw"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={2} decimals={2}
/> />
<CompareRow <CompareRow
stats={stats} stats={stats}
indent indent
value="mediumTerrainCoefficientRaw" value="mediumTerrainCoefficientRaw"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={2} decimals={2}
/> />
<CompareRow <CompareRow
stats={stats} stats={stats}
indent indent
value="softTerrainCoefficientRaw" value="softTerrainCoefficientRaw"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={2} decimals={2}
/> />
<CompareRow <CompareRow
@@ -246,7 +247,7 @@ export function Body({ stats }: BodyProps) {
<CompareRow <CompareRow
stats={stats} stats={stats}
value="fireChance" value="fireChance"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
display={(stats) => (stats.fireChance * 100).toFixed(0)} display={(stats) => (stats.fireChance * 100).toFixed(0)}
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)} deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
@@ -288,28 +289,28 @@ export function Body({ stats }: BodyProps) {
<CompareRow <CompareRow
stats={stats} stats={stats}
value="volume" value="volume"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={1} decimals={1}
/> />
<CompareRow <CompareRow
stats={stats} stats={stats}
indent indent
value="width" value="width"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={1} decimals={1}
/> />
<CompareRow <CompareRow
stats={stats} stats={stats}
indent indent
value="height" value="height"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={1} decimals={1}
/> />
<CompareRow <CompareRow
stats={stats} stats={stats}
indent indent
value="length" value="length"
deltaType="lowerIsBetter" deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={1} decimals={1}
/> />
</Table.Body> </Table.Body>
@@ -6,17 +6,17 @@ import {
import { Flex, IconButton, Table, Text } from "@radix-ui/themes"; import { Flex, IconButton, Table, Text } from "@radix-ui/themes";
import { useRef } from "react"; import { useRef } from "react";
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics"; import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
import { Var } from "../../core/radix/var";
import { useLocale } from "../../hooks/useLocale"; import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral"; import { CompareEphemeral } from "../../stores/compareEphemeral";
import { ComparePersistent } from "../../stores/comparePersistent"; import { ComparePersistent } from "../../stores/comparePersistent";
import { CompareCell, CompareCellDirection } from "../CompareCell";
import { StickyRowHeaderCell } from "../StickyRowHeaderCell"; import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
type CompareRowProps = { type CompareRowProps = {
display?: ( display?: (
member: Awaited<TankCharacteristics> member: Awaited<TankCharacteristics>
) => number | string | undefined; ) => number | string | undefined;
deltaType?: "higherIsBetter" | "lowerIsBetter"; deltaType?: CompareCellDirection;
decimals?: number; decimals?: number;
deltaNominalDisplay?: (delta: number) => string | number; deltaNominalDisplay?: (delta: number) => string | number;
indent?: boolean; indent?: boolean;
@@ -33,7 +33,7 @@ type CompareRowProps = {
export function CompareRow({ export function CompareRow({
display, display,
deltaType = "higherIsBetter", deltaType = CompareCellDirection.HIGHER_IS_BETTER,
decimals, decimals,
deltaNominalDisplay, deltaNominalDisplay,
indent, indent,
@@ -108,33 +108,18 @@ export function CompareRow({
{values.map((value, index) => { {values.map((value, index) => {
const delta = value - values[0]; const delta = value - values[0];
const deltaPercentage = value / values[0] - 1; const deltaPercentage = value / values[0] - 1;
const normalizedDeltaPercentage = Math.round(
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25)
);
const resolvedDisplayValue = display const resolvedDisplayValue = display
? display(stats[index]) ? display(stats[index])
: decimals === undefined : decimals === undefined
? value ? value
: value?.toFixed(decimals); : value?.toFixed(decimals);
return ( return (
<Table.Cell <CompareCell
key={index} key={index}
style={{ truth={values[0]}
backgroundColor: value={value}
index === 0 || direction={deltaType}
value === undefined ||
values[0] === undefined ||
value === values[0]
? undefined
: (
deltaType === "higherIsBetter"
? value > values[0]
: value < values[0]
)
? `color-mix(in srgb, ${Var("green-7")} ${normalizedDeltaPercentage}%, ${Var("green-3")})`
: `color-mix(in srgb, ${Var("red-7")} ${normalizedDeltaPercentage}%, ${Var("red-3")})`,
}}
> >
<Flex <Flex
align="center" align="center"
@@ -160,8 +145,8 @@ export function CompareRow({
deltaNominalDisplay deltaNominalDisplay
? deltaNominalDisplay(delta) ? deltaNominalDisplay(delta)
: decimals === undefined : decimals === undefined
? delta ? delta
: delta.toFixed(decimals) : delta.toFixed(decimals)
}`} }`}
) )
</Text> </Text>
@@ -179,7 +164,7 @@ export function CompareRow({
</> </>
)} )}
</Flex> </Flex>
</Table.Cell> </CompareCell>
); );
})} })}
</Table.Row> </Table.Row>
@@ -51,19 +51,17 @@ export function CompareTable({ stats }: CompareTableProps) {
variant="surface" variant="surface"
style={{ style={{
position: "absolute", position: "absolute",
width: "100%", maxWidth: "100%",
height: "100%", height: "100%",
borderRadius: 0, borderBottomRightRadius: 0,
borderBottomLeftRadius: 0,
}} }}
> >
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<StickyColumnHeaderCell width="0" style={{ height: "100%" }}> <StickyColumnHeaderCell width="0" style={{ height: "100%" }}>
<Flex <Flex
style={{ style={{ width: "100%", height: "100%" }}
width: "100%",
height: "100%",
}}
align="center" align="center"
justify="center" justify="center"
> >
@@ -107,8 +105,8 @@ export function CompareTable({ stats }: CompareTableProps) {
crewSkills[skill] === 0 crewSkills[skill] === 0
? Var("gray-11") ? Var("gray-11")
: permanentSkills.includes(skill) : permanentSkills.includes(skill)
? Var("crimson-11") ? Var("crimson-11")
: Var("amber-11"), : Var("amber-11"),
}} }}
/> />
))} ))}
@@ -102,7 +102,7 @@ export function TankCard({ index, tank }: TankCardProps) {
}, []); }, []);
return ( return (
<StickyColumnHeaderCell> <StickyColumnHeaderCell width="0">
<Flex direction="column" align="center" justify="between" gap="2"> <Flex direction="column" align="center" justify="between" gap="2">
<TankControl index={index} key={tank.id} slug={tank.slug} /> <TankControl index={index} key={tank.id} slug={tank.slug} />
@@ -0,0 +1,41 @@
import { Table } from "@radix-ui/themes";
import type { CellProps } from "@radix-ui/themes/components/table";
export enum CompareCellDirection {
HIGHER_IS_BETTER,
LOWER_IS_BETTER,
}
interface CompareCellProps extends CellProps {
value: number;
truth: number;
direction?: CompareCellDirection;
}
export function CompareCell({
value,
truth,
style,
direction = CompareCellDirection.HIGHER_IS_BETTER,
...props
}: CompareCellProps) {
const percentage = value / truth - 1;
const shiftedPercentage = Math.round(
Math.min(100, Math.abs(percentage) * 2 * 100 + 25)
);
return (
<Table.Cell
style={{
backgroundColor:
value === truth
? undefined
: (direction === CompareCellDirection.HIGHER_IS_BETTER) ===
value > truth
? `color-mix(in srgb, var(--green-7) ${shiftedPercentage}%, var(--green-3))`
: `color-mix(in srgb, var(--red-7) ${shiftedPercentage}%, var(--red-3))`,
}}
{...props}
/>
);
}
@@ -1,3 +1,5 @@
import { Box, Text } from "@radix-ui/themes";
import { Html } from "@react-three/drei";
import { memo, type ComponentProps } from "react"; import { memo, type ComponentProps } from "react";
interface Props extends ComponentProps<"spotLight"> { interface Props extends ComponentProps<"spotLight"> {
@@ -8,10 +10,20 @@ export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
return ( return (
<> <>
{debug && ( {debug && (
<mesh position={props.position}> <group position={props.position}>
<icosahedronGeometry args={[0.1, 2]} /> <mesh>
<meshBasicMaterial color={props.color} /> <icosahedronGeometry args={[0.1, 2]} />
</mesh> <meshBasicMaterial color={props.color} />
</mesh>
<Html center>
<Box pb="9">
<Text wrap="nowrap">
{props.intensity} @ {JSON.stringify(props.userData)}
</Text>
</Box>
</Html>
</group>
)} )}
<spotLight {...props} /> <spotLight {...props} />
@@ -1,20 +1,22 @@
import { Flex } from '@radix-ui/themes'; import { Flex } from "@radix-ui/themes";
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps'; import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { StickyTableRoot } from '../StickyTableRoot'; import { StickyTableRoot } from "../StickyTableRoot";
import { Header } from './Header'; import { Header } from "./Header";
import { Tanks } from './Tanks'; import { Tanks } from "./Tanks";
export function TankTable({ skeleton }: MaybeSkeletonComponentProps) { export function TankTable({ skeleton }: MaybeSkeletonComponentProps) {
return ( return (
<Flex justify="center" maxWidth="100%" flexGrow="1" position="relative"> <Flex justify="center" maxWidth="100%" flexGrow="1" position="relative">
<StickyTableRoot <StickyTableRoot
size={{ initial: '1', sm: '2' }} size={{ initial: "1", sm: "2" }}
variant="surface" variant="surface"
style={{ style={{
position: 'absolute', position: "absolute",
maxWidth: '100%', maxWidth: "100%",
maxHeight: '100%', maxHeight: "100%",
height: '100%', height: "100%",
borderBottomRightRadius: 0,
borderBottomLeftRadius: 0,
}} }}
> >
<Header /> <Header />
@@ -1,24 +1,26 @@
import { TankDefinition, formatCompact } from '@blitzkit/core'; import { TankDefinition, formatCompact } from "@blitzkit/core";
import { Table } from '@radix-ui/themes'; import { Table } from "@radix-ui/themes";
import { memo } from 'react'; import { memo } from "react";
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions'; import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio'; import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from "../../hooks/useLocale";
import { Performance } from '../../stores/performance'; import { Performance } from "../../stores/performance";
import { TankRowHeaderCell } from '../TankRowHeaderCell'; import { CompareCell, CompareCellDirection } from "../CompareCell";
import { TankRowHeaderCell } from "../TankRowHeaderCell";
interface TankRowProps { interface TankRowProps {
tank: TankDefinition; tank: TankDefinition;
average: Record<string, number>;
} }
const averageDefinitions = await awaitableAverageDefinitions; const averageDefinitions = await awaitableAverageDefinitions;
export const TankRow = memo<TankRowProps>( export const TankRow = memo<TankRowProps>(
({ tank }) => { ({ tank, average }) => {
const averages = averageDefinitions.averages[tank.id]; const averages = averageDefinitions.averages[tank.id];
const ratio = useAveragesExclusionRatio(); const ratio = useAveragesExclusionRatio();
const playerCountPeriod = Performance.use( const playerCountPeriod = Performance.use(
(state) => state.playerCountPeriod, (state) => state.playerCountPeriod
); );
const { locale } = useLocale(); const { locale } = useLocale();
@@ -26,59 +28,108 @@ export const TankRow = memo<TankRowProps>(
<Table.Row> <Table.Row>
<TankRowHeaderCell tank={tank} /> <TankRowHeaderCell tank={tank} />
<Table.Cell align="center"> <CompareCell
value={averages.mu.wins / averages.mu.battles}
truth={average.winrate}
align="center"
>
{((averages.mu.wins / averages.mu.battles) * 100).toFixed(1)}% {((averages.mu.wins / averages.mu.battles) * 100).toFixed(1)}%
</Table.Cell> </CompareCell>
<Table.Cell align="center"> <Table.Cell align="center">
{formatCompact( {formatCompact(
locale, locale,
Math.round(ratio * averages.samples[playerCountPeriod]), Math.round(ratio * averages.samples[playerCountPeriod])
)} )}
</Table.Cell> </Table.Cell>
<Table.Cell align="center"> <CompareCell
value={averages.mu.damage_dealt / averages.mu.battles}
truth={average.damage}
align="center"
>
{Math.round( {Math.round(
averages.mu.damage_dealt / averages.mu.battles, averages.mu.damage_dealt / averages.mu.battles
).toLocaleString(locale)} ).toLocaleString(locale)}
</Table.Cell> </CompareCell>
<Table.Cell align="center"> <CompareCell
value={averages.mu.survived_battles / averages.mu.battles}
truth={average.survival}
align="center"
>
{Math.round( {Math.round(
(averages.mu.survived_battles / averages.mu.battles) * 100, (averages.mu.survived_battles / averages.mu.battles) * 100
)} )}
% %
</Table.Cell> </CompareCell>
<Table.Cell align="center"> <CompareCell
value={averages.mu.xp / averages.mu.battles}
truth={average.xp}
align="center"
>
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString( {Math.round(averages.mu.xp / averages.mu.battles).toLocaleString(
locale, locale
)} )}
</Table.Cell> </CompareCell>
<Table.Cell align="center"> <CompareCell
value={averages.mu.frags / averages.mu.battles}
truth={average.kills}
align="center"
>
{(averages.mu.frags / averages.mu.battles).toFixed(2)} {(averages.mu.frags / averages.mu.battles).toFixed(2)}
</Table.Cell> </CompareCell>
<Table.Cell align="center"> <CompareCell
value={averages.mu.spotted / averages.mu.battles}
truth={average.spots}
align="center"
>
{(averages.mu.spotted / averages.mu.battles).toFixed(2)} {(averages.mu.spotted / averages.mu.battles).toFixed(2)}
</Table.Cell> </CompareCell>
<Table.Cell align="center"> <CompareCell
value={averages.mu.hits / averages.mu.shots}
truth={average.accuracy}
align="center"
>
{Math.round((averages.mu.hits / averages.mu.shots) * 100)}% {Math.round((averages.mu.hits / averages.mu.shots) * 100)}%
</Table.Cell> </CompareCell>
<Table.Cell align="center"> <CompareCell
value={averages.mu.shots / averages.mu.battles}
truth={average.shots}
align="center"
>
{(averages.mu.shots / averages.mu.battles).toFixed(1)} {(averages.mu.shots / averages.mu.battles).toFixed(1)}
</Table.Cell> </CompareCell>
<Table.Cell align="center"> <CompareCell
value={averages.mu.hits / averages.mu.battles}
truth={average.hits}
align="center"
>
{(averages.mu.hits / averages.mu.battles).toFixed(1)} {(averages.mu.hits / averages.mu.battles).toFixed(1)}
</Table.Cell> </CompareCell>
<Table.Cell align="center"> <CompareCell
value={averages.mu.damage_dealt / averages.mu.damage_received}
truth={average.damageRatio}
align="center"
>
{(averages.mu.damage_dealt / averages.mu.damage_received).toFixed(2)} {(averages.mu.damage_dealt / averages.mu.damage_received).toFixed(2)}
</Table.Cell> </CompareCell>
<Table.Cell align="center"> <CompareCell
value={averages.mu.damage_received / averages.mu.battles}
truth={average.damageReceived}
direction={CompareCellDirection.LOWER_IS_BETTER}
align="center"
>
{Math.round( {Math.round(
averages.mu.damage_received / averages.mu.battles, averages.mu.damage_received / averages.mu.battles
).toLocaleString(locale)} ).toLocaleString(locale)}
</Table.Cell> </CompareCell>
<Table.Cell align="center"> <CompareCell
value={averages.mu.capture_points / averages.mu.battles}
truth={average.capturePoints}
align="center"
>
{(averages.mu.capture_points / averages.mu.battles).toFixed(2)} {(averages.mu.capture_points / averages.mu.battles).toFixed(2)}
</Table.Cell> </CompareCell>
</Table.Row> </Table.Row>
); );
}, },
(prev, next) => prev.tank.id === next.tank.id, (prev, next) => prev.tank.id === next.tank.id
); );
@@ -1,17 +1,27 @@
import { formatCompact, type BlitzkitStats } from "@blitzkit/core";
import { Table } from "@radix-ui/themes"; import { Table } from "@radix-ui/themes";
import { times } from "lodash-es"; import { times } from "lodash-es";
import { Fragment, Suspense, useEffect, useMemo, useState } from "react"; import {
Fragment,
Suspense,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import usePromise from "react-promise-suspense"; import usePromise from "react-promise-suspense";
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions"; import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions"; import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
import { filterTanks } from "../../core/blitzkit/filterTanks"; import { filterTanks } from "../../core/blitzkit/filterTanks";
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
import { useLocale } from "../../hooks/useLocale";
import { Performance } from "../../stores/performance"; import { Performance } from "../../stores/performance";
import { TankFilters } from "../../stores/tankFilters"; import { TankFilters } from "../../stores/tankFilters";
import { TankPerformanceSort } from "../../stores/tankPerformanceSort"; import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps"; import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
import { RowLoader } from "./RowLoader"; import { RowLoader } from "./RowLoader";
import { TankRow } from "./TankRow"; import { TankRow } from "./TankRow";
import { Total } from "./Total";
const PREVIEW_COUNT = 10; const PREVIEW_COUNT = 10;
const DEFAULT_LOADED_ROWS = 25; const DEFAULT_LOADED_ROWS = 25;
@@ -145,9 +155,83 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
setLoadedRows(DEFAULT_LOADED_ROWS); setLoadedRows(DEFAULT_LOADED_ROWS);
}, [filters, sort]); }, [filters, sort]);
const playerCountPeriod = Performance.use((state) => state.playerCountPeriod);
const { strings, locale } = useLocale();
const ratio = useAveragesExclusionRatio();
const sum = useCallback(
(slice: (tank: BlitzkitStats) => number) => {
return tanksSorted.reduce(
(acc, tank) => acc + tank.samples[playerCountPeriod] * slice(tank.mu),
0
);
},
[tanksSorted]
);
const battles = sum(({ battles }) => battles);
const winrate = sum(({ wins }) => wins) / battles;
const damage = sum(({ damage_dealt }) => damage_dealt) / battles;
const survival = sum(({ survived_battles }) => survived_battles) / battles;
const xp = sum(({ xp }) => xp) / battles;
const kills = sum(({ frags }) => frags) / battles;
const spots = sum(({ spotted }) => spotted) / battles;
const hits = sum(({ hits }) => hits) / battles;
const shots = sum(({ shots }) => shots) / battles;
const accuracy = hits / shots;
const damageReceived =
sum(({ damage_received }) => damage_received) / battles;
const damageRatio = damage / damageReceived;
const capturePoints = sum(({ capture_points }) => capture_points) / battles;
const average = {
battles,
winrate,
damage,
survival,
xp,
kills,
spots,
hits,
shots,
accuracy,
damageReceived,
damageRatio,
capturePoints,
};
return ( return (
<Table.Body> <Table.Body>
<Total tanks={tanksSorted} /> <Table.Row>
<StickyRowHeaderCell style={{ overflow: "hidden", display: "flex" }}>
{strings.website.tools.performance.table.tanks.total}
</StickyRowHeaderCell>
<Table.Cell align="center">{(winrate * 100).toFixed(1)}%</Table.Cell>
<Table.Cell align="center">
{formatCompact(
locale,
ratio * averageDefinitions.samples[playerCountPeriod]
)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(damage).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(survival * 100).toFixed(1)}%
</Table.Cell>
<Table.Cell align="center">
{Math.round(xp).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{kills.toFixed(2)}</Table.Cell>
<Table.Cell align="center">{spots.toFixed(2)}</Table.Cell>
<Table.Cell align="center">{(accuracy * 100).toFixed(0)}%</Table.Cell>
<Table.Cell align="center">{shots.toFixed(1)}</Table.Cell>
<Table.Cell align="center">{hits.toFixed(1)}</Table.Cell>
<Table.Cell align="center">{damageRatio.toFixed(2)}</Table.Cell>
<Table.Cell align="center">
{Math.round(damageReceived).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{capturePoints.toFixed(2)}</Table.Cell>
</Table.Row>
{tanksSorted.slice(0, loadedRows).map((averages) => { {tanksSorted.slice(0, loadedRows).map((averages) => {
const tank = tankDefinitions.tanks[averages.id]; const tank = tankDefinitions.tanks[averages.id];
@@ -157,7 +241,7 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
return ( return (
<Fragment key={tank.id}> <Fragment key={tank.id}>
<Suspense fallback={<RowLoader />}> <Suspense fallback={<RowLoader />}>
<TankRow tank={tank} /> <TankRow average={average} tank={tank} />
</Suspense> </Suspense>
</Fragment> </Fragment>
); );
@@ -1,91 +0,0 @@
import {
AverageDefinitionsEntry,
formatCompact,
type BlitzkitStats,
} from "@blitzkit/core";
import { Table } from "@radix-ui/themes";
import { memo, useCallback } from "react";
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
import { useLocale } from "../../hooks/useLocale";
import { Performance } from "../../stores/performance";
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
export interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
id: number;
}
interface TotalProps {
tanks: AverageDefinitionsEntryWithId[];
}
const averageDefinitions = await awaitableAverageDefinitions;
export const Total = memo<TotalProps>(
({ tanks }) => {
const playerCountPeriod = Performance.use(
(state) => state.playerCountPeriod
);
const { strings, locale } = useLocale();
const ratio = useAveragesExclusionRatio();
const sum = useCallback(
(slice: (tank: BlitzkitStats) => number) => {
return tanks.reduce(
(acc, tank) => acc + tank.samples[playerCountPeriod] * slice(tank.mu),
0
);
},
[tanks]
);
const battles = sum(({ battles }) => battles);
const winrate = sum(({ wins }) => wins) / battles;
const damage = sum(({ damage_dealt }) => damage_dealt) / battles;
const survival = sum(({ survived_battles }) => survived_battles) / battles;
const xp = sum(({ xp }) => xp) / battles;
const kills = sum(({ frags }) => frags) / battles;
const spots = sum(({ spotted }) => spotted) / battles;
const hits = sum(({ hits }) => hits) / battles;
const shots = sum(({ shots }) => shots) / battles;
const accuracy = hits / shots;
const damageReceived =
sum(({ damage_received }) => damage_received) / battles;
const damageRatio = damage / damageReceived;
const capturePoints = sum(({ capture_points }) => capture_points) / battles;
return (
<Table.Row>
<StickyRowHeaderCell style={{ overflow: "hidden", display: "flex" }}>
{strings.website.tools.performance.table.tanks.total}
</StickyRowHeaderCell>
<Table.Cell align="center">{(winrate * 100).toFixed(1)}%</Table.Cell>
<Table.Cell align="center">
{formatCompact(
locale,
ratio * averageDefinitions.samples[playerCountPeriod]
)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(damage).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(survival * 100).toFixed(1)}%
</Table.Cell>
<Table.Cell align="center">
{Math.round(xp).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{kills.toFixed(2)}</Table.Cell>
<Table.Cell align="center">{spots.toFixed(2)}</Table.Cell>
<Table.Cell align="center">{(accuracy * 100).toFixed(0)}%</Table.Cell>
<Table.Cell align="center">{shots.toFixed(1)}</Table.Cell>
<Table.Cell align="center">{hits.toFixed(1)}</Table.Cell>
<Table.Cell align="center">{damageRatio.toFixed(2)}</Table.Cell>
<Table.Cell align="center">
{Math.round(damageReceived).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{capturePoints.toFixed(2)}</Table.Cell>
</Table.Row>
);
},
(prev, next) => prev.tanks === next.tanks
);
@@ -37,26 +37,26 @@ export function PlaylistActions() {
{strings.website.tools.playlist.shuffle} {strings.website.tools.playlist.shuffle}
</Button> </Button>
<Button
size={{ initial: "1", xs: "2" }}
variant="outline"
onClick={() => {
Playlist.mutate((draft) => {
draft.list = draft.list?.filter(
(entry) =>
(!entry.now ||
!entry.then ||
entry.now.battles === entry.then.battles) &&
!entry.checked
);
});
}}
>
{strings.website.tools.playlist.remove}
</Button>
{wargaming && ( {wargaming && (
<> <>
<Button
size={{ initial: "1", xs: "2" }}
variant="outline"
onClick={() => {
Playlist.mutate((draft) => {
draft.list = draft.list?.filter(
(entry) =>
(!entry.now ||
!entry.then ||
entry.now.battles === entry.then.battles) &&
!entry.checked
);
});
}}
>
{strings.website.tools.playlist.remove}
</Button>
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
<Button size={{ initial: "1", xs: "2" }}> <Button size={{ initial: "1", xs: "2" }}>
@@ -5,11 +5,7 @@ import {
TankType, TankType,
TIER_ROMAN_NUMERALS, TIER_ROMAN_NUMERALS,
} from "@blitzkit/core"; } from "@blitzkit/core";
import { import { LockClosedIcon, LockOpen2Icon } from "@radix-ui/react-icons";
ComponentBooleanIcon,
LockClosedIcon,
LockOpen2Icon,
} from "@radix-ui/react-icons";
import { import {
Box, Box,
Flex, Flex,
@@ -71,7 +67,9 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
<img <img
style={{ width: "1em", height: "1em" }} style={{ width: "1em", height: "1em" }}
src={asset( src={asset(
`icons/shells/${shellTypeIcons[shells[index]]}${premium ? "_premium" : ""}.webp` `icons/shells/${shellTypeIcons[shells[index]]}${
premium ? "_premium" : ""
}.webp`
)} )}
/> />
)} )}
@@ -107,7 +105,9 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
> >
<img <img
src={asset( src={asset(
`icons/shells/${shellTypeIcons[shellType]}${premium ? "_premium" : ""}.webp` `icons/shells/${shellTypeIcons[shellType]}${
premium ? "_premium" : ""
}.webp`
)} )}
style={{ width: "1em", height: "1em" }} style={{ width: "1em", height: "1em" }}
/> />
@@ -135,29 +135,13 @@ function OwnershipFilter(props: FlexProps) {
<IconButton <IconButton
size={size} size={size}
disabled={!wargaming} disabled={!wargaming}
variant={tankFilters.ownership === "all" ? "solid" : "soft"} variant={tankFilters.showOwned ? "solid" : "soft"}
radius="none" radius="none"
color={tankFilters.ownership === "all" ? undefined : "gray"} color={tankFilters.showOwned ? undefined : "gray"}
highContrast highContrast
onClick={() => { onClick={() => {
TankFilters.mutate((draft) => { TankFilters.mutate((draft) => {
draft.ownership = "all"; draft.showOwned = !draft.showOwned;
});
}}
>
<ComponentBooleanIcon />
</IconButton>
<IconButton
size={size}
disabled={!wargaming}
variant={tankFilters.ownership === "owned" ? "solid" : "soft"}
radius="none"
color={tankFilters.ownership === "owned" ? undefined : "gray"}
highContrast
onClick={() => {
TankFilters.mutate((draft) => {
draft.ownership = "owned";
}); });
}} }}
> >
@@ -167,13 +151,13 @@ function OwnershipFilter(props: FlexProps) {
<IconButton <IconButton
size={size} size={size}
disabled={!wargaming} disabled={!wargaming}
variant={tankFilters.ownership === "unowned" ? "solid" : "soft"} variant={tankFilters.showUnowned ? "solid" : "soft"}
radius="none" radius="none"
color={tankFilters.ownership === "unowned" ? undefined : "gray"} color={tankFilters.showUnowned ? undefined : "gray"}
highContrast highContrast
onClick={() => { onClick={() => {
TankFilters.mutate((draft) => { TankFilters.mutate((draft) => {
draft.ownership = "unowned"; draft.showUnowned = !draft.showUnowned;
}); });
}} }}
> >
@@ -579,27 +563,13 @@ export function FilterControl() {
> >
<IconButton <IconButton
size={size} size={size}
variant={tankFilters.testing === "include" ? "solid" : "soft"} variant={tankFilters.showNonTesting ? "solid" : "soft"}
radius="none" radius="none"
color={tankFilters.testing === "include" ? undefined : "gray"} color={tankFilters.showNonTesting ? undefined : "gray"}
highContrast highContrast
onClick={() => { onClick={() => {
TankFilters.mutate((draft) => { TankFilters.mutate((draft) => {
draft.testing = "include"; draft.showNonTesting = !draft.showNonTesting;
});
}}
>
<ComponentBooleanIcon />
</IconButton>
<IconButton
size={size}
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
radius="none"
color={tankFilters.testing === "exclude" ? undefined : "gray"}
highContrast
onClick={() => {
TankFilters.mutate((draft) => {
draft.testing = "exclude";
}); });
}} }}
> >
@@ -607,15 +577,16 @@ export function FilterControl() {
style={{ width: "1em", height: "1em", color: "currentColor" }} style={{ width: "1em", height: "1em", color: "currentColor" }}
/> />
</IconButton> </IconButton>
<IconButton <IconButton
size={size} size={size}
variant={tankFilters.testing === "only" ? "solid" : "soft"} variant={tankFilters.showTesting ? "solid" : "soft"}
radius="none" radius="none"
color={tankFilters.testing === "only" ? undefined : "gray"} color={tankFilters.showTesting ? undefined : "gray"}
highContrast highContrast
onClick={() => { onClick={() => {
TankFilters.mutate((draft) => { TankFilters.mutate((draft) => {
draft.testing = "only"; draft.showTesting = !draft.showTesting;
}); });
}} }}
> >
@@ -487,7 +487,7 @@ export const TankSearch = memo<TankSearchProps>(
)} )}
</Flex> </Flex>
{tankFilters.testing === "only" && ( {tankFilters.showTesting && !tankFilters.showNonTesting && (
<Flex justify="center" mt="4"> <Flex justify="center" mt="4">
<Callout.Root color="amber"> <Callout.Root color="amber">
<Callout.Icon> <Callout.Icon>
@@ -12,6 +12,7 @@ import {
EyeOpenIcon, EyeOpenIcon,
ShadowInnerIcon, ShadowInnerIcon,
ShadowNoneIcon, ShadowNoneIcon,
SunIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { import {
Button, Button,
@@ -55,7 +56,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const hasCustomShell = Tankopedia.use( const hasCustomShell = Tankopedia.use(
(state) => state.customShell !== undefined (state) => state.customShell !== undefined
); );
const display = Tankopedia.use((state) => state.display); const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const advancedHighlighting = TankopediaPersistent.use( const advancedHighlighting = TankopediaPersistent.use(
(state) => state.advancedHighlighting (state) => state.advancedHighlighting
@@ -73,6 +74,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const { strings, unwrap } = useLocale(); const { strings, unwrap } = useLocale();
const revealed = Tankopedia.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
return ( return (
<> <>
@@ -84,7 +86,9 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
gap="2" gap="2"
direction="column" direction="column"
top="50%" top="50%"
right={display === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"} right={
requestedDisplay === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"
}
style={{ style={{
position: "absolute", position: "absolute",
transform: "translateY(-50%)", transform: "translateY(-50%)",
@@ -296,7 +300,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
> >
{disturbed && ( {disturbed && (
<> <>
{display === TankopediaDisplay.DynamicArmor && ( {requestedDisplay === TankopediaDisplay.DynamicArmor && (
<IconButton <IconButton
size="2" size="2"
highContrast highContrast
@@ -349,6 +353,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
<IconButton
size="2"
highContrast
onClick={() => {
TankopediaPersistent.mutate((draft) => {
draft.highGraphics = !draft.highGraphics;
});
}}
color={highGraphics ? undefined : "gray"}
variant={highGraphics ? "solid" : "surface"}
>
<SunIcon />
</IconButton>
<ScreenshotButton <ScreenshotButton
color="gray" color="gray"
size="2" size="2"
@@ -384,11 +402,11 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Flex gap="3" align="center" mt="2"> <Flex gap="3" align="center" mt="2">
<SegmentedControl.Root <SegmentedControl.Root
variant="classic" variant="classic"
value={`${disturbed ? display : -1}`} value={`${disturbed ? requestedDisplay : -1}`}
onValueChange={(value) => { onValueChange={(value) => {
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.disturbed = true; draft.disturbed = true;
draft.display = Number(value); draft.requestedDisplay = Number(value);
}); });
}} }}
> >
@@ -430,7 +448,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</SegmentedControl.Item> </SegmentedControl.Item>
</SegmentedControl.Root> </SegmentedControl.Root>
{display === TankopediaDisplay.DynamicArmor && ( {requestedDisplay === TankopediaDisplay.DynamicArmor && (
<Dialog.Root <Dialog.Root
open={antagonistSelectorOpen} open={antagonistSelectorOpen}
onOpenChange={setAntagonistSelectorOpen} onOpenChange={setAntagonistSelectorOpen}
@@ -22,14 +22,12 @@ const poseDistances: Record<Pose, number> = {
const modelDefinitions = await awaitableModelDefinitions; const modelDefinitions = await awaitableModelDefinitions;
interface ControlsProps { interface ControlsProps {
naked?: boolean;
autoRotate?: boolean; autoRotate?: boolean;
zoomable?: boolean; zoomable?: boolean;
enableRotate?: boolean; enableRotate?: boolean;
} }
export function Controls({ export function Controls({
naked,
autoRotate = true, autoRotate = true,
zoomable = true, zoomable = true,
enableRotate = true, enableRotate = true,
@@ -79,11 +77,7 @@ export function Controls({
protagonistHullOrigin.y + protagonistHullOrigin.y +
protagonistTurretOrigin.y + protagonistTurretOrigin.y +
protagonistGunOrigin.y; protagonistGunOrigin.y;
const inspectModeInitialPosition = new Vector3( const inspectModeInitialPosition = new Vector3(-8, 2, -13);
-8,
naked ? gunHeight + 10 : 2,
-13
);
const t0 = useRef(Date.now() / 1000); const t0 = useRef(Date.now() / 1000);
useFrame(() => { useFrame(() => {
@@ -236,14 +230,10 @@ export function Controls({
orbitControls.current.rotateSpeed = 0.25; orbitControls.current.rotateSpeed = 0.25;
camera.position.copy(inspectModeInitialPosition); camera.position.copy(inspectModeInitialPosition);
orbitControls.current.target.set( orbitControls.current.target.set(0, gunHeight * 0.6, 0);
0,
gunHeight * (naked ? 1 / 4 : 3 / 4),
0
);
orbitControls.current.enablePan = true; orbitControls.current.enablePan = true;
orbitControls.current.enableZoom = true; orbitControls.current.enableZoom = true;
camera.fov = naked ? 20 : 25; camera.fov = 25;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
} }
@@ -1,40 +0,0 @@
import { invalidate, useThree } from "@react-three/fiber";
import { useEffect } from "react";
import type { Fog } from "three";
import { fogAnimationTime, fogFar0, fogFar1, forNear0, forNear1 } from "..";
import { Tankopedia } from "../../../../../../stores/tankopedia";
export function InitialFogReveal() {
const fog = useThree((state) => state.scene.fog as Fog | null);
useEffect(() => {
Tankopedia.mutate((draft) => {
draft.revealed = true;
});
if (!fog) return;
const t0 = Date.now();
const interval = setInterval(() => {
const t = (Date.now() - t0) / 1000;
const x = t / fogAnimationTime;
const near = forNear1 + (forNear0 - forNear1) * x;
const far = fogFar1 + (fogFar0 - fogFar1) * x;
fog.near = near;
fog.far = far;
invalidate();
if (x >= 1) {
fog.near = forNear0;
fog.far = fogFar0;
clearInterval(interval);
}
}, 1000 / 60);
return () => clearInterval(interval);
}, []);
return null;
}
@@ -1,84 +1,118 @@
import { useHelper } from "@react-three/drei"; import { useFrame, useThree } from "@react-three/fiber";
import { useFrame } from "@react-three/fiber";
import { times } from "lodash-es"; import { times } from "lodash-es";
import { useRef, useState } from "react"; import { Quicklime } from "quicklime";
import { Color, SpotLight, SpotLightHelper } from "three"; import { useEffect, useRef } from "react";
import { degToRad } from "three/src/math/MathUtils.js"; import { HemisphereLight, SpotLight, type Group } from "three";
import { isHalloween } from "../../../../../../core/blitzkit/isHalloween"; import { clamp, degToRad, lerp } from "three/src/math/MathUtils.js";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
import { HelpingSpotLight } from "../../../../../HelpingSpotLight"; import { HelpingSpotLight } from "../../../../../HelpingSpotLight";
const ANGLE = Math.PI * 2 ** -2;
const REVEAL_ANIMATION_TIME = 3;
const TRANSITION_ANIMATION_TIME = 0.5;
const LIGHTS_COUNT = 5; const LIGHTS_COUNT = 5;
const THETA_OFFSET = degToRad(-152);
const LIGHT_DISTANCE = 13; const LIGHT_DISTANCE = 13;
const LIGHT_HEIGHT_0 = 4; const LIGHT_HEIGHT_0 = 4;
const LIGHT_HEIGHT_1 = 2; const LIGHT_HEIGHT_1 = 2;
const INTENSITY = 50; const INTENSITY_0 = 3.5;
const INTENSITY_1 = 1.5;
const HEMISPHERE_INTENSITY = 2;
const N = 128; export const transitionEvent = new Quicklime<number>(0);
const TARGET_FRAME_RATE = 30;
export function Lighting() { export function Lighting() {
const [cast, setCast] = useState(true); const wrapper = useRef<Group>(null!);
const t0 = useRef(0); const t0 = useRef(0);
const topLight = useRef<SpotLight>(null!); const transition = useRef(false);
const frontLight = useRef<SpotLight>(null!); const clock = useThree((state) => state.clock);
const display = Tankopedia.use((state) => state.display);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const animationTime = useRef(REVEAL_ANIMATION_TIME);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
useHelper(topLight, SpotLightHelper, "red"); useEffect(() => {
useHelper(frontLight, SpotLightHelper, "green"); Tankopedia.mutate((draft) => {
draft.revealed = true;
});
const last = useRef<number>(0); t0.current = clock.elapsedTime - REVEAL_ANIMATION_TIME;
const samples = useRef<number[]>([]); }, []);
useFrame(({ clock, frameloop }) => { useFrame(({ invalidate }) => {
if (frameloop === "never") { if (transition.current) {
transition.current = false;
t0.current = clock.elapsedTime; t0.current = clock.elapsedTime;
samples.current = [];
return;
}
if (!cast || clock.elapsedTime - t0.current < TARGET_FRAME_RATE * N) {
return;
} }
const dt = clock.elapsedTime - last.current; const x = clamp(
last.current = clock.elapsedTime; (clock.elapsedTime - t0.current) / animationTime.current,
0,
2
);
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
const fps = 1 / dt; if (t !== transitionEvent.last) {
samples.current.push(fps); transitionEvent.dispatch(t);
}
if (samples.current.length > N) samples.current.shift(); for (const child of wrapper.current.children) {
if (child instanceof SpotLight) {
child.angle = ANGLE * t;
} else if (child instanceof HemisphereLight) {
child.intensity = HEMISPHERE_INTENSITY * t;
}
}
const avg = if (x < 2) invalidate();
samples.current.reduce((a, b) => a + b, 0) / samples.current.length;
if (avg < TARGET_FRAME_RATE) setCast(false); if (
x > 1 &&
Tankopedia.state.display !== Tankopedia.state.requestedDisplay
) {
Tankopedia.mutate((draft) => {
draft.display = requestedDisplay;
});
}
}); });
useEffect(() => {
if (requestedDisplay === display) return;
transition.current = true;
animationTime.current = TRANSITION_ANIMATION_TIME;
}, [requestedDisplay]);
return ( return (
<> <group ref={wrapper}>
<hemisphereLight intensity={0} color="#ffffff" groundColor="#afafaf" />
{times(LIGHTS_COUNT, (index) => { {times(LIGHTS_COUNT, (index) => {
const x = index / (LIGHTS_COUNT - 1); const x = index / (LIGHTS_COUNT - 1);
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) - degToRad(152); const theta = 2 * Math.PI * (index / LIGHTS_COUNT) + THETA_OFFSET;
const position = [ const position = [
LIGHT_DISTANCE * Math.sin(theta), LIGHT_DISTANCE * Math.sin(theta),
LIGHT_HEIGHT_0 * (1 - x) + LIGHT_HEIGHT_1 * x, lerp(LIGHT_HEIGHT_0, LIGHT_HEIGHT_1, x),
LIGHT_DISTANCE * Math.cos(theta), LIGHT_DISTANCE * Math.cos(theta),
] as const; ] as const;
const color = new Color(); const intensity = lerp(INTENSITY_0, INTENSITY_1, x);
if (isHalloween()) color.set(1, 0, 0);
return ( return (
<HelpingSpotLight <HelpingSpotLight
userData={{ index }}
key={index} key={index}
position={position} position={position}
intensity={INTENSITY} intensity={intensity}
penumbra={1} penumbra={1}
castShadow={cast} castShadow={highGraphics}
decay={1} decay={0}
color={color} color="#ffffff"
angle={0}
/> />
); );
})} })}
</> </group>
); );
} }
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more