Compare commits

..
Author SHA1 Message Date
tresabhi f355cb9f76 proper concurrency 2026-08-13 12:34:53 -05:00
tresabhi e42576b005 proper concurrency 2026-08-13 12:30:48 -05:00
tresabhi f68198475e install clis 2026-08-13 12:15:55 -05:00
tresabhi 896ae20dce remove pages 2026-08-13 12:06:55 -05:00
tresabhi b8fdc5424d logs 2026-08-13 12:02:49 -05:00
tresabhi 204c3eacc1 i give up 2026-08-13 12:01:16 -05:00
tresabhi 4d0f21e10b zip download 2026-08-13 11:05:01 -05:00
tresabhi d3202e8607 steam downloader 2026-08-13 10:56:15 -05:00
tresabhi a438eb704f smarter max rate 2026-08-13 10:29:11 -05:00
tresabhi 1569de4475 Merge branch 'main' of https://github.com/blitzkit/blitzkit 2026-08-12 20:33:40 -05:00
tresabhi 0f022f8295 chunking based on fetch rate 2026-08-12 20:33:38 -05:00
TrèsAbhi 75c21a5129 New Crowdin updates (#261) 2026-08-12 20:25:55 -05:00
tresabhi 5092d43e8e fix has pbr 2026-08-12 13:41:59 -05:00
TrèsAbhi 56d5d5e956 Delete CNAME 2026-08-12 13:10:52 -05:00
TrèsAbhi 70eca3293b Create CNAME 2026-08-12 13:10:42 -05:00
tresabhi 4f7ee54ef7 logs 2026-08-12 12:17:20 -05:00
TrèsAbhi 269df9586d Merge pull request #265 from blitzkit/infrastructure
Infrastructure
2026-08-12 11:47:24 -05:00
tresabhi 3152c18840 simplify robots.txt 2026-08-12 11:46:45 -05:00
tresabhi 52630596b8 finalize workflows 2026-08-12 11:44:26 -05:00
tresabhi 7e184b8be0 eh whatever, this'll make it into the next update 2026-08-12 11:36:55 -05:00
tresabhi 877ba9408d some more recording 2026-08-11 20:29:50 -05:00
tresabhi 8dc9a6b46d start of recording 2026-08-11 20:11:12 -05:00
tresabhi b3e439669b start of performance 2026-08-11 20:03:57 -05:00
tresabhi 08c909fbe2 rename ids to data-ids 2026-08-11 19:46:26 -05:00
tresabhi a44e2e9866 fix tank modes not switching 2026-08-11 19:45:09 -05:00
tresabhi bae2f253bc screenshot friendly contact shadows 2026-08-11 19:37:57 -05:00
tresabhi d485a463dd orange ground 2026-08-11 14:07:57 -05:00
tresabhi 3da1ed4d6c ground and sky color 2026-08-11 11:30:36 -05:00
tresabhi 051e9ec74a ground with shadows 2026-08-11 10:58:35 -05:00
tresabhi 486171f4db shadow focus and res 2026-08-11 10:52:33 -05:00
tresabhi 72de42a9ab performant ground 2026-08-11 10:38:36 -05:00
tresabhi 404e6625c0 remmove lghts 2026-08-11 10:35:41 -05:00
tresabhi 32e4bf3fd2 fading out fragment 2026-08-11 10:16:14 -05:00
tresabhi 0d1088fefb non pbr lihging 2026-08-11 10:01:34 -05:00
tresabhi 1a17d607fb move light target up 2026-08-11 09:56:58 -05:00
tresabhi dcda627518 dvpl free dir 2026-08-11 09:45:17 -05:00
tresabhi c130da7f0e heheheha 2026-08-10 17:50:48 -05:00
tresabhi e2df4d35b8 flip lighitng and add metalness 2026-08-10 16:58:40 -05:00
tresabhi 7739dbd0a3 faster lighting 2026-08-10 16:42:35 -05:00
tresabhi 9b7aa13d61 fix occlusion baking 2026-08-10 10:39:31 -05:00
TrèsAbhi 84ecaec5d3 base 2026-08-09 22:20:53 -05:00
tresabhi 01a5f1bd47 5hr 55min 2026-08-09 22:18:36 -05:00
tresabhi de0433ca02 Merge branch 'infrastructure' of https://github.com/blitzkit/blitzkit into infrastructure 2026-08-09 22:17:43 -05:00
TrèsAbhi cf76ae7136 add dispatch 2026-08-09 22:13:07 -05:00
tresabhi ea21636c4e abstract away some code 2026-08-09 20:26:39 -05:00
tresabhi dafa834d28 some script cleanup 2026-08-09 20:10:04 -05:00
tresabhi f1a1c78588 remove stub 2026-08-09 20:08:08 -05:00
tresabhi 14d16e5daf shell files raw debug 2026-08-09 19:50:53 -05:00
tresabhi b52c1fc2d9 ok cool 2026-08-09 16:12:26 -05:00
tresabhi f45b24f7ab 5hr 30min runtime 2026-08-09 15:05:06 -05:00
tresabhi 6ae175e5e0 reset author 2026-08-09 15:02:04 -05:00
tresabhi 0df27c38a1 ids token 2026-08-09 14:58:25 -05:00
tresabhi e27cc3f6ed github actions bot 2026-08-09 14:51:26 -05:00
tresabhi 968c3957d0 60s, just for testing 2026-08-09 14:46:35 -05:00
tresabhi 577d2323a8 checkout recursive 2026-08-09 14:45:12 -05:00
tresabhi 625c6cc649 workflow for discovering ids 2026-08-09 14:44:16 -05:00
tresabhi 9bc753b512 i was going to add verification but eh, no right now... 2026-08-09 14:39:05 -05:00
tresabhi fb34e6ce2f shell file paths list 2026-08-09 14:12:46 -05:00
tresabhi f882ea29e2 total saved 2026-08-09 14:06:40 -05:00
tresabhi 62f68f1f10 add manifests 2026-08-09 13:55:15 -05:00
tresabhi 8c9a41d72b add getStaticPaths for 404 page 2026-08-09 13:35:21 -05:00
tresabhi 5bda754b6e branch names 2026-08-08 20:56:45 -05:00
tresabhi c5bb537bf2 shell paths debug 2026-08-08 20:49:39 -05:00
tresabhi c59779bedb move 404 to pages chunk 2026-08-08 20:48:06 -05:00
tresabhi 5059e382b3 some missing aliases 2026-08-08 20:45:09 -05:00
tresabhi 953570461c safer exit 2026-08-08 14:09:48 -05:00
tresabhi 6d527a7170 save on exit 2026-08-08 14:08:35 -05:00
tresabhi ffd74de79f quiet all 2026-08-08 14:02:20 -05:00
tresabhi 7c9ef9e5e0 quiet git add 2026-08-08 13:59:47 -05:00
tresabhi cd10d9ab4a some stats 2026-08-08 13:57:35 -05:00
tresabhi 578d4a3ff2 max ids 2026-08-08 13:52:07 -05:00
tresabhi 0cd01fa258 try streak detector 2026-08-08 13:42:16 -05:00
tresabhi 21b37ced8d ids update 2026-08-08T18:33:52.481Z 2026-08-08 13:33:52 -05:00
tresabhi 81856276b0 seed basing 2026-08-08 13:15:04 -05:00
tresabhi 389052c692 1 sec bro 2026-08-08 13:01:29 -05:00
tresabhi 09edd39a06 more! 2026-08-08 12:28:45 -05:00
tresabhi 5c3077033e more modern way of handling id discovery 2026-08-07 14:33:10 -05:00
tresabhi ef84d5b3b5 don't log paths 2026-08-07 13:34:56 -05:00
tresabhi d29f91afbc don't fail fast for now 2026-08-07 13:33:28 -05:00
tresabhi ab87282103 use PUBLIC_ENVIRONMENT 2026-08-07 13:33:05 -05:00
tresabhi 408417ca68 bruh i am ignorant 2026-08-07 13:30:25 -05:00
tresabhi b171ce2011 beta environment as default for now 2026-08-07 12:46:49 -05:00
tresabhi 58d0d92d4f on push 2026-08-07 12:36:10 -05:00
tresabhi 754f39f623 environment based deploys 2026-08-07 12:34:58 -05:00
tresabhi 10c8ac95a4 move api 2026-08-06 20:10:29 -05:00
tresabhi b367866a22 revert debug env 2026-08-06 20:02:09 -05:00
tresabhi b26c5137f3 aliases 2026-08-06 20:01:50 -05:00
tresabhi 13cda7aec0 environment configs 2026-08-06 19:37:07 -05:00
tresabhi d1b8bfe244 fix imports 2026-08-06 14:29:13 -05:00
tresabhi 734db0957e flatten structure 2026-08-06 14:21:14 -05:00
tresabhi 050cb7a834 chunk typo 2026-08-05 20:17:05 -05:00
tresabhi 6dbcbc2afc relative game dir 2026-08-05 20:02:31 -05:00
tresabhi 484cb8fcb5 max downloads 64 2026-08-05 19:27:51 -05:00
tresabhi e09b407e61 force bun 2026-08-05 19:22:25 -05:00
tresabhi b8742bae66 fix linting errors 2026-08-05 14:17:33 -05:00
tresabhi af58b4614d master deploy 2026-08-05 14:09:00 -05:00
tresabhi db6c4679b7 fix app id 2026-08-05 13:50:25 -05:00
tresabhi 8169dfd85d no .env 2026-08-05 13:43:10 -05:00
tresabhi e6d7f78ea4 game dir 2026-08-05 13:38:50 -05:00
tresabhi f1f29b85b7 s'more fixes 2026-08-05 13:32:33 -05:00
tresabhi f587bc0983 fix legacy redirect 2026-08-05 13:30:20 -05:00
tresabhi 7ef4e0b9af envs 2026-08-05 13:29:55 -05:00
tresabhi b52919ebfe update matrix 2026-08-05 13:22:33 -05:00
tresabhi 84290b1c0e remove assets 2026-08-05 13:19:27 -05:00
tresabhi 24297dcae8 remove historic averages 2026-08-05 13:18:27 -05:00
tresabhi c7ee3dbf7e update changelogs 2026-08-05 13:16:32 -05:00
tresabhi dd769a9aa5 update settings and docs 2026-08-05 13:15:47 -05:00
tresabhi 645cfacecb update embed and fix varuna bug 2026-08-05 13:13:48 -05:00
tresabhi d2041d0ee6 update tier list 2026-08-05 13:05:31 -05:00
tresabhi ed7dd76ee5 update session 2026-08-05 13:04:11 -05:00
tresabhi bc2b9f1267 glossary 2026-08-05 13:01:51 -05:00
tresabhi 851a1108d2 some updates to gallery 2026-08-04 20:38:24 -05:00
tresabhi f09663e1c0 update guesser 2026-08-04 20:35:51 -05:00
tresabhi 6a05f11001 update mixer 2026-08-04 20:34:11 -05:00
tresabhi 36e8a20cd5 update playlist 2026-08-04 20:31:49 -05:00
tresabhi 7630cc52d3 update performance 2026-08-04 20:29:54 -05:00
tresabhi 23d2ca9a42 update compare 2026-08-04 20:28:54 -05:00
tresabhi 6e759e52c9 use small tank icons 2026-08-04 20:25:47 -05:00
tresabhi 85883cd2c5 faded flags 2026-08-04 20:23:50 -05:00
tresabhi 9e4d9c7968 tank model and collision 2026-08-04 20:18:05 -05:00
tresabhi 9968b2c83d small tank icons 2026-08-04 19:54:08 -05:00
tresabhi be0804ae59 booster icons 2026-08-04 19:52:06 -05:00
tresabhi 129a2c52f9 camo icon 2026-08-04 19:49:53 -05:00
tresabhi 996c9c05df game mode banners 2026-08-04 19:45:20 -05:00
tresabhi 36dbb24572 skill icons 2026-08-04 19:42:06 -05:00
tresabhi aaae8fb8ff module icons 2026-08-04 19:40:57 -05:00
tresabhi e5e747f7a0 equipment icons 2026-08-04 14:28:34 -05:00
tresabhi 487d149587 shell icons 2026-08-04 14:17:12 -05:00
tresabhi 48c4a4c76d currencies 2026-08-04 14:12:32 -05:00
tresabhi 33ebbbeb65 imports 2026-08-04 14:02:52 -05:00
tresabhi 1a7a1223d5 scratched flags 2026-08-04 13:46:19 -05:00
tresabhi 9813459310 tank icons 2026-08-04 13:44:19 -05:00
tresabhi 94c3608ca7 fix missing tank map 2026-08-04 13:35:51 -05:00
tresabhi 7e9be14a9a label all chunks 2026-08-04 13:22:44 -05:00
tresabhi 5f773931bc split giant class 2026-08-04 13:19:17 -05:00
tresabhi b2fd85a5b0 some api paths 2026-08-04 13:07:27 -05:00
tresabhi b060e65af5 dynamically import client api 2026-08-02 20:28:57 -05:00
tresabhi 8a3e57637d provision icons 2026-08-02 20:24:39 -05:00
tresabhi 90ad923672 consumable icons 2026-08-02 20:22:32 -05:00
tresabhi 59108d7637 update (4) 2026-08-01 15:08:33 -05:00
tresabhi f85d097c16 rename 2026-08-01 14:55:57 -05:00
tresabhi ed92440b94 update (3) 2026-08-01 14:54:40 -05:00
tresabhi d41d45abae update (2) 2026-08-01 14:48:18 -05:00
tresabhi 32a7db238b update (1) 2026-08-01 14:44:23 -05:00
tresabhi 2dc23ba09d some returns 2026-08-01 14:32:25 -05:00
tresabhi 30522bfda0 caching 2026-08-01 14:21:11 -05:00
tresabhi ed31f40876 (5) some implementation 2026-08-01 14:13:18 -05:00
tresabhi 6ad64c9bd9 (4) some implementation 2026-08-01 14:01:34 -05:00
tresabhi bd7c952478 (3) some implementation 2026-08-01 13:59:56 -05:00
tresabhi f7180f0e1a (2) some implementation 2026-08-01 13:53:00 -05:00
tresabhi b0611d7d72 (1) some implementation 2026-08-01 13:44:44 -05:00
tresabhi 244968bbbf (3) some definitions headers 2026-08-01 13:26:45 -05:00
tresabhi fd8448b0a6 (2) some definitions headers 2026-08-01 13:07:04 -05:00
tresabhi 17c54be1c8 some definitions headers 2026-08-01 12:56:12 -05:00
tresabhi 39745dd475 infrastructure for inline vfs and clients 2026-07-31 20:48:33 -05:00
tresabhi d81d885767 reintroduce assets temporarily 2026-07-31 20:30:35 -05:00
tresabhi 901cb0fcc8 remove assets 2026-07-31 20:30:19 -05:00
tresabhi 7ee49c4026 remove prisma 2026-07-31 20:28:18 -05:00
tresabhi 4cff06110a remove app 2026-07-31 20:27:39 -05:00
tresabhi 251d40b0f4 fix typo 2026-07-31 20:23:39 -05:00
tresabhi ad15f83abb move to chunks 2026-07-31 20:22:12 -05:00
tresabhi c2a5b742a7 clean up root 2026-07-31 20:21:27 -05:00
tresabhi 514999e588 remove bot 2026-07-31 20:17:57 -05:00
tresabhi 50d21918b4 update logs 2026-07-31 20:14:03 -05:00
tresabhi 43b59e4f02 update workflows to work with production as the deploy branch 2026-07-31 20:12:54 -05:00
tresabhi d0834ed572 update equipments 2026-07-31 19:48:03 -05:00
tresabhi caf238dd32 add robust slugs 2026-07-31 13:43:53 -05:00
tresabhi 0a7a220af3 logs 2026-07-31 11:23:45 -05:00
tresabhi 434d1ede3c use transliteration 2026-07-31 11:21:50 -05:00
tresabhi 0f47208f5e add support for missing vfs0 dirs 2026-07-31 11:07:54 -05:00
tresabhi cb2d795461 fix url 2026-07-11 18:14:22 -05:00
tresabhi 6e934e0ff1 logs 2026-07-11 18:13:02 -05:00
tresabhi 375adadbe5 Merge branch 'main' into dev 2026-07-11 18:04:36 -05:00
tresabhi 082a756f1b auto scroll on drag in tier list 2026-07-11 18:02:49 -05:00
tresabhi 9fc8a8e2a2 more generous pitch transition zones 2026-07-11 17:41:14 -05:00
tresabhi 1413475679 bigger floor and more aggressive light decay 2026-07-11 17:38:11 -05:00
tresabhi 86504e685e softer shadows 2026-07-11 17:33:37 -05:00
tresabhi 9df81044d1 increasing lighting angle to cover larger tanks 2026-07-11 17:29:44 -05:00
tresabhi c5fd7591ea complete lighting animation even if canvas is unfocused 2026-07-11 17:29:15 -05:00
TrèsAbhi cd8e43f945 Merge pull request #256 from Hitjack007/mixer-crashing 2026-07-11 17:17:11 -05:00
tresabhi 8df112aab4 type-safe disposal 2026-07-11 17:16:52 -05:00
TrèsAbhi 94c04ce102 Merge pull request #255 from Hitjack007/external-ricochets 2026-07-11 17:14:10 -05:00
TrèsAbhi 3a170202ca Merge pull request #254 from Hitjack007/he-crash-fix 2026-07-11 17:10:49 -05:00
tresabhi 46937693a0 implement nullable refs in a type-safe manner 2026-07-11 17:09:38 -05:00
TrèsAbhi b391d0b036 Merge pull request #253 from Hitjack007/static-armor-uncapitalised-fix 2026-07-11 17:06:29 -05:00
Mark Greene 1061e003f7 Fix Safari crash on mixer texture disposal 2026-07-10 21:30:21 +12:00
Mark Greene a2b573abca fix ricochets on external modules 2026-07-10 17:51:32 +12:00
Mark Greene 4571d141e6 Fix crash on repeated HE shell clicks in Tankopedia armor viewer 2026-07-10 16:03:05 +12:00
Mark Greene d70225b03c fix static armor plate title to use localized strings
Was rendering the raw lowercase layerTypeNames key instead of resolving it through strings.website.tools.tankopedia.sandbox.dynamic.shot_card.element, same pattern already used in ShotDisplayCard.
2026-07-10 15:41:46 +12:00
894 changed files with 36864 additions and 18097 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
indentSize: 2
+45
View File
@@ -0,0 +1,45 @@
name: Discover IDs
on:
push:
branches: production
schedule:
- cron: 0 */6 * * *
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
discover:
name: Discover IDs
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: production
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Setup Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Discover IDs
run: bun run discover-ids
env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
GH_TOKEN: ${{ secrets.IDS_TOKEN }}
+120 -46
View File
@@ -2,46 +2,98 @@ name: Website
on:
push:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
inputs:
branch:
description: Branch
type: choice
required: true
default: production
options:
- production
- main
environment:
description: Environment
type: choice
required: true
default: production
options:
- development
- beta
- production
- preview
- opentest
client_provider:
description: Client provider
type: choice
required: true
default: steam
options:
- steam
- zip
- wgpkg
client_source:
description: Client source
type: string
required: true
default: 444200/windows
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ inputs.environment || github.ref }}
cancel-in-progress: true
env:
# blitz
WOTB_SERVER: ${{ secrets.WOTB_SERVER }}
WOTB_VERSION: ${{ secrets.WOTB_VERSION }}
PUBLIC_WOTB_CLIENT: ../../temp/game
WOTB_USMAP: ../closed/blitz.usmap
# google
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
# steam
# client
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
STEAM_APP: 3341250
STEAM_OS: windows
STEAM_FINGERPRINT: Manifest_UFSFiles_Win64.txt
CLIENT_PROVIDER: ${{ inputs.client_provider || 'steam' }}
CLIENT_SOURCE: ${{ inputs.client_source || '444200/windows' }}
CLIENT_DIR: ../../temp/game
# debug
DOTNET_EnableDiagnostics: 1
# blitzkit
PUBLIC_ENVIRONMENT: ${{ inputs.environment || 'production' }}
PUBLIC_PROMOTE_OPENTEST: false
PUBLIC_STATIC_ASSET_BASE: https://assets.blitzkit.app
PUBLIC_GUIDE_WEBSITE: https://guidesblitz.com
# blitz
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
# google
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
# wargaming
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
jobs:
build:
name: Build chunk ${{ matrix.chunk }}
name: Build website
runs-on: ubuntu-latest
strategy:
fail-fast: false
fail-fast: true
matrix:
chunk: [pages, api, media, models, textures]
chunk: [default, api]
steps:
- name: Free disk space
@@ -50,46 +102,68 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch || 'production' }}
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: Set up Bun
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: latest
- name: Set up Dotnet
uses: actions/setup-dotnet@v5
with:
dotnet-version: 9.0.x
- name: Set up Protoc
- name: Setup protoc
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- name: Set up project
- name: Setup CLIs
run: |
bun run setup
./scripts/setup_depot_downloader.sh
sudo apt-get update
sudo apt-get install -y aria2 libarchive-tools
- name: Download Steam game
run: scripts/download_game.sh
- name: Install dependencies
run: bun install
- name: Enable core dumps
run: |
ulimit -c unlimited
- name: Download Client
run: bun run download-client
- name: Build
run: bun run build-website
run: bun run --bun build
env:
BUILD_CHUNK: ${{ matrix.chunk }}
CHUNK: ${{ matrix.chunk }}
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
id: upload
uses: actions/upload-artifact@v4
with:
name: chunk-${{ matrix.chunk }}
path: ${{ matrix.chunk == 'pages' && 'dist/website' || format('dist/website/{0}', matrix.chunk) }}
path: dist/website
- name: Trigger deployment
if: ${{ github.ref == 'refs/heads/production' }}
env:
TOKEN: ${{ secrets.GH_NODE_DEPLOY_TOKEN }}
CHUNK: ${{ matrix.chunk }}
run: |
ENV_FILE="environments/${PUBLIC_ENVIRONMENT}.json"
TARGET_REPO=$(jq -r ".nodes.\"$CHUNK\"" "$ENV_FILE")
if [ "$TARGET_REPO" == "null" ] || [ -z "$TARGET_REPO" ]; then
exit 1
fi
curl \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
"https://api.github.com/repos/$TARGET_REPO/dispatches" \
-d "{
\"event_type\": \"deploy-pages\",
\"client_payload\": {
\"source_repo\": \"${{ github.repository }}\",
\"run_id\": \"${{ github.run_id }}\",
\"artifact_id\": \"${{ steps.upload.outputs.artifact-id }}\",
\"chunk\": \"$CHUNK\"
}
}"
+2 -4
View File
@@ -1,7 +1,7 @@
node_modules
_node_modules
package-lock.json
*.env
.env
/test-results/
/playwright-report/
/playwright/.cache/
@@ -22,12 +22,11 @@ depots
public/sitemap*
/firebase.json
target
packages/website-ue/src/protos/*.ts
# packages/core/src/protos/*.ts
/.vercel
.astro
.vercel
/src
/packages/bot/src/prisma
dev-dist
# Logs
logs
@@ -53,4 +52,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
*.lscache
-3
View File
@@ -1,6 +1,3 @@
[submodule "packages/closed"]
path = packages/closed
url = https://github.com/blitzkit/closed.git
[submodule "packages/cue4parse"]
path = packages/cue4parse
url = https://github.com/tresabhi/CUE4Parse.git
-3
View File
@@ -1,3 +0,0 @@
{
"tabWidth": 2
}
+4
View File
@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}
+258
View File
@@ -0,0 +1,258 @@
{
"cSpell.words": [
"AABBOX",
"Abhi",
"absarc",
"accountinfo",
"adsense",
"aeson",
"alphatest",
"analyticsdata",
"analyticsreporting",
"APAC",
"apcr",
"appinfo",
"applypatches",
"artefacts",
"astrojs",
"astrolib",
"atgm",
"autocrop",
"Autoloader",
"Autoreloader",
"AYUV",
"battleresults",
"bbox",
"beforeinstallprompt",
"bepaint",
"betakrieg",
"BINORMAL",
"Binormals",
"bkni",
"BKRL",
"BKRTS",
"BKRTSMANIFEST",
"bkwd",
"blitzkit",
"blitzrinth",
"blitzstars",
"bufbuild",
"cacheable",
"Callouts",
"Camo",
"Carro",
"CCPAOPTOUT",
"CDON",
"clanid",
"classname",
"cleandist",
"closedkrieg",
"cockroackdb",
"Coef",
"Combattimento",
"cooldown",
"corelations",
"corsproxy",
"CUBEMAP",
"CUBETEXCOORD",
"customperiod",
"datasource",
"Dava",
"dbid",
"Ddpf",
"Ddsd",
"deadrail",
"debugflags",
"deburr",
"decalmask",
"decompressor",
"décor",
"dedup",
"defence",
"degressive",
"depotdownloader",
"devkrieg",
"diacriticless",
"DIDS",
"donotremove",
"drei",
"Droodles",
"dvpd",
"dvpl",
"DVPLCRC",
"dvpm",
"dxgi",
"easimon",
"echarts",
"Everytime",
"FASTNAME",
"favicons",
"flexdirection",
"fontsource",
"FOURCC",
"frameloop",
"fsformat",
"Fullscreen",
"fullstats",
"fuzzysort",
"gamemode",
"gameplay",
"glsl",
"GLTF",
"GSOR",
"gtag",
"haha",
"Herdbreaker",
"Hetz",
"highp",
"iife",
"imgur",
"immer",
"Inclusivity",
"JOINTINDEX",
"JOINTWEIGHT",
"kranvagn",
"krieg",
"Leaderboard",
"leaderboards",
"lerp",
"LINEARSIZE",
"linktarget",
"lmao",
"loadouts",
"loddist",
"lods",
"lossrate",
"Maddoxkkm",
"malum",
"mantlets",
"mediump",
"metallicness",
"metalness",
"minitelrose",
"Minotauro",
"MIPMAPCOUNT",
"mišek",
"mmnow",
"modeltransform",
"MURICA",
"nanostores",
"NEGATIVEX",
"NEGATIVEY",
"NEGATIVEZ",
"networkidle",
"NITROPAY",
"nivo",
"nmatname",
"noout",
"normalmap",
"oneof",
"oopsies",
"Oopsy",
"opentest",
"OPTML",
"otemp",
"outdir",
"outfile",
"ownedtanks",
"padder",
"partytown",
"patchnotes",
"PIXELFORMAT",
"playerachievements",
"playerinfo",
"playform",
"pngjs",
"POSITIVEX",
"POSITIVEY",
"POSITIVEZ",
"precompute",
"Promisable",
"proto",
"protobuf",
"protoc",
"protos",
"proxyclient",
"prunestrings",
"Pyogenics",
"ratingleaderboard",
"raycaster",
"Redirector",
"Relogin",
"researchable",
"Resvg",
"rizz",
"roboto",
"rtsc",
"RTSM",
"SCPG",
"scroller",
"searchclans",
"searchplayers",
"searchtanks",
"SHAREDEXP",
"SKLLD",
"sluggify",
"smoothstep",
"Smri",
"Specularity",
"SSAO",
"startcase",
"statkit",
"statsfull",
"steamapps",
"Stringifying",
"struct",
"structs",
"strvk",
"subnation",
"Synx",
"tankaverages",
"Tankerch",
"tankmen",
"tankopedia",
"tankopedias",
"tankstats",
"tauri",
"texconv",
"TEXCOORD",
"tresabhi",
"tungstentip",
"Turretless",
"TYPELESS",
"ubjson",
"Unicum",
"UNLOCALIZED",
"UNORM",
"unpickled",
"Unregistering",
"unrotate",
"unwrapper",
"userns",
"vercel",
"vickers",
"vitepress",
"Vovko",
"wargaming",
"webp",
"wgpkg",
"whitespaces",
"WHSS",
"winrate",
"wireframe",
"Woah",
"WOTB",
"wotblitz",
"wotbreplay",
"WOTINSPECTOR",
"xffn",
"xkdkw",
"XMLDVPL",
"YAMLDVPL",
"yolo",
"youtuber",
"youtubers",
"zoomable"
],
"typescript.tsdk": "node_modules\\typescript\\lib"
}
+31 -95
View File
@@ -1,105 +1,41 @@
# BlitzKit
Encyclopedia for World of Tanks Blitz.
Tools for everything and anything, World of Tanks Blitz.
## Development
This GitHub repository contains the code for BlitzKit and all its services. If you are here, looking to use BlitzKit, go to https://blitzkit.app/.
Setting up a local developer environment for BlitzKit is more complex than most projects, but this section will guide you through all the steps. If you're ever lost or confused, feel free to ask for help in [the official Discord server](https://discord.gg/nDt7AjGJQH).
If you intend to contribute to the code, note that you may need to check out sensitive, closed-source submodules to locally develop certain aspects of BlitzKit. Communicate with `@tresabhi` on Discord to get started or discuss sensitive content, such as environment variables or submodules.
### Prerequisites
## Packages
Before you touch any code, you'll need the following.
| Package | Title | Source | Memo |
| ------------------------------------ | -------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| [packages/core](packages/core) | | | Mutual internal code shared between all other packages. |
| [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/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. |
#### Linux
## Submodules
Because Node API for .NET doesn't resolve runtime-specific object files for libraries, the entirety of the BlitzKit project is "vendor-locked" to a single OS. GitHub Actions, responsible for BlitzKit CD/CI works best with Ubuntu, thus we must also use a Linux OS.
| Submodule | Title | Source | Memo |
| ------------------------------------------------------ | --------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [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. |
#### Windows (Optional)
## Environment Variables
...
#### Bun
https://bun.com/
BlitzKit is written mostly in TypeScript, with packages fetched from NPM, managed by Bun.
#### .NET
https://dotnet.microsoft.com/download/dotnet/9.0/
You'll need the .NET 9.0 SDK to build and run the interface layer between BlitzKit and your local installation of World of Tanks Blitz.
#### Protocol Buffers
https://protobuf.dev/installation/
Both BlitzKit and World of Tanks Blitz encode their APIs with Protocol Buffers.
#### World of Tanks Blitz
https://store.steampowered.com/app/444200/
You'll need an up-to-date installation of World of Tanks Blitz on your computer. You may use the same installation that you play, even on a Windows partition if you dual boot.
#### UE4SS
...
#### protodump
...
#### FModel (Optional)
...
### Set Up
Now we can start installing and initiating the code of BlitzKit.
#### Cloning
BlitzKit is a massive monorepo with a lot of submodules. You'll likely not need all the branches and commit history. So, clone with depth 1.
```bash
git clone --depth 1 https://github.com/blitzkit/blitzkit.git
cd blitzkit
git submodule update --init --recursive --depth 1
```
#### Proto Files
...
#### Mappings
...
#### Environment Variables
...
#### The Big One
...
### Modifying The Code
This is where you can start changing things and proposing them through pull requests.
#### Server
...
#### Building
...
#### Branches & Pull Requests
...
#### Updating
...
| 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_PROMOTE_OPENTEST` | | `false` | Optional boolean dictating whether the website advertises [the opentest subdomain of BlitzKit](https://opentest.blitzkit.app/). |
| `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_ANALYTICS_PROPERTY_ID` | [Google Analytics](https://analytics.google.com/) | `430711767` | Google Analytics' unique property ID for [BlitzKit's apex domain](https://blitzkit.app/) and subdomains. |
| `GOOGLE_APPLICATION_CREDENTIALS` | [Google Cloud](https://cloud.google.com/) | | Google Cloud's API key, primarily used for ranking popular tanks and finding relevant tank review videos in Tankopedia. |
| `PUBLIC_DISCORD_SERVER_ID` | [BlitzKit Discord Server](https://discord.gg/nDt7AjGJQH) | `734786591205359697` | BlitzKit Discord Server's id. |
| `PUBLIC_ASSET_BASE` | [blitzkit/assets](https://github.com/blitzkit/assets) | `https://blitzkit.github.io/assets` | The root domain and path that serves the external assets like tank models and icons. |
| `PUBLIC_GUIDE_WEBSITE` | [GuidesBlitz](https://guidesblitz.com/) | `https://guidesblitz.com` | The website that serves the encyclopedia-like content in BlitzKit. |
| `SERVICING` | | `false` | Wether the website is down for servicing. |
| `GH_TOKEN` | [GitHub Fine-Grained Tokens](https://github.com/settings/personal-access-tokens) | | The GitHub API key used for checking out private submodules and pushing content to the asset repository. |
| `DATABASE_URL` | [CockroachDB](https://www.cockroachlabs.com/) | | The CockroachDB-SQL database URL used to link Wargaming and Discord users. |
| `WOTB_DLC_CDN` | | | |
+10
View File
@@ -0,0 +1,10 @@
{
"folders": [
{
"path": ".",
},
{
"path": "C:/Program Files/WindowsApps/7458BE2C.WorldofTanksBlitz_11.1.256.0_x64__x4tje2y229k00/Data",
},
],
}
+536 -1315
View File
File diff suppressed because it is too large. Load diff
+3 -24
View File
@@ -1,27 +1,6 @@
{
"words": [
"gltf",
"amogos",
"Tankopedia",
"uasset",
"oodle",
"blitzkit",
"webp",
"hocs",
"cooldown",
"Lods",
"Skia",
"RMAO",
"Verts",
"drei",
"frameloop",
"pointermove",
"pointerup",
"pointerdown"
],
"userWords": [
"WOTB",
"USMAP",
"favicons"
"gamemodes",
"Tankopedia"
]
}
}
+20
View File
@@ -0,0 +1,20 @@
# BlitzKit 1.30.7
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features
- Dragging items in the Tier List now automatically scrolls the screen if you reach the top/bottom edge of the screen
## Fixes
- [Mark Greene] Clicking on the armor multiple times with HE no longer crashes BlitzKit
- [Mark Greene] Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration
- The lighting animation no longer stops prematurely when BlitzKit is unfocused
- The spot light angle has been increased to more evenly light up larger tanks
- BlitzKit once again uses soft shadows, reducing the pixelated shadows
- Fixed the weird patterns that formed on the tracks of tanks under self-shadows
- Increased the floor size and light decay to create the illusion that the floor is infinite
- [Mark Greene] Safari no longer crashes when unloading textures for unloaded tanks
- The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0
- [Mark Greene] Armor plate names in Static Armor are now properly capitalized in English and translated to other languages
+9
View File
@@ -0,0 +1,9 @@
# BlitzKit 1.30.8
Support for tanks with Chinese names.
## Fixes
- The URL generator for BlitzKit now handles names that have only Chinese characters
- The Mixed Virtual File System now handles non-existent directories correctly
- Updated equipments to match the game again
+40
View File
@@ -0,0 +1,40 @@
# BlitzKit 1.31.0
This update massively upgrades BlitzKit's graphical fidelity (while making it faster) and updates the way BlitzKit deploys updates, speeding up future versions.
## Features
- Improved the performance of the Tank Sandbox in Tankopedia
- Reduced the number of lights by 25%
- Reduced the resolution of shadows by 50%
- Improved the graphical fidelity of tanks in the Tankopedia
- Added soft contact shadows below tanks
- Added sky and ground reflected lighting to the top and bottom of tanks
- Removed the fog that used to darken tanks when zoomed far away
- Made BlitzKitLand™ fade out circularly to hide the sharp edges
- Pointed the lights 1.5m above the ground
- This genuinely makes BlitzKit almost photo-realistic in my opinion
- Non-PBR tanks are now lit up brighter to accommodate for their dark textures
## Fixes
- Fixed a bug where the ambient occlusion map (a measure of how dark a part of a tank gets due to low light bounces) was applied to the color of the tank directly, making many tanks appear dark even in direct light
- Tanks with chinese characters now generate urls through transliteration
## Known Issues
- The flags found behind the selected tank in the tech tree section of the Tankopedia is offset incorrectly
## Technical Features
- BlitzKit can now calculate and store performance statistics and account details for up 10,000,000,000 players
- This will help make the BlitzKit Performance tool more accurate, and play well with an upcoming, unannounced major feature
## Technical Changes
- The `production` branch now deploys updates to `blitzkit.app` instead of the `main` branch
- `robots.txt` now disables indexing of BlitzKit variants based on the environment
- The website is now compiled in "chunks" of which the `api` chunk now takes on the task of mangling what was previously stored in `assets3`
- `assets3` will be removed in `1.31.1`, please migrate to the new `api` if you use any BlitzKit assets
- Removed the `bot` and `app` packages
- Removed Prisma
+3
View File
@@ -0,0 +1,3 @@
# BlitzKit 1.31.1
This update adds capabilities to release BlitzKit OpenTest and Preview.
-11
View File
@@ -2,17 +2,6 @@
🎉 Services for everything and anything World of Tanks Blitz
## Bot
BlitzKit is available as a Discord bot and can be [installed very easily](/guide/invite).
- Track statistics
- Find inactive players in a clan
- Lookup owned tanks
- Create embeds for streams
- Track a player/tanks statistical evolution
- ...and a variety of other similar things
## Website
Provides a bunch of very helpful tools; https://blitzkit.app/
-3
View File
@@ -1,3 +0,0 @@
# Inviting the Bot
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
-13
View File
@@ -1,13 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.3.0",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-beta.blitzkit.app",
"api": "blitzkit/deploy-node-api.beta.blitzkit.app"
},
"aliases": {
"api": "https://api.beta.blitzkit.app",
"static": "https://static.beta.blitzkit.app"
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": "api"
},
"nodes": {},
"aliases": {
"api": "/api",
"static": "https://static.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-opentest.blitzkit.app",
"api": "blitzkit/deploy-node-api.opentest.blitzkit.app"
},
"aliases": {
"api": "https://api.opentest.blitzkit.app",
"static": "https://static.opentest.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-preview.blitzkit.app",
"api": "blitzkit/deploy-node-api.preview.blitzkit.app"
},
"aliases": {
"api": "https://api.preview.blitzkit.app",
"static": "https://static.preview.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": true,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-blitzkit.app",
"api": "blitzkit/deploy-node-api.blitzkit.app"
},
"aliases": {
"api": "https://api.blitzkit.app",
"static": "https://static.blitzkit.app"
}
}
+10 -7
View File
@@ -1,21 +1,24 @@
{
"name": "blitzkit",
"version": "2.0.0-beta.3",
"version": "1.31.1",
"license": "BlitzKit License",
"private": true,
"scripts": {
"setup": "chmod a+x scripts/*.sh && ./scripts/setup.sh",
"dev": "./scripts/dev.sh",
"build-game": "./scripts/build_game.sh",
"build-website": "./scripts/build_website.sh",
"discover-servers": "./scripts/discover_servers.sh"
"dev": "cd packages/website && bun run dev",
"build": "cd packages/website && bun run build",
"discover-ids": "cd packages/scripts && bun run discover-ids",
"build-protos": "cd packages/scripts && bun run build-protos",
"record-performance": "cd packages/scripts && bun run record-performance",
"download-client": "cd packages/scripts && bun run download-client"
},
"type": "module",
"workspaces": [
"packages/*"
],
"dependencies": {
"prettier": "^3.8.3",
"@gltf-transform/extensions": "^4.4.2",
"lz4js": "^0.2.0",
"sharp": "^0.34.5",
"typescript": "^5.9.3"
}
}
+3 -4
View File
@@ -8,12 +8,11 @@
"@bufbuild/protobuf": "^2.9.0",
"@octokit/plugin-throttling": "^11.0.1",
"@octokit/rest": "^22.0.0",
"@prisma/client": "^6.16.2",
"@types/adm-zip": "^0.5.7",
"@types/bun": "^1.3.1",
"@types/bun": "^1.3.14",
"@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1",
"@types/three": "^0.184.1",
"@types/three": "^0.180.0",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^17.2.2",
@@ -21,7 +20,7 @@
"lodash-es": "^4.17.21",
"pickleparser": "^0.2.1",
"protobufjs": "^7.5.4",
"three": "^0.184.0",
"three": "^0.180.0",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1"
}
+89
View File
@@ -0,0 +1,89 @@
import { assertSecret, EventManager } from "../blitzkit";
import { patientFetch } from "../blitzkit/patientFetch";
import type { Region } from "./regions";
const RETRY_ERRORS = ["REQUEST_LIMIT_EXCEEDED", "SOURCE_NOT_AVAILABLE"];
export const retryAbleBlitzFetchEvent = new EventManager();
export const blitzFetchQueueAvailableEvent = new EventManager();
type BlitzResponse<Data extends object> =
| {
status: "error";
error: { field: string; message: string; code: 402; value: null };
}
| {
status: "ok";
data: Data;
};
const MAX_CALLS_PER_SECOND = 10;
const queue: { url: string; resolve: (data: object) => void }[] = [];
let inProgress = 0;
function decrement() {
inProgress--;
blitzFetchQueueAvailableEvent.emit(undefined);
manageQueue();
}
async function manageQueue() {
if (queue.length > 0 && inProgress < MAX_CALLS_PER_SECOND) {
inProgress++;
const timeout = setTimeout(decrement, 1000);
const request = queue.shift()!;
const data = (await patientFetch(request.url)
.then((response) => response.json())
.catch((error) => ({ status: "error", error }))) as BlitzResponse<
typeof request.resolve
>;
if (data.status === "ok") {
request.resolve(data.data);
} else {
if (RETRY_ERRORS.includes(data.error.message)) {
retryAbleBlitzFetchEvent.emit(undefined);
clearTimeout(timeout);
setTimeout(() => {
queue.push(request);
decrement();
}, 1000 / MAX_CALLS_PER_SECOND);
} else {
clearTimeout(timeout);
decrement();
throw new Error(`Wargaming response error status: "${data.status}"`, {
cause: `Message: "${data.error.message}"\nURL: "${request.url}"`,
});
}
}
}
}
export type FetchBlitzParams = Record<string, string | number | undefined>;
export function fetchBlitz<Data extends object>(
region: Region,
path: string,
params: FetchBlitzParams = {}
) {
return new Promise<Data>((resolve) => {
queue.push({
url: `https://api.wotblitz.${region}/wotb/${path}/?${Object.entries({
application_id: assertSecret(
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
),
...params,
})
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => `${key}=${encodeURIComponent(value!)}`)
.join("&")}`,
resolve(data) {
resolve(data as Data);
},
});
manageQueue();
});
}
+11
View File
@@ -0,0 +1,11 @@
import { assertSecret } from "../blitzkit";
import type { BlitzGlossary } from "../types";
export async function fetchGlossary(locale: string) {
const response = await fetch(
`${assertSecret(
import.meta.env.WOTB_GLOSSARY_DOMAIN,
)}/${locale}${assertSecret(import.meta.env.WOTB_GLOSSARY_PATH)}`,
);
return (await response.json()) as BlitzGlossary;
}
@@ -0,0 +1,35 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import type { Region } from "./regions";
interface IndividualAccountAchievements {
achievements: {
[achievement: string]: number;
};
max_series: {
[achievement: string]: number;
};
}
export interface AccountAchievements {
[id: number]: IndividualAccountAchievements;
}
export async function getAccountAchievements<
Ids extends number | number[],
ReturnType = Ids extends number
? IndividualAccountAchievements
: IndividualAccountAchievements[],
>(region: Region, ids: Ids) {
const object = await fetchBlitz<AccountAchievements>(
region,
"account/achievements",
{ account_id: normalizeIds(ids) }
);
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
+96
View File
@@ -0,0 +1,96 @@
import { BlitzStats } from "../statistics";
import { FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import { Region } from "./regions";
export interface SupplementaryStats {
WN8?: number;
tier?: number;
}
export interface ClanStats {
spotted: number;
max_frags_tank_id: number;
hits: number;
frags: number;
max_xp: number;
max_xp_tank_id: number;
wins: number;
losses: number;
capture_points: number;
battles: number;
damage_dealt: number;
damage_received: number;
max_frags: number;
shots: number;
frags8p: number;
xp: number;
win_and_survived: number;
survived_battles: number;
dropped_capture_points: number;
}
export interface RatingStats {
spotted: number;
calibration_battles_left: number;
hits: number;
frags: number;
recalibration_start_time: number;
mm_rating: number;
wins: number;
losses: number;
is_recalibration: boolean;
capture_points: number;
battles: number;
damage_dealt: number;
damage_received: number;
shots: number;
frags8p: number;
xp: number;
win_and_survived: number;
survived_battles: number;
dropped_capture_points: number;
}
export interface IndividualAccountInfo {
account_id: number;
created_at: number;
updated_at: number;
private: null;
last_battle_time: number;
nickname: string;
statistics: {
all: BlitzStats;
clan: ClanStats;
rating?: RatingStats;
frags: null | number;
};
}
export interface AccountInfo {
[accountId: number]: IndividualAccountInfo;
}
export async function getAccountInfo<
Ids extends number | number[],
ReturnType = Ids extends number
? IndividualAccountInfo
: IndividualAccountInfo[],
>(
region: Region,
ids: Ids,
extra: string[] = [],
params: FetchBlitzParams = {}
) {
const object = await fetchBlitz<AccountInfo>(region, "account/info", {
account_id: normalizeIds(ids),
extra: extra.length === 0 ? undefined : extra.join(","),
...params,
});
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
@@ -0,0 +1,46 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import type { Region } from "./regions";
type IndividualClanAccountInfo = {
account_id: number;
joined_at: number;
clan_id: number;
role: string;
account_name: string;
clan?: {
members_count: number;
name: string;
created_at: number;
tag: string;
clan_id: number;
emblem_set_id: number;
};
} | null;
interface ClanAccountInfo {
[accountId: number]: IndividualClanAccountInfo;
}
export async function getClanAccountInfo<
Ids extends number | number[],
ReturnType = Ids extends number
? IndividualClanAccountInfo
: IndividualClanAccountInfo[],
>(region: Region, ids: Ids, extra: string[] = []) {
const object = await fetchBlitz<ClanAccountInfo>(
region,
"clans/accountinfo",
{
account_id: normalizeIds(ids),
extra: extra.length === 0 ? undefined : extra.join(","),
}
);
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
+51
View File
@@ -0,0 +1,51 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import type { Region } from "./regions";
interface IndividualClanInfo {
recruiting_options: {
vehicles_level: number;
wins_ratio: number;
average_battles_per_day: number;
battles: number;
average_damage: number;
};
members_count: number;
name: string;
creator_name: string;
clan_id: number;
created_at: number;
updated_at: number;
leader_name: string;
members_ids: number[];
recruiting_policy: string;
tag: string;
is_clan_disbanded: boolean;
old_name: string;
emblem_set_id: number;
creator_id: number;
motto: string;
renamed_at: number;
old_tag: null;
leader_id: number;
description: string;
}
interface ClanInfo {
[clanId: number]: IndividualClanInfo;
}
export async function getClanInfo<
Ids extends number | number[],
ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[],
>(region: Region, ids: Ids) {
const object = await fetchBlitz<ClanInfo>(region, "clans/info", {
clan_id: normalizeIds(ids),
});
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
@@ -1,5 +0,0 @@
import { LEAGUES } from "./rating";
export function getLeagueFromScore(score: number) {
return LEAGUES.find(({ minScore }) => score >= minScore)!;
}
-42
View File
@@ -1,42 +0,0 @@
import { context } from "../blitzkit/context";
import { patientFetch } from "../blitzkit/patientFetch";
import type { RatingLeague, RatingReward } from "./rating";
import type { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export type RatingInfo =
| {
detail: undefined;
title: string;
icon: string | null;
start_at: string;
finish_at: string;
current_season: number;
updated_at: string;
count: number;
rewards: RatingReward[];
leagues: RatingLeague[];
}
| { detail: { error: string } };
const cache: Partial<Record<Region, RatingInfo>> = {};
export async function getRatingInfo(region: Region) {
if (cache[region]) return cache[region]!;
const response = await patientFetch(
context === "website"
? `/api/${region}/rating/current/info`
: `https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
undefined
);
const data = (await response.json()) as RatingInfo;
cache[region] = data;
return data;
}
@@ -1,29 +0,0 @@
import { context } from "../blitzkit/context";
import { patientFetch } from "../blitzkit/patientFetch";
import type { RatingPlayer } from "../statistics";
import type { LeagueTop } from "./rating";
import type { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export async function getRatingLeague(
region: Region,
league: number,
usePatientFetcher = false
) {
const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: "no-store" })
: fetch(url, { cache: "no-store" });
const response = await fetcher(
context === "website"
? `/api/${region}/rating/current/league/${league}`
: `https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`
);
const json = await response.json();
return context === "website"
? (json as RatingPlayer[])
: (json as LeagueTop).result;
}
@@ -1,27 +0,0 @@
import { patientFetch } from "../blitzkit/patientFetch";
import { withCORSProxy } from "../blitzkit/withCORSProxy";
import type { RatingNeighbors } from "./rating";
import type { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export async function getRatingNeighbors(
region: Region,
id: number,
count: number,
usePatientFetcher = false
) {
const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: "no-store" })
: fetch(url, { cache: "no-store" });
const response = await fetcher(
withCORSProxy(
`https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`
)
);
const json = (await response.json()) as RatingNeighbors;
return json;
}
+16
View File
@@ -0,0 +1,16 @@
import type { TanksStats } from "../types";
import { type FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
import type { Region } from "./regions";
export async function getTankStats(
region: Region,
id: number,
params?: FetchBlitzParams
) {
const tankStats = await fetchBlitz<TanksStats>(region, "tanks/stats", {
account_id: id,
...params,
});
return tankStats[id];
}
+10 -6
View File
@@ -3,13 +3,16 @@ export * from "./clans";
export * from "./classes";
export * from "./constants";
export * from "./equipmentPrice";
export * from "./fetchBlitz";
export * from "./fetchGlossary";
export * from "./generateStats";
export * from "./getLeagueFromScore";
export * from "./getRatingInfo";
export * from "./getRatingLeague";
export * from "./getRatingNeighbors";
export * from "./getAccountAchievements";
export * from "./getAccountInfo";
export * from "./getClanAccountInfo";
export * from "./getClanInfo";
export * from "./getTankStats";
export * from "./idToRegion";
export * from "./isOnGoingRatingSeason";
export * from "./isValidBlitzId";
export * from "./nations";
export * from "./normalizeExtra";
export * from "./normalizeIds";
@@ -19,5 +22,6 @@ export * from "./readDVPL";
export * from "./regions";
export * from "./regionSubdomainToRegion";
export * from "./regionToRegionSubdomain";
export * from "./searchCurrentRatingPlayers";
export * from "./searchClansAcrossRegions";
export * from "./searchPlayersAcrossRegions";
export * from "./toUniqueId";
@@ -1,6 +0,0 @@
import { getRatingInfo } from "./getRatingInfo";
export async function isOnGoingRatingSeason() {
const ratingInfo = await getRatingInfo("com");
return ratingInfo.detail === undefined;
}
+32
View File
@@ -0,0 +1,32 @@
import { assertSecret } from "../blitzkit";
import { idToRegion } from "./idToRegion";
import { Region } from "./regions";
export async function isValidBlitzId(id: number, token?: string) {
let region: Region;
try {
region = idToRegion(id);
} catch (error) {
return false;
}
const json = await fetch(
`https://api.wotblitz.${region}/wotb/account/info/?account_id=${id}&application_id=${assertSecret(
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
)}${
token === undefined ? "" : `&access_token=${token}`
}&fields=account_id%2C-account_id`
).then(
(response) =>
response.json() as Promise<
| {
status: "ok";
data: Record<number, {} | null>;
}
| { status: "error" }
>
);
return json.status === "ok" && json.data[id] !== null;
}
@@ -0,0 +1,51 @@
import type { ClanList } from "./clans";
import { fetchBlitz } from "./fetchBlitz";
import type { Region } from "./regions";
export async function searchClansAcrossRegions(search: string, limit = 25) {
const trimmed = search.trim();
if (trimmed.length < 3 && trimmed.length > 100) return [];
const normalizedLimit = Math.floor(limit / 3);
// TODO: remove splice once wargaming fixes the functionality of the limit felid
return (
await Promise.all([
fetchBlitz<ClanList>("com", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "com" as Region,
}))
.splice(0, normalizedLimit)
),
fetchBlitz<ClanList>("eu", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "eu" as Region,
}))
.splice(0, normalizedLimit)
),
fetchBlitz<ClanList>("asia", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "asia" as Region,
}))
.splice(0, normalizedLimit)
),
])
)
.filter(Boolean)
.flat();
}
@@ -1,40 +0,0 @@
import { context } from "../blitzkit/context";
import { type Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
type CurrentRatingPlayerSearch = Record<
number,
{
spa_id: number;
nickname: string;
clan_tag: string;
} & (
| {
skip: true;
}
| {
skip: false;
mmr: number;
season_number: number;
calibrationBattlesLeft: number;
number: number;
updated_at: string;
neighbors: [];
}
)
>;
export async function searchCurrentRatingPlayers(region: Region, name: string) {
const encodedSearch = encodeURIComponent(name);
const response = await fetch(
context === "website"
? `/api/${region}/rating/current/search/${encodedSearch}`
: `https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`,
{ cache: "no-store" }
);
const accountList = (await response.json()) as CurrentRatingPlayerSearch;
return accountList;
}
@@ -0,0 +1,2 @@
export const usernamePattern = /^[a-zA-Z0-9_]{3,24}$/;
export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/;
@@ -0,0 +1,64 @@
import { fetchBlitz } from "../fetchBlitz";
import type { Region } from "../regions";
import { usernamePattern } from "./constants";
export interface AccountListItem {
nickname: string;
account_id: number;
}
export type AccountList = AccountListItem[];
export type AccountListWithServer = (AccountListItem & {
region: Region;
})[];
export async function searchPlayersAcrossRegions(search: string, limit = 25) {
const trimmed = search.trim();
if (trimmed.length < 3 && trimmed.length > 100) return [];
const normalizedLimit = Math.floor(limit / 3);
if (usernamePattern.test(trimmed)) {
return (
await Promise.all([
fetchBlitz<AccountList>("com", "account/list", {
search,
limit: normalizedLimit,
}).then(
(value) =>
value &&
value.map((account) => ({
...account,
region: "com" as Region,
}))
),
fetchBlitz<AccountList>("eu", "account/list", {
search,
limit: normalizedLimit,
}).then(
(value) =>
value &&
value.map((account) => ({
...account,
region: "eu" as Region,
}))
),
fetchBlitz<AccountList>("asia", "account/list", {
search,
limit: normalizedLimit,
}).then(
(value) =>
value &&
value.map((account) => ({
...account,
region: "asia" as Region,
}))
),
])
)
.filter(Boolean)
.flat() as AccountListWithServer;
} else {
return [] as AccountListWithServer;
}
}
export * from "./constants";
+13
View File
@@ -0,0 +1,13 @@
import { environmentConfig } from "@blitzkit/core";
export function alias(alias: string, path: string) {
const environment = environmentConfig();
if (!(alias in environment.aliases)) {
throw new Error(`Invalid alias "${alias}"`);
}
const base = environment.aliases[alias];
return `${base}${path}`;
}
+25
View File
@@ -0,0 +1,25 @@
import {
CamouflageDefinitions,
ConsumableDefinitions,
EquipmentDefinitions,
Gallery,
GameDefinitions,
MapDefinitions,
ModelDefinitions,
ProvisionDefinitions,
SkillDefinitions,
TankDefinitions,
} from "@blitzkit/core";
export abstract class BlitzKitAPI {
abstract skillDefinitions(): Promise<SkillDefinitions>;
abstract provisionDefinitions(): Promise<ProvisionDefinitions>;
abstract consumableDefinitions(): Promise<ConsumableDefinitions>;
abstract equipmentDefinitions(): Promise<EquipmentDefinitions>;
abstract mapDefinitions(): Promise<MapDefinitions>;
abstract modelDefinitions(): Promise<ModelDefinitions>;
abstract camouflageDefinitions(): Promise<CamouflageDefinitions>;
abstract tankDefinitions(): Promise<TankDefinitions>;
abstract gameDefinitions(): Promise<GameDefinitions>;
abstract galleryDefinitions(): Promise<Gallery>;
}
+69
View File
@@ -0,0 +1,69 @@
import { alias } from "@blitzkit/core";
import { fetchPB } from "../../protobuf";
import {
CamouflageDefinitions,
ConsumableDefinitions,
EquipmentDefinitions,
Gallery,
GameDefinitions,
MapDefinitions,
ModelDefinitions,
ProvisionDefinitions,
SkillDefinitions,
TankDefinitions,
} from "../../protos";
import { BlitzKitAPI } from "./base";
export class ClientBlitzKitAPI extends BlitzKitAPI {
gameDefinitions() {
return fetchPB(alias("api", "/definitions/game.pb"), GameDefinitions);
}
consumableDefinitions() {
return fetchPB(
alias("api", "/definitions/consumables.pb"),
ConsumableDefinitions,
);
}
tankDefinitions() {
return fetchPB(alias("api", "/definitions/tanks.pb"), TankDefinitions);
}
camouflageDefinitions() {
return fetchPB(
alias("api", "/definitions/camouflage.pb"),
CamouflageDefinitions,
);
}
provisionDefinitions() {
return fetchPB(
alias("api", "/definitions/provisions.pb"),
ProvisionDefinitions,
);
}
modelDefinitions() {
return fetchPB(alias("api", "/definitions/models.pb"), ModelDefinitions);
}
skillDefinitions() {
return fetchPB(alias("api", "/definitions/skills.pb"), SkillDefinitions);
}
equipmentDefinitions() {
return fetchPB(
alias("api", "/definitions/equipment.pb"),
EquipmentDefinitions,
);
}
galleryDefinitions() {
return fetchPB(alias("api", "/definitions/gallery.pb"), Gallery);
}
mapDefinitions() {
return fetchPB(alias("api", "/definitions/maps.pb"), MapDefinitions);
}
}
@@ -0,0 +1,304 @@
import {
BlitzCrewType,
BlitzModuleType,
BlitzStrings,
BlitzTankClass,
BlitzTankFilterDefinitionCategory,
CamouflagesXml,
CamouflagesYaml,
CombatRolesYaml,
ConsumablesCommon,
ConsumableTankCategoryFilterCategory,
CrewType,
I18nString,
MapsYaml,
ModuleType,
OptionalDevices,
OptionalDeviceSlots,
ProvisionsCommon,
ShellKind,
ShellType,
SquadBattleTypeStylesYaml,
TankClass,
TankmenAvatar,
toUniqueId,
VehicleDefinitionList,
} from "@blitzkit/core";
import { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import { parse as parseYaml } from "yaml";
import { BlitzKitAPI } from "../base";
export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
protected vfs: AbstractVFS;
protected nationSlugDiscriminators = {
china: "cn",
european: "eu",
france: "fr",
germany: "de",
japan: "jp",
other: "ot",
uk: "gb",
usa: "us",
ussr: "ru",
};
protected blitzModuleTypeToBlitzkit: Record<
keyof BlitzModuleType,
ModuleType
> = {
chassis: ModuleType.MODULE_TYPE_TRACKS,
engine: ModuleType.MODULE_TYPE_ENGINE,
gun: ModuleType.MODULE_TYPE_GUN,
turret: ModuleType.MODULE_TYPE_TURRET,
vehicle: ModuleType.MODULE_TYPE_VEHICLE,
};
protected blitzkitCrewTypeToBlitz: Record<CrewType, BlitzCrewType> = {
[CrewType.CREW_TYPE_COMMANDER]: "commander",
[CrewType.CREW_TYPE_DRIVER]: "driver",
[CrewType.CREW_TYPE_GUNNER]: "gunner",
[CrewType.CREW_TYPE_LOADER]: "loader",
[CrewType.CREW_TYPE_RADIOMAN]: "radioman",
};
protected blitzCrewTypeToBlitzkit: Record<BlitzCrewType, CrewType> = {
commander: CrewType.CREW_TYPE_COMMANDER,
driver: CrewType.CREW_TYPE_DRIVER,
gunner: CrewType.CREW_TYPE_GUNNER,
loader: CrewType.CREW_TYPE_LOADER,
radioman: CrewType.CREW_TYPE_RADIOMAN,
};
protected blitzTankClassToBlitzkit: Record<BlitzTankClass, TankClass> = {
lightTank: TankClass.TANK_CLASS_LIGHT,
"AT-SPG": TankClass.TANK_CLASS_TANK_DESTROYER,
heavyTank: TankClass.TANK_CLASS_HEAVY,
mediumTank: TankClass.TANK_CLASS_MEDIUM,
};
protected blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = {
ARMOR_PIERCING: ShellType.SHELL_TYPE_AP,
ARMOR_PIERCING_CR: ShellType.SHELL_TYPE_APCR,
HIGH_EXPLOSIVE: ShellType.SHELL_TYPE_HE,
HOLLOW_CHARGE: ShellType.SHELL_TYPE_HEAT,
};
protected blitzTankFilterDefinitionCategoryToBlitzkit: Record<
BlitzTankFilterDefinitionCategory,
ConsumableTankCategoryFilterCategory
> = {
clip: ConsumableTankCategoryFilterCategory.CONSUMABLE_TANK_CATEGORY_FILTER_CATEGORY_CLIP,
};
protected botPattern = /^.+((tutorial_bot(\d+)?)|(TU))$/;
protected stringsI18n: Record<string, Record<string, string>> = {};
protected nationsDir?: string[];
protected tankStringIdMap: Record<string, number> = {};
protected consumablesCommon: ConsumablesCommon = {};
protected provisionsCommon: ProvisionsCommon = {};
// TODO: suffix with "yaml"s and "xml"s
protected optionalDevices?: { root: OptionalDevices };
protected optionalDeviceSlots?: { root: OptionalDeviceSlots };
protected tankmenAvatar?: { root: TankmenAvatar };
protected mapsYaml?: MapsYaml;
protected camouflagesXml?: { root: CamouflagesXml };
protected camouflagesYaml?: CamouflagesYaml;
protected squadBattleTypeStyles?: SquadBattleTypeStylesYaml;
protected gameTypeSelectorStyles?: SquadBattleTypeStylesYaml;
protected combatRoles?: CombatRolesYaml;
protected tierEqualizer?: string[][];
constructor(vfs: AbstractVFS) {
super();
this.vfs = vfs;
}
async init() {
console.log("Initializing virtual file system...");
await this.vfs.init();
console.log("Fetching game data...");
this.nationsDir = await this.vfs
.dir("Data/XML/item_defs/vehicles")
.then((files) => files.filter((nation) => nation !== "common"));
this.optionalDevices = await this.vfs.xml<{ root: OptionalDevices }>(
"Data/XML/item_defs/vehicles/common/optional_devices.xml",
);
this.optionalDeviceSlots = await this.vfs.xml<{
root: OptionalDeviceSlots;
}>("Data/XML/item_defs/vehicles/common/optional_device_slots.xml");
this.tankmenAvatar = await this.vfs.xml<{ root: TankmenAvatar }>(
"Data/XML/item_defs/tankmen/avatar.xml",
);
this.mapsYaml = await this.vfs.yaml<MapsYaml>("Data/maps.yaml");
this.camouflagesXml = await this.vfs.xml<{ root: CamouflagesXml }>(
"Data/XML/item_defs/vehicles/common/camouflages.xml",
);
this.camouflagesYaml = await this.vfs.yaml<CamouflagesYaml>(
"Data/camouflages.yaml",
);
this.squadBattleTypeStyles = await this.vfs.yaml<SquadBattleTypeStylesYaml>(
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`,
);
this.gameTypeSelectorStyles =
await this.vfs.yaml<SquadBattleTypeStylesYaml>(
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`,
);
this.combatRoles = await this.vfs.yaml<CombatRolesYaml>(
`Data/XML/item_defs/vehicles/common/combat_roles.yaml`,
);
this.tierEqualizer = await this.vfs.csv(
// spell:disable-next-line
`Data/XML/item_defs/vehicles/common/tier_equializer.csv`,
{ delimiter: ";" },
);
console.log("Fetching game localizations...");
if (import.meta.env.DEV) {
locales.supported = [locales.supported[0]];
}
let fetchedLocalizations = 0;
await Promise.all(
locales.supported.map(async ({ locale }) => {
const blitzLocale = SUPPORTED_LOCALE_BLITZ_MAP[locale];
const cache = await fetch(
`https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml`,
)
.then((response) => response.text())
.then((string) => parseYaml(string) as BlitzStrings);
const preInstalled = await this.vfs.yaml<BlitzStrings>(
`Data/Strings/${blitzLocale}.yaml`,
);
this.stringsI18n[locale] = {
...cache,
...preInstalled,
};
console.log(
`Fetched localizations for ${locale} (${++fetchedLocalizations}/${locales.supported.length})`,
);
}),
);
console.log("Fetching consumable and provision data...");
for (const match of (
await this.vfs.text(
`Data/XML/item_defs/vehicles/common/consumables/list.xml`,
)
).matchAll(/<items path="(.+)\.xml"\/>/g)) {
if (match[1] === "prototypes") continue;
Object.assign(
this.consumablesCommon,
(
await this.vfs.xml<{ root: ConsumablesCommon }>(
`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`,
)
).root,
);
}
for (const match of (
await this.vfs.text(
`Data/XML/item_defs/vehicles/common/provisions/list.xml`,
)
).matchAll(/<items path="(.+)\.xml"\/>/g)) {
if (match[1] === "prototypes") continue;
Object.assign(
this.provisionsCommon,
(
await this.vfs.xml<{ root: ConsumablesCommon }>(
`Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`,
)
).root,
);
}
for (const nation of this.nationsDir!) {
const tankList = await this.vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
);
for (const tankKey in tankList.root) {
if (this.botPattern.test(tankKey)) continue;
const tank = tankList.root[tankKey];
const tankId = toUniqueId(nation, tank.id);
this.tankStringIdMap[`${nation}:${tankKey}`] = tankId;
}
}
return this;
}
protected getString(name: string) {
const collection: Record<string, string> = {
[locales.default]: this.stringsI18n[locales.default][name],
};
for (const { locale } of locales.supported) {
const localizedString = this.stringsI18n[locale][name];
if (
localizedString === undefined ||
localizedString === collection[locales.default]
) {
continue;
}
collection[locale] = localizedString;
}
return { locales: collection } satisfies I18nString;
}
}
export function Cache(disableInDev = false) {
const cache = new WeakMap<object, Map<string, Promise<unknown>>>();
return function <This, Arguments extends any[], Return>(
target: (this: This, ...args: Arguments) => Promise<Return>,
// TODO: evaluate if we need context to make the error trace easier
// context: ClassMethodDecoratorContext<
// This,
// (this: This, ...args: Arguments) => Promise<Return>
// >,
) {
return async function replacementMethod(this: This, ...args: Arguments) {
const key = args.join("-");
let thisCache: Map<string, Promise<Return>>;
if (cache.has(this as object)) {
thisCache = cache.get(this as object) as Map<string, Promise<Return>>;
} else {
thisCache = new Map();
cache.set(this as object, thisCache);
}
const disabled = disableInDev && import.meta.env.DEV;
if (thisCache.has(key) && !disabled) {
return thisCache.get(key)!;
}
const promise = (async () => {
try {
return await target.apply(this, args);
} catch (error) {
thisCache!.delete(key);
throw error;
}
})();
thisCache.set(key, promise);
return promise;
};
};
}
@@ -0,0 +1,67 @@
import { Avatar, fetchGlossary, Gallery } from "@blitzkit/core";
import locales from "@blitzkit/i18n/locales.json";
import { extname } from "path";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI9 } from "./9_gameDefinitions";
export abstract class ServerBlitzKitAPI10 extends ServerBlitzKitAPI9 {
@Cache()
async galleryDefinitions() {
const avatars: Record<string, { avatar: Avatar; url: string }> = {};
await Promise.all(
locales.supported.map(async (supported) => {
const glossary = await fetchGlossary(
supported.blitz ?? supported.locale,
);
console.log(
`Found ${Object.keys(glossary).length} things for ${supported.locale}`,
);
for (const key in glossary) {
const glossaryEntry = glossary[key];
if (
!key.startsWith("avatar") ||
key.endsWith("_part") ||
glossaryEntry.image_url === null
) {
continue;
}
if (key in avatars) {
avatars[key].avatar.name!.locales[supported.locale] =
glossaryEntry.title;
} else {
const extension = extname(glossaryEntry.image_url);
avatars[key] = {
url: glossaryEntry.image_url,
avatar: {
id: key,
name: { locales: { [supported.locale]: glossaryEntry.title } },
extension,
},
};
}
}
}),
);
const gallery: Gallery = { avatars: [] };
await Promise.all(
Object.entries(avatars).map(async ([key, { url, avatar }]) => {
if (!(locales.default in avatars[key].avatar.name!.locales)) {
console.warn(`Avatar ${key} has no ${locales.default} name`);
return;
}
gallery.avatars.push(avatar);
}),
);
return gallery;
}
}
@@ -0,0 +1,718 @@
import {
AssaultRanges,
BlitzCrewType,
BlitzModuleType,
BlitzTankClass,
ChassisDefinitionsList,
Crew,
EngineDefinitionsList,
Equalizer,
GunDefinition,
GunDefinitionsList,
ModuleType,
ResearchCost,
ShellDefinitionsList,
ShellKind,
sluggify,
TankDefinitions,
TankPrice,
TankPriceType,
TankType,
toUniqueId,
TurretDefinitionsList,
Unlock,
UnlocksListing,
VehicleDefinitionList,
VehicleDefinitions,
} from "@blitzkit/core";
import { Cache, ServerBlitzKitAPI0 } from "./0_base";
export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
private parseResearchCost(raw: number | string) {
if (typeof raw === "number") {
return {
research_cost_type: { $case: "xp", value: raw },
} satisfies ResearchCost;
} else {
return {
research_cost_type: {
$case: "seasonal_tokens",
value: {
season: Number(/prx_season_(\d+):\d+/.exec(raw)![1]),
tokens: Number(/prx_season_\d+:(\d+)/.exec(raw)![1]),
},
},
} satisfies ResearchCost;
}
}
@Cache()
async tankDefinitions() {
const tankDefinitions = TankDefinitions.create();
const gameModeNativeNames: Record<string, number> = {};
const squadBattleTypeGameModeNativeNameMatches =
this.squadBattleTypeStyles!.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g,
);
const gameTypeGameModeNativeNameMatches =
this.gameTypeSelectorStyles!.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/eGameMode\.([a-zA-Z]+) -> "~res:\/Gfx\/UI\/Hangar\/GameTypes\/battle-type_([^"]+)"/g,
);
for (const match of squadBattleTypeGameModeNativeNameMatches) {
const id = Number(match[1]);
gameModeNativeNames[match[3]] = id;
}
for (const match of gameTypeGameModeNativeNameMatches) {
Object.entries(gameModeNativeNames).forEach(([key, value]) => {
if (key.toLowerCase() === match[2].toLowerCase()) {
gameModeNativeNames[match[1]] = value;
}
});
}
const slugRequesters = new Map<string, { id: number; key: string }[]>();
const idToNation: Record<number, string> = {};
for (const nation of this.nationsDir!) {
const tankList = await this.vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
);
for (const tankKey in tankList.root) {
if (this.botPattern.test(tankKey)) continue;
const tank = tankList.root[tankKey];
const tankId = toUniqueId(nation, tank.id);
const name = (
(tank.shortUserString
? this.getString(tank.shortUserString)
: undefined) ?? this.getString(tank.userString)
).locales.en;
let slug = sluggify(name);
idToNation[tankId] = nation;
if (slugRequesters.has(slug)) {
slugRequesters.get(slug)!.push({ id: tankId, key: tankKey });
} else {
slugRequesters.set(slug, [{ id: tankId, key: tankKey }]);
}
}
}
const slugs = new Map<number, string>();
slugRequesters.forEach((requesters, slug) => {
if (requesters.length === 1) {
slugs.set(requesters[0].id, slug);
return;
}
console.warn(
`Multiple tanks share slug ${slug}: ${requesters
.map(({ key }) => key)
.join(", ")}`,
);
if (requesters.length !== 2) {
throw new Error("Unresolvable number of duplicates :(");
}
const nonCanonical = requesters.find(({ key }) => key.endsWith("TUR"));
if (nonCanonical === undefined) {
console.log("Using nations to discriminate");
// both are non-tutorial tanks, will have to discriminate using nation
requesters.forEach(({ id, key }) => {
const nation = idToNation[id];
const discriminator =
this.nationSlugDiscriminators[
nation as keyof typeof this.nationSlugDiscriminators
];
console.log(`Solution: ${key} -> ${slug}-${discriminator}`);
slugs.set(id, `${slug}-${discriminator}`);
});
} else {
console.log("Using tutorial bot suffix to discriminate");
const canonical = requesters.find(({ key }) => !key.endsWith("TUR"));
if (canonical === undefined) {
throw new Error(
"Two tutorial bots share the same slug? The world is truly broken.",
);
}
console.log(`Solution: ${canonical.key} -> ${slug}`);
slugs.set(canonical.id, slug);
console.log(`Solution: ${nonCanonical.key} -> ${slug}-tur`);
slugs.set(nonCanonical.id, `${slug}-tur`);
}
});
const tankXps = new Map<number, ResearchCost>();
for (const nation of this.nationsDir!) {
const tankList = await this.vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
);
const turretList = await this.vfs.xml<{
root: TurretDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/turrets.xml`);
const gunList = await this.vfs.xml<{
root: GunDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/guns.xml`);
const shellList = await this.vfs.xml<{
root: ShellDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/shells.xml`);
const enginesList = await this.vfs.xml<{
root: EngineDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/engines.xml`);
const chassisList = await this.vfs.xml<{
root: ChassisDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/chassis.xml`);
function resolveUnlocks(
blitzModuleTypeToBlitzkit: Record<keyof BlitzModuleType, ModuleType>,
unlocks?: BlitzModuleType,
) {
if (!unlocks) return [];
return Object.entries(unlocks)
.map(([type, list]) =>
(Array.isArray(list) ? list : [list]).map((item) => {
const typeTyped = type as keyof BlitzModuleType;
let rawId: number;
if (typeTyped === "chassis") {
rawId = chassisList.root.ids[item["#text"]];
} else if (typeTyped === "engine") {
rawId = enginesList.root.ids[item["#text"]];
} else if (typeTyped === "gun") {
rawId = gunList.root.ids[item["#text"]];
} else if (typeTyped === "turret") {
rawId = turretList.root.ids[item["#text"]];
} else if (typeTyped === "vehicle") {
rawId = tankList.root[item["#text"]].id;
}
return {
id: toUniqueId(nation, rawId!),
type: blitzModuleTypeToBlitzkit[typeTyped],
cost: {
type:
typeof item.cost === "number"
? "xp"
: item.cost.split(":")[0],
value:
typeof item.cost === "number"
? item.cost
: Number(item.cost.split(":")[1]),
},
} satisfies Unlock;
}),
)
.flat();
}
for (const tankKey in tankList.root) {
if (this.botPattern.test(tankKey)) continue;
const gunXps = new Map<number, ResearchCost>();
const turretXps = new Map<number, ResearchCost>();
const engineXps = new Map<number, ResearchCost>();
const trackXps = new Map<number, ResearchCost>();
const tank = tankList.root[tankKey];
let tankPrice: TankPrice;
const tankDefinition = await this.vfs.xml<{ root: VehicleDefinitions }>(
`Data/XML/item_defs/vehicles/${nation}/${tankKey}.xml`,
);
const tankId = toUniqueId(nation, tank.id);
const tankTags = tank.tags.split(" ");
const equipment = tankDefinition.root.optDevicePreset;
const slug = slugs.get(tankId);
if (slug === undefined) {
throw new Error(
`Could not find slug for ${nation}/${tankKey} (${tankId})`,
);
}
if (tank.sellPrice) {
tankPrice = {
type: TankPriceType.TANK_PRICE_TYPE_GOLD,
value: tank.sellPrice["#text"] * 2,
};
} else if (typeof tank.price === "number") {
tankPrice = {
type: TankPriceType.TANK_PRICE_TYPE_CREDITS,
value: tank.price,
};
} else {
tankPrice = {
type: TankPriceType.TANK_PRICE_TYPE_CREDITS,
value: tank.price["#text"] * 400,
};
}
const crew: Crew[] = [];
const fixedCamouflage = tankTags.includes("eventCamouflage_user");
const totalUnlocks: UnlocksListing[] = [];
for (const crewKey in tankDefinition.root.crew) {
const value = tankDefinition.root.crew[crewKey as BlitzCrewType];
let entry: Crew;
const index = crew.findIndex(
({ type }) => this.blitzkitCrewTypeToBlitz[type] === crewKey,
);
if (index === -1) {
if (crewKey === "#text") continue;
entry = {
type: this.blitzCrewTypeToBlitzkit[crewKey as BlitzCrewType],
count: 0,
substitute: [],
};
crew.push(entry);
} else {
entry = crew[index];
}
if (typeof value === "string") {
entry.count++;
if (value !== "") {
entry.substitute = value.split(/\n| /).map((member) => {
return this.blitzCrewTypeToBlitzkit[
member.trim() as BlitzCrewType
];
});
}
} else {
if (entry.count === undefined) {
entry.count = value.length;
} else {
entry.count += value.length;
}
}
}
const camouflages = Object.entries(
this.camouflagesXml!.root.camouflages,
)
.filter(([, camo]) => {
if (!camo.vehicleFilter.include) return false;
if (camo.unlockCostCategory !== "legendary-skins-gold")
return false;
const includeArray = Array.isArray(camo.vehicleFilter.include)
? camo.vehicleFilter.include
: [camo.vehicleFilter.include];
return includeArray.some((filter) => {
if ("vehicle" in filter && filter.vehicle?.name) {
return filter.vehicle.name === `${nation}:${tankKey}`;
}
return false;
});
})
.map(([, camo]) => camo.id);
let equalizerEntry = this.tierEqualizer!.find(
(line) => line[0] === `${nation}:${tankKey}`,
);
let equalizer: Equalizer | undefined;
if (equalizerEntry) {
const [health, penetration, module_health, damage, armor] =
equalizerEntry?.slice(1).map(Number);
equalizer = { health, penetration, module_health, damage, armor };
}
tankDefinitions.tanks[tankId] = {
ancestors: [],
successors: [],
id: tankId,
dev_name: tankKey,
roles: {},
camouflages: camouflages,
fixed_camouflage: fixedCamouflage,
crew,
weight: tankDefinition.root.hull.weight,
health: tankDefinition.root.hull.maxHealth,
speed_forwards: tankDefinition.root.speedLimits.forward,
speed_backwards: tankDefinition.root.speedLimits.backward,
equipment_preset:
typeof equipment === "string" ? equipment : equipment.at(-1)!,
max_consumables: tankDefinition.root.consumableSlots,
max_provisions: tankDefinition.root.provisionSlots,
name:
(tank.shortUserString
? this.getString(tank.shortUserString)
: undefined) ?? this.getString(tank.userString),
slug,
nation,
type: tankTags.includes("collectible")
? TankType.TANK_TYPE_COLLECTOR
: (typeof tank.price === "number" ? false : "gold" in tank.price)
? TankType.TANK_TYPE_PREMIUM
: TankType.TANK_TYPE_RESEARCHABLE,
tier: tank.level,
class: this.blitzTankClassToBlitzkit[tankTags[0] as BlitzTankClass],
testing: tankTags.includes("testTank"),
deprecated: tankTags.includes("deprecated"),
price: tankPrice,
camouflage_still: tankDefinition.root.invisibility.still,
camouflage_moving: tankDefinition.root.invisibility.moving,
camouflage_on_fire: tankDefinition.root.invisibility.firePenalty,
turrets: [],
engines: [],
tracks: [],
equalizer,
};
if (tank.combatRole) {
Object.entries(tank.combatRole).forEach(([gameMode, role]) => {
const id = Object.entries(gameModeNativeNames).find(
([key]) => key.toLowerCase() === gameMode.toLowerCase(),
)?.[1];
if (id === undefined) {
throw new Error(
`Unknown game mode in tank ${tankKey}: ${gameMode}`,
);
}
tankDefinitions.tanks[tankId].roles[id] =
this.combatRoles![role].id;
});
}
Object.keys(tankDefinition.root.chassis).forEach((key) => {
const track = tankDefinition.root.chassis[key];
const trackId = toUniqueId(nation, chassisList.root.ids[key]);
const terrainResistances = track.terrainResistance
.split(" ")
.map(Number);
totalUnlocks.push(track.unlocks);
tankDefinitions.tanks[tankId].tracks.push({
id: trackId,
weight: track.weight,
name: this.getString(track.userString),
traverse_speed: track.rotationSpeed,
dispersion_move: track.shotDispersionFactors.vehicleMovement,
dispersion_traverse: track.shotDispersionFactors.vehicleRotation,
resistance_hard: terrainResistances[0],
resistance_medium: terrainResistances[1],
resistance_soft: terrainResistances[2],
tier: track.level,
unlocks: resolveUnlocks(
this.blitzModuleTypeToBlitzkit,
track.unlocks,
),
});
});
Object.keys(tankDefinition.root.engines).forEach((engineKey) => {
const engine = tankDefinition.root.engines[engineKey];
const engineListEntry = enginesList.root.shared[engineKey];
const engineId = toUniqueId(nation, enginesList.root.ids[engineKey]);
totalUnlocks.push(engine.unlocks);
tankDefinitions.tanks[tankId].engines.push({
id: engineId,
name: this.getString(engineListEntry.userString),
fire_chance: engineListEntry.fireStartingChance,
tier: engineListEntry.level,
weight: engineListEntry.weight,
power: engineListEntry.power,
unlocks: resolveUnlocks(
this.blitzModuleTypeToBlitzkit,
engine.unlocks,
),
});
});
let turretIndex = 0;
for (const turretKey in tankDefinition.root.turrets0) {
const turret = tankDefinition.root.turrets0[turretKey];
const turretId = toUniqueId(nation, turretList.root.ids[turretKey]);
totalUnlocks.push(turret.unlocks);
tankDefinitions.tanks[tankId].turrets.push({
id: turretId,
traverse_speed: turret.rotationSpeed,
name: this.getString(turret.userString),
tier: turret.level,
guns: [],
health: turret.maxHealth,
view_range: turret.circularVisionRadius,
weight: turret.weight,
unlocks: resolveUnlocks(
this.blitzModuleTypeToBlitzkit,
turret.unlocks,
),
});
let gunIndex = 0;
for (const gunKey in turret.guns) {
const gun = turret.guns[gunKey];
const gunId = toUniqueId(nation, gunList.root.ids[gunKey]);
const gunListEntry = gunList.root.shared[gunKey];
const gunName = this.getString(gunListEntry.userString);
const gunType =
"clip" in gun
? gun.pumpGunMode
? "autoReloader"
: "autoLoader"
: "regular";
const gunClipCount = gunType === "regular" ? 1 : gun.clip!.count;
const shotDispersionFactors =
gun.shotDispersionFactors ?? gunListEntry.shotDispersionFactors;
let assault_ranges: AssaultRanges | undefined;
if (gun.extras?.trayShell) {
const types = gun.extras.trayShell.kinds
.split(" ")
.map((string) => {
const trimmed = string.trim();
if (trimmed in this.blitzShellKindToBlitzkit) {
return this.blitzShellKindToBlitzkit[trimmed as ShellKind];
}
throw new SyntaxError(`Invalid shell kind: ${trimmed}`);
});
const sectorNames = Object.keys(gun.extras.trayShell.sectors);
if (sectorNames.length !== 1 || sectorNames[0] !== "sector") {
throw new SyntaxError("Invalid tray shell sector");
}
const sector = gun.extras.trayShell.sectors.sector;
assault_ranges = {
types,
ranges: sector.map((value) => ({
factor: value.factor,
distance: value.distance,
})),
};
}
totalUnlocks.push(gun.unlocks);
tankDefinitions.tanks[tankId].turrets[turretIndex].guns.push({
id: gunId,
weight: gunListEntry.weight,
rotation_speed: gunListEntry.rotationSpeed,
name: gunName,
tier: gunListEntry.level,
shells: [],
camouflage_loss:
typeof gun.invisibilityFactorAtShot === "number"
? gun.invisibilityFactorAtShot
: gun.invisibilityFactorAtShot.at(-1)!,
aim_time: gun.aimingTime ?? gunListEntry.aimingTime,
dispersion_base:
gun.shotDispersionRadius ?? gunListEntry.shotDispersionRadius,
dispersion_damaged: shotDispersionFactors.whileGunDamaged,
dispersion_shot: shotDispersionFactors.afterShot,
dispersion_traverse: shotDispersionFactors.turretRotation,
unlocks: resolveUnlocks(
this.blitzModuleTypeToBlitzkit,
gun.unlocks,
),
shell_capacity: gun.maxAmmo ?? gunListEntry.maxAmmo,
assault_ranges,
burst:
gun.burst && gun.burst.count > 1
? {
count: gun.burst.count,
interval: 60 / gun.burst.rate,
}
: undefined,
gun_type:
gunType === "regular"
? {
$case: "regular",
value: {
reload: gun.reloadTime,
},
}
: gunType === "autoReloader"
? {
$case: "auto_reloader",
value: {
intra_clip: 60 / gun.clip!.rate,
shell_count: gunClipCount,
shell_reloads: gun
.pumpGunReloadTimes!.split(" ")
.map(Number),
},
}
: {
$case: "auto_loader",
value: {
intra_clip: 60 / gun.clip!.rate,
clip_reload: gun.reloadTime,
shell_count: gunClipCount,
},
},
} satisfies GunDefinition);
for (const shellKey in gunListEntry.shots) {
const gunShellEntry = gunListEntry.shots[shellKey];
const shell = shellList.root[shellKey];
const shellId = toUniqueId(nation, shell.id);
const shellName = this.getString(shell.userString);
const penetrationRaw = gunShellEntry.piercingPower
.split(" ")
.filter((penetrationString) => penetrationString !== "")
.map(Number);
tankDefinitions.tanks[tankId].turrets[turretIndex].guns[
gunIndex
].shells.push({
id: shellId,
name: shellName,
velocity: gunShellEntry.speed,
armor_damage: shell.damage.armor,
module_damage: shell.damage.devices,
caliber: shell.caliber,
normalization: shell.normalizationAngle,
ricochet: shell.ricochetAngle,
type: this.blitzShellKindToBlitzkit[shell.kind],
explosion_radius:
shell.kind === "HIGH_EXPLOSIVE"
? (shell.explosionRadius ?? 0)
: undefined,
icon: shell.icon,
penetration: {
near: penetrationRaw[0],
far: penetrationRaw[1],
},
range: gunShellEntry.maxDistance,
});
}
gunIndex++;
}
turretIndex++;
}
for (const unlocks of totalUnlocks) {
if (unlocks === undefined) continue;
for (const key in unlocks) {
const value = unlocks[key as keyof BlitzModuleType];
for (const vehicle of Array.isArray(value) ? value : [value]) {
switch (key as keyof BlitzModuleType) {
case "vehicle": {
const tankListEntry = tankList.root[vehicle["#text"]];
const currentTank = tankDefinitions.tanks[tankId];
const successorId = toUniqueId(nation, tankListEntry.id);
tankXps.set(
successorId,
this.parseResearchCost(vehicle.cost),
);
if (currentTank.successors === undefined) {
currentTank.successors = [];
}
if (!currentTank.successors!.includes(successorId)) {
currentTank.successors!.push(successorId);
}
break;
}
case "gun": {
gunXps.set(
toUniqueId(nation, gunList.root.ids[vehicle["#text"]]),
this.parseResearchCost(vehicle.cost),
);
break;
}
case "turret": {
turretXps.set(
toUniqueId(nation, turretList.root.ids[vehicle["#text"]]),
this.parseResearchCost(vehicle.cost),
);
break;
}
case "engine": {
engineXps.set(
toUniqueId(nation, enginesList.root.ids[vehicle["#text"]]),
this.parseResearchCost(vehicle.cost),
);
break;
}
case "chassis": {
trackXps.set(
toUniqueId(nation, chassisList.root.ids[vehicle["#text"]]),
this.parseResearchCost(vehicle.cost),
);
break;
}
}
}
}
}
Object.values(tankDefinitions.tanks[tankId].turrets).forEach(
(turret) => {
turret.research_cost = turretXps.get(turret.id);
Object.values(turret.guns).forEach((gunRaw) => {
gunRaw.research_cost = gunXps.get(gunRaw.id);
});
},
);
Object.values(tankDefinitions.tanks[tankId].engines).forEach(
(engine) => {
engine.research_cost = engineXps.get(engine.id);
},
);
Object.values(tankDefinitions.tanks[tankId].tracks).forEach((track) => {
track.research_cost = trackXps.get(track.id);
});
}
}
Object.values(tankDefinitions.tanks).forEach((tank) => {
tank.research_cost = tankXps.get(tank.id);
});
Object.values(tankDefinitions.tanks).forEach((tank) => {
tank.successors?.forEach((predecessorId) => {
if (
!tankDefinitions.tanks[predecessorId].ancestors?.includes(tank.id)
) {
tankDefinitions.tanks[predecessorId].ancestors?.push(tank.id);
}
});
});
return tankDefinitions;
}
}
@@ -0,0 +1,34 @@
import { CamouflageDefinitions } from "@blitzkit/core";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI1 } from "./1_tankDefinitions";
export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
@Cache()
async camouflageDefinitions() {
const camouflageDefinitions = CamouflageDefinitions.create();
for (const camoKey in this.camouflagesXml!.root.camouflages) {
const camo = this.camouflagesXml!.root.camouflages[camoKey];
const yamlEntry = this.camouflagesYaml![camoKey];
const fullName = yamlEntry.userString
? this.getString(yamlEntry.userString)
: undefined;
const shortName = yamlEntry.shortUserString
? this.getString(yamlEntry.shortUserString)
: undefined;
const resolvedTankName = shortName ?? fullName;
const resolvedTankNameFull =
resolvedTankName === fullName ? undefined : fullName;
camouflageDefinitions.camouflages[camo.id] = {
id: camo.id,
name: this.getString(camo.userString),
tank_name: resolvedTankName,
tank_name_full: resolvedTankNameFull,
};
}
return camouflageDefinitions;
}
}
@@ -0,0 +1,333 @@
import {
Armor,
BlitzCrewType,
ChassisDefinitionsList,
Crew,
EngineDefinitionsList,
GunDefinitionsList,
ModelDefinitions,
ResearchCost,
TankParameters,
toUniqueId,
TurretDefinitionsList,
Vector3,
VehicleDefinitionArmor,
VehicleDefinitionList,
VehicleDefinitions,
} from "@blitzkit/core";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI2 } from "./2_camouflageDefinitions";
import { parse as parsePath } from "path";
import { Vector3Tuple } from "three";
export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
private vector3TupleToBlitzkit(tuple: Vector3Tuple) {
return { x: tuple[0], y: tuple[1], z: tuple[2] } satisfies Vector3;
}
private assignArmor(
raw: VehicleDefinitionArmor[string],
id: number,
armor: Armor,
) {
if (typeof raw === "number") {
armor.thickness[id] = raw;
} else if (Array.isArray(raw)) {
armor.thickness[id] = raw.at(-1)!;
} else {
if (!armor.spaced) armor.spaced = [];
armor.thickness[id] = raw["#text"];
armor.spaced.push(id);
}
}
@Cache()
async modelDefinitions() {
const modelDefinitions = ModelDefinitions.create();
for (const nation of this.nationsDir!) {
const tankList = await this.vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
);
const turretList = await this.vfs.xml<{
root: TurretDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/turrets.xml`);
const gunList = await this.vfs.xml<{
root: GunDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/guns.xml`);
const enginesList = await this.vfs.xml<{
root: EngineDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/engines.xml`);
const chassisList = await this.vfs.xml<{
root: ChassisDefinitionsList;
}>(`Data/XML/item_defs/vehicles/${nation}/components/chassis.xml`);
for (const tankKey in tankList.root) {
if (this.botPattern.test(tankKey)) continue;
const gunXps = new Map<number, ResearchCost>();
const turretXps = new Map<number, ResearchCost>();
const engineXps = new Map<number, ResearchCost>();
const trackXps = new Map<number, ResearchCost>();
const tank = tankList.root[tankKey];
const tankDefinition = await this.vfs.xml<{ root: VehicleDefinitions }>(
`Data/XML/item_defs/vehicles/${nation}/${tankKey}.xml`,
);
const tankParameters = await this.vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
);
const turretOrigin = tankDefinition.root.hull.turretPositions.turret
.split(" ")
.map(Number) as Vector3Tuple;
const tankId = toUniqueId(nation, tank.id);
const hullArmor: Armor = { spaced: [], thickness: {} };
this.tankStringIdMap[`${nation}:${tankKey}`] = tankId;
Object.keys(tankDefinition.root.hull.armor)
.filter((name) => name.startsWith("armor_"))
.forEach((name) => {
const armorIdString = name.match(/armor_(\d+)/)?.[1];
if (armorIdString === undefined) {
throw new SyntaxError(`Invalid armor id: ${name}`);
}
const armorId = parseInt(armorIdString);
const armorRaw = tankDefinition.root.hull.armor[name];
this.assignArmor(armorRaw, armorId, hullArmor);
});
const crew: Crew[] = [];
for (const crewKey in tankDefinition.root.crew) {
const value = tankDefinition.root.crew[crewKey as BlitzCrewType];
let entry: Crew;
const index = crew.findIndex(
({ type }) => this.blitzkitCrewTypeToBlitz[type] === crewKey,
);
if (index === -1) {
if (crewKey === "#text") continue;
entry = {
type: this.blitzCrewTypeToBlitzkit[crewKey as BlitzCrewType],
count: 0,
substitute: [],
};
crew.push(entry);
} else {
entry = crew[index];
}
if (typeof value === "string") {
entry.count++;
if (value !== "") {
entry.substitute = value.split(/\n| /).map((member) => {
return this.blitzCrewTypeToBlitzkit[
member.trim() as BlitzCrewType
];
});
}
} else {
if (entry.count === undefined) {
entry.count = value.length;
} else {
entry.count += value.length;
}
}
}
modelDefinitions.models[tankId] = {
armor: hullArmor,
turret_origin: this.vector3TupleToBlitzkit(turretOrigin),
initial_turret_rotation: tankDefinition.root.hull
.turretInitialRotation
? {
yaw: tankDefinition.root.hull.turretInitialRotation.yaw,
pitch: tankDefinition.root.hull.turretInitialRotation.pitch,
roll: tankDefinition.root.hull.turretInitialRotation.roll,
}
: undefined,
bounding_box: {
min: this.vector3TupleToBlitzkit(
tankParameters.collision.hull.bbox.min,
),
max: this.vector3TupleToBlitzkit(
tankParameters.collision.hull.bbox.max,
),
},
turrets: {},
tracks: {},
};
for (const key in tankDefinition.root.chassis) {
const track = tankDefinition.root.chassis[key];
const trackId = toUniqueId(nation, chassisList.root.ids[key]);
const trackArmorRaw = track.armor.leftTrack;
const hullOrigin = track.hullPosition
.split(" ")
.map(Number) as Vector3Tuple;
modelDefinitions.models[tankId].tracks[trackId] = {
thickness:
typeof trackArmorRaw === "number"
? trackArmorRaw
: trackArmorRaw["#text"],
origin: this.vector3TupleToBlitzkit(hullOrigin),
};
}
Object.keys(tankDefinition.root.turrets0).forEach((turretKey) => {
const turret = tankDefinition.root.turrets0[turretKey];
const turretModel = Number(
parsePath(turret.models.undamaged).name.split("_")[1],
);
const turretId = toUniqueId(nation, turretList.root.ids[turretKey]);
const turretYaw = (
typeof turret.yawLimits === "string"
? turret.yawLimits
: turret.yawLimits.at(-1)!
)
.split(" ")
.map(Number) as [number, number];
const gunOrigin = (
typeof turret.gunPosition === "string"
? turret.gunPosition
: turret.gunPosition[0]
)
.split(" ")
.map(Number) as Vector3Tuple;
const turretArmor: Armor = { thickness: {}, spaced: [] };
Object.keys(turret.armor)
.filter((name) => name.startsWith("armor_"))
.forEach((name) => {
const armorIdString = name.match(/armor_(\d+)/)?.[1];
if (armorIdString === undefined) {
throw new SyntaxError(`Invalid armor id: ${name}`);
}
const armorId = parseInt(armorIdString);
const armorRaw = turret.armor[name];
this.assignArmor(armorRaw, armorId, turretArmor);
});
turret.userString;
modelDefinitions.models[tankId].turrets[turretId] = {
armor: turretArmor,
gun_origin: this.vector3TupleToBlitzkit(gunOrigin),
model_id: turretModel,
yaw:
-turretYaw[0] + turretYaw[1] === 360
? undefined
: { min: turretYaw[0], max: turretYaw[1] },
guns: {},
bounding_box: {
min: this.vector3TupleToBlitzkit(
tankParameters.collision[
parsePath(turret.hitTester.collisionModel).name.toLowerCase()
].bbox.min,
),
max: this.vector3TupleToBlitzkit(
tankParameters.collision[
parsePath(turret.hitTester.collisionModel).name.toLowerCase()
].bbox.max,
),
},
};
Object.keys(turret.guns).forEach((gunKey, gunIndex) => {
const gun = turret.guns[gunKey];
const gunId = toUniqueId(nation, gunList.root.ids[gunKey]);
const gunListEntry = gunList.root.shared[gunKey];
const pitchLimitsRaw = gun.pitchLimits ?? gunListEntry.pitchLimits;
const gunPitch = (
typeof pitchLimitsRaw === "string"
? pitchLimitsRaw
: pitchLimitsRaw.at(-1)!
)
.split(" ")
.map(Number) as [number, number];
const gunModel = Number(
parsePath(gun.models.undamaged).name.split("_")[1],
);
const front = gun.extraPitchLimits?.front
? gun.extraPitchLimits.front.split(" ").map(Number)
: undefined;
const back = gun.extraPitchLimits?.back
? gun.extraPitchLimits.back.split(" ").map(Number)
: undefined;
const transition = gun.extraPitchLimits?.transition
? typeof gun.extraPitchLimits.transition === "number"
? gun.extraPitchLimits.transition
: gun.extraPitchLimits.transition.at(-1)!
: undefined;
const gunArmor: Armor = { thickness: {}, spaced: [] };
const maskName = `gun_${gunModel.toString().padStart(2, "0")}`;
const maskEnabled =
tankParameters.maskSlice?.[maskName]?.enabled ?? false;
let mask: number | undefined;
if (maskEnabled) {
const maskRaw = tankParameters.maskSlice![maskName]!;
mask = maskRaw.planePosition[1];
} else {
mask = undefined;
}
Object.keys(gun.armor)
.filter((name) => name.startsWith("armor_"))
.forEach((name) => {
const armorIdString = name.match(/armor_(\d+)/)?.[1];
if (armorIdString === undefined) {
throw new SyntaxError(`Invalid armor id: ${name}`);
}
const armorId = parseInt(armorIdString);
const armorRaw = gun.armor[name];
this.assignArmor(armorRaw, armorId, gunArmor);
});
modelDefinitions.models[tankId].turrets[turretId].guns[gunId] = {
armor: gunArmor,
model_id: gunModel,
mask,
thickness:
gun.armor.gun === undefined
? 0
: typeof gun.armor.gun === "number"
? gun.armor.gun
: gun.armor.gun["#text"],
pitch: {
min: gunPitch[0],
max: gunPitch[1],
front: front
? {
min: front[0],
max: front[1],
range: front[2],
}
: undefined,
back: back
? {
min: back[0],
max: back[1],
range: back[2],
}
: undefined,
transition,
},
};
});
});
}
}
return modelDefinitions;
}
}
@@ -0,0 +1,21 @@
import { MapDefinitions } from "@blitzkit/core";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI3 } from "./3_modelDefinitions";
export abstract class ServerBlitzKitAPI4 extends ServerBlitzKitAPI3 {
@Cache()
async mapDefinitions() {
const mapDefinitions = MapDefinitions.create();
for (const key in this.mapsYaml!.maps) {
const map = this.mapsYaml!.maps[key];
mapDefinitions.maps[map.id] = {
id: map.id,
name: this.getString(`#maps:${key}:${map.localName}`),
};
}
return mapDefinitions;
}
}
@@ -0,0 +1,42 @@
import { EquipmentDefinitions, EquipmentSlot } from "@blitzkit/core";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI4 } from "./4_mapDefinitions";
export abstract class ServerBlitzKitAPI5 extends ServerBlitzKitAPI4 {
@Cache()
async equipmentDefinitions() {
const equipmentDefinitions = EquipmentDefinitions.create();
Object.entries(this.optionalDevices!.root).forEach(
([optionalDeviceKey, optionalDeviceEntry]) => {
if (optionalDeviceKey === "nextAvailableId") return;
equipmentDefinitions.equipments[optionalDeviceEntry.id] = {
name: this.getString(optionalDeviceEntry.userString),
description: this.getString(optionalDeviceEntry.description),
};
},
);
Object.entries(this.optionalDeviceSlots!.root.presets).forEach(
([optionalDeviceSlotKey, optionalDeviceSlotEntry]) => {
if (optionalDeviceSlotKey === "emptyPreset") return;
equipmentDefinitions.presets[optionalDeviceSlotKey] = {
slots: Object.values(optionalDeviceSlotEntry)
.map((level) => {
return Object.values(level).map((options) => {
return {
left: this.optionalDevices!.root[options.device0].id,
right: this.optionalDevices!.root[options.device1].id,
} satisfies EquipmentSlot;
});
})
.flat(),
};
},
);
return equipmentDefinitions;
}
}
@@ -0,0 +1,114 @@
import {
BlitzTankFilterDefinitionCategory,
Consumable,
ConsumableDefinitions,
} from "@blitzkit/core";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI5 } from "./5_equipmentDefinitions";
export abstract class ServerBlitzKitAPI6 extends ServerBlitzKitAPI5 {
@Cache()
async consumableDefinitions() {
const consumableDefinitions = ConsumableDefinitions.create();
Object.entries(this.consumablesCommon).forEach(([key, consumable]) => {
const entry: Consumable = {
id: consumable.id,
game_mode_exclusive: "gameModeFilter" in consumable,
cooldown: consumable.script.cooldown,
duration: consumable.script.duration,
name: this.getString(consumable.userString),
exclude: [],
include: [],
};
consumableDefinitions.consumables[consumable.id] = entry;
const includeRaw = consumable.vehicleFilter?.include.vehicle;
const excludeRaw = consumable.vehicleFilter?.exclude?.vehicle;
if (includeRaw) {
entry.include = [];
if ("minLevel" in includeRaw) {
entry.include.push({
filter_type: {
$case: "tiers",
value: {
min: includeRaw.minLevel,
max: includeRaw.maxLevel,
},
},
});
} else if ("name" in includeRaw) {
entry.include.push({
filter_type: {
$case: "ids",
value: {
ids: includeRaw.name.split(/ +/).map((key) => {
return this.tankStringIdMap[key];
}),
},
},
});
} else throw new SyntaxError("Unhandled include type");
if (consumable.vehicleFilter?.include.nations) {
entry.include.push({
filter_type: {
$case: "nations",
value: {
nations: consumable.vehicleFilter.include.nations.split(" "),
},
},
});
}
}
if (excludeRaw) {
entry.exclude = [];
if ("name" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "ids",
value: {
ids: excludeRaw.name.split(/ +/).map((key) => {
return this.tankStringIdMap[key];
}),
},
},
});
} else if ("extendedTags" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "categories",
value: {
categories: excludeRaw.extendedTags
.split(" ")
.map(
(item) =>
this.blitzTankFilterDefinitionCategoryToBlitzkit[
item as BlitzTankFilterDefinitionCategory
],
),
},
},
});
} else throw new SyntaxError("Unhandled exclude type");
if (consumable.vehicleFilter?.exclude?.nations) {
entry.exclude!.push({
filter_type: {
$case: "nations",
value: {
nations: consumable.vehicleFilter.exclude.nations.split(" "),
},
},
});
}
}
});
return consumableDefinitions;
}
}
@@ -0,0 +1,117 @@
import {
BlitzTankFilterDefinitionCategory,
Provision,
ProvisionDefinitions,
} from "@blitzkit/core";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI6 } from "./6_consumableDefinitions";
export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
@Cache()
async provisionDefinitions() {
const provisionDefinitions = ProvisionDefinitions.create();
Object.entries(this.provisionsCommon).forEach(([, provision]) => {
const entry: Provision = {
id: provision.id,
exclude: [],
include: [],
game_mode_exclusive: "gameModeFilter" in provision,
name: this.getString(provision.userString),
};
provisionDefinitions.provisions[provision.id] = entry;
const includeRaw = provision.vehicleFilter?.include.vehicle;
const excludeRaw = provision.vehicleFilter?.exclude?.vehicle;
if (includeRaw) {
entry.include = [];
if ("minLevel" in includeRaw) {
entry.include.push({
filter_type: {
$case: "tiers",
value: {
min: includeRaw.minLevel,
max: includeRaw.maxLevel,
},
},
});
} else if ("name" in includeRaw) {
entry.include.push({
filter_type: {
$case: "ids",
value: {
ids: includeRaw.name.split(/ +/).map((key) => {
return this.tankStringIdMap[key];
}),
},
},
});
} else throw new SyntaxError("Unhandled include type");
if (provision.vehicleFilter?.include.nations) {
entry.include.push({
filter_type: {
$case: "nations",
value: {
nations: provision.vehicleFilter.include.nations.split(" "),
},
},
});
}
}
if (excludeRaw) {
entry.exclude = [];
if ("name" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "ids",
value: {
ids: excludeRaw.name
.split(/ +/)
.map((key) => this.tankStringIdMap[key]),
},
},
});
} else if ("extendedTags" in excludeRaw) {
entry.exclude!.push({
filter_type: {
$case: "categories",
value: {
categories: excludeRaw.extendedTags
.split(" ")
.map(
(item) =>
this.blitzTankFilterDefinitionCategoryToBlitzkit[
item as BlitzTankFilterDefinitionCategory
],
),
},
},
});
} else throw new SyntaxError("Unhandled exclude type");
if (provision.vehicleFilter?.exclude?.nations) {
entry.exclude!.push({
filter_type: {
$case: "nations",
value: {
nations: provision.vehicleFilter.exclude.nations.split(" "),
},
},
});
}
}
if (provision.script.bonusValues?.crewLevelIncrease !== undefined) {
provisionDefinitions.provisions[provision.id].crew =
provision.script.bonusValues?.crewLevelIncrease;
}
});
return provisionDefinitions;
}
}
@@ -0,0 +1,21 @@
import { BlitzTankClass, SkillDefinitions } from "@blitzkit/core";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI7 } from "./7_provisionDefinitions";
export abstract class ServerBlitzKitAPI8 extends ServerBlitzKitAPI7 {
@Cache()
async skillDefinitions() {
const skillDefinitions = SkillDefinitions.create();
for (const tankClass in this.tankmenAvatar!.root.skillsByClasses) {
const skills = this.tankmenAvatar!.root.skillsByClasses[tankClass];
skillDefinitions.classes[
this.blitzTankClassToBlitzkit[tankClass as BlitzTankClass]
] = { skills: skills.split(" ") };
}
return skillDefinitions;
}
}
@@ -0,0 +1,58 @@
import { AvailableNationsYaml, GameDefinitions } from "@blitzkit/core";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI8 } from "./8_skillDefinitions";
export abstract class ServerBlitzKitAPI9 extends ServerBlitzKitAPI8 {
@Cache()
async gameDefinitions() {
const gameDefinitions = GameDefinitions.create();
const consumableNativeNames: Record<string, number> = {};
const provisionNativeNames: Record<string, number> = {};
Object.entries(this.consumablesCommon).forEach(([key, consumable]) => {
consumableNativeNames[key] = consumable.id;
});
Object.entries(this.provisionsCommon).forEach(([key, provision]) => {
provisionNativeNames[key] = provision.id;
});
const version = (await this.vfs.text("Data/version.txt")).split(" ")[0];
gameDefinitions.version = version;
gameDefinitions.nations = (
await this.vfs.yaml<AvailableNationsYaml>("Data/available_nations.yaml")
).available_nations;
for (const match of this.squadBattleTypeStyles!.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g,
)) {
const id = Number(match[1]);
const name = this.getString(match[2]);
gameDefinitions.gameModes[id] = {
name,
};
}
Object.entries(this.combatRoles!).forEach(([, value]) => {
gameDefinitions.roles[value.id] = { provisions: [], consumables: [] };
value.default_abilities.forEach((ability) => {
if (ability in consumableNativeNames) {
gameDefinitions.roles[value.id].consumables.push(
consumableNativeNames[ability],
);
} else if (ability in provisionNativeNames) {
gameDefinitions.roles[value.id].provisions.push(
provisionNativeNames[ability],
);
} else throw new Error(`Unknown ability ${ability}`);
});
});
return gameDefinitions;
}
}
@@ -0,0 +1,3 @@
import { ServerBlitzKitAPI10 } from "@blitzkit/core/src/blitzkit/api/server/10_galleryDefinitions";
export class ServerBlitzKitAPI extends ServerBlitzKitAPI10 {}
@@ -0,0 +1,4 @@
export function assertSecret(secret?: string) {
if (secret === undefined) throw new Error("Missing secret");
return secret;
}
@@ -0,0 +1,22 @@
import { emptyAllStats } from '../../blitz/constants';
import { Samples } from '../../protos';
import type { BlitzkitStats } from '../../statistics';
export const emptyAverageDefinitionsAllStats: BlitzkitStats = {
...emptyAllStats,
battle_life_time: 0,
};
export const averageDefinitionsAllStatsKeys = Object.keys(
emptyAverageDefinitionsAllStats,
) as (keyof BlitzkitStats)[];
export const emptySamples: Samples = {
d_1: 0,
d_7: 0,
d_30: 0,
d_60: 0,
d_90: 0,
d_120: 0,
total: 0,
};
@@ -0,0 +1,17 @@
import { staticAsset } from "@blitzkit/core";
import { fetchPB } from "../../protobuf";
import { AverageDefinitions } from "../../protos";
export interface AverageDefinitionsManifest {
version: 1;
/**
* epoch in days
*/
latest: number;
}
export async function fetchAverageDefinitions() {
return await fetchPB(staticAsset("averages.pb"), AverageDefinitions);
}
export * from "./constants";
@@ -0,0 +1,12 @@
import { staticAsset } from "@blitzkit/core";
export interface DiscoveredIdsDefinitions {
time: number;
chunks: number;
count: number;
}
export async function fetchDiscoveredIdsDefinitions() {
const response = await fetch(staticAsset("ids/manifest.json"));
return response.json() as Promise<DiscoveredIdsDefinitions>;
}
+47
View File
@@ -0,0 +1,47 @@
import { assertSecret } from "@blitzkit/core";
interface EnvironmentConfig {
robots: boolean;
canonical: string;
paths: Record<string, string | null>;
nodes: Record<string, string>;
aliases: Record<string, string>;
}
let hasReadEnvironments = false;
const environments: Record<string, EnvironmentConfig> = {};
/**
* This is set up this way to avoid errors in runtimes where
* `import.meta.glob` does not exist.
*/
function readEnvironments() {
if (hasReadEnvironments) return;
hasReadEnvironments = true;
const glob = import.meta.glob("../../../../environments/*.json", {
eager: true,
import: "default",
}) as Record<string, EnvironmentConfig>;
const root = "../../../../environments";
for (const key in glob) {
const trimmed = key.slice(root.length + 1, -5);
environments[trimmed] = glob[key];
delete environments[key];
}
}
export function environmentConfig() {
readEnvironments();
const environment = assertSecret(import.meta.env.PUBLIC_ENVIRONMENT);
if (!(environment in environments)) {
throw new Error(`Invalid environment "${environment}"`);
}
return environments[environment];
}
@@ -1,30 +0,0 @@
// import { RatingInfo, Region, asset } from '@blitzkit/core';
// const cache: Record<
// Region,
// Record<
// number,
// RatingInfo & {
// detail: undefined;
// }
// >
// > = {
// com: {},
// eu: {},
// asia: {},
// };
// export async function getArchivedRatingInfo(region: Region, season: number) {
// if (!cache[region][season]) {
// const info = await fetch(
// asset(`regions/${region}/rating/${season}/info.cdon.lz4`),
// )
// .then((response) => response.arrayBuffer())
// .then((buffer) =>
// superDecompress<RatingInfo & { detail: undefined }>(buffer),
// );
// cache[region][season] = info;
// }
// return cache[region][season];
// }
+15 -3
View File
@@ -1,22 +1,34 @@
export * from "./alias";
export * from "./assertSecret";
export * from "./availableProvisions";
export * from "./averageDefinitions";
export * from "./coefficient";
export * from "./createDefaultProvisions";
export * from "./createDefaultSkills";
export * from "./degressiveStat";
export * from "./discoveredIdDefinitions";
export * from "./emblemURL";
export * from "./environment";
export * from "./eventManager";
export * from "./getTimeDaysAgo";
export * from "./isExplosive";
export * from "./metaSortTank";
export * from "./normalizeBoundingBox";
export * from "./patientFetchJSON";
export * from "./popularTanks";
export * from "./progressiveStat";
export * from "./resolveDpm";
export * from "./resolvePenetrationCoefficient";
export * from "./reviews";
export * from "./romanize";
export * from "./sluggify";
export * from "./staticAsset";
export * from "./sum";
export * from "./tankDefinitions";
export * from "./tankIcon";
export * from "./tankNames";
export * from "./unionBoundingBox";
export * from "./uniqueGuns";
export * from "./unwrapBlitzkitResponse";
export * from "./votes";
export * from "./createDefaultProvisions";
export * from "./metaSortTank";
export * from "./romanize";
export * from "./youtubers";
@@ -0,0 +1,7 @@
import { staticAsset } from "@blitzkit/core";
import { fetchPB } from "../protobuf";
import { PopularTanks } from "../protos";
export function fetchPopularTanks() {
return fetchPB(staticAsset("definitions/popular-tanks.pb"), PopularTanks);
}
@@ -8,12 +8,12 @@ export function resolvePenetrationCoefficient(
) {
const calibrated = calibratedShells
? type === ShellType.SHELL_TYPE_AP
? 1.08
? 1.06
: type === ShellType.SHELL_TYPE_APCR
? 1.05
? 1.06
: type === ShellType.SHELL_TYPE_HEAT
? 1.13
: 1.08
? 1.07
: 1.07
: 1;
const equalizer = (equalize ? equalizerObj?.penetration : undefined) ?? 1;
+7
View File
@@ -0,0 +1,7 @@
import { staticAsset } from "@blitzkit/core";
import { fetchPB } from "../protobuf";
import { Reviews } from "../protos";
export function fetchReviews() {
return fetchPB(staticAsset("definitions/reviews.pb"), Reviews);
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { deburr } from "lodash-es";
import { slugify } from "transliteration";
const nonAlphanumericRegex = /[^a-z0-9]/g;
const multipleDashesRegex = /--+/g;
@@ -6,7 +6,7 @@ const trailingDashRegex = /-$/g;
const leadingDashRegex = /^-/g;
export function sluggify(value: string) {
let slug = deburr(value).toLowerCase();
let slug = slugify(value);
slug = slug.replaceAll(nonAlphanumericRegex, "-");
slug = slug.replaceAll(multipleDashesRegex, "-");
slug = slug.replaceAll(trailingDashRegex, "");
@@ -0,0 +1,5 @@
import { assertSecret } from "./assertSecret";
export function staticAsset(path: string) {
return `${assertSecret(import.meta.env.PUBLIC_STATIC_ASSET_BASE)}/${path}`;
}
@@ -0,0 +1,74 @@
import { times } from "lodash-es";
import { CrewType, ShellType, TankClass, TankType } from "../../protos";
export const SHELL_NAMES: Record<ShellType, string> = {
[ShellType.SHELL_TYPE_AP]: "AP",
[ShellType.SHELL_TYPE_APCR]: "APCR",
[ShellType.SHELL_TYPE_HEAT]: "HEAT",
[ShellType.SHELL_TYPE_HE]: "HE",
};
export const CREW_MEMBER_NAMES = {
[CrewType.CREW_TYPE_COMMANDER]: "commander",
[CrewType.CREW_TYPE_DRIVER]: "driver",
[CrewType.CREW_TYPE_GUNNER]: "gunner",
[CrewType.CREW_TYPE_LOADER]: "loader",
[CrewType.CREW_TYPE_RADIOMAN]: "radioman",
} as const;
/**
* @deprecated use svg
*/
export const TANK_ICONS: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/BIHSEH0.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/CSNha5V.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/wvf3ltm.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/ECeqlZa.png",
};
/**
* @deprecated use svg
*/
export const TANK_ICONS_PREMIUM: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/TCu3EdR.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/zdkpTRb.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/3z7eHX6.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/P3vbmyA.png",
};
/**
* @deprecated use svg
*/
export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/WTjeirB.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/EwhtKkU.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/u8YDMBh.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/8xRf3nc.png",
};
export const TIERS = times(10, (index) => index + 1);
export const TIER_ROMAN_NUMERALS: Record<number, string> = {
1: "I",
2: "II",
3: "III",
4: "IV",
5: "V",
6: "VI",
7: "VII",
8: "VIII",
9: "IX",
10: "X",
};
export const flags: Record<string, string> = {
ussr: "<:ussr:1218421042033197197>",
germany: "🇩🇪",
usa: "🇺🇸",
china: "🇨🇳",
uk: "🇬🇧",
france: "🇫🇷",
japan: "🇯🇵",
european: "🇪🇺",
other: "<:other:1218421572243558482>",
};
export const TREE_TYPE_ICONS: Record<TankType, Record<TankClass, string>> = {
[TankType.TANK_TYPE_RESEARCHABLE]: TANK_ICONS,
[TankType.TANK_TYPE_PREMIUM]: TANK_ICONS_PREMIUM,
[TankType.TANK_TYPE_COLLECTOR]: TANK_ICONS_COLLECTOR,
};
@@ -0,0 +1 @@
export * from "./constants";
+7
View File
@@ -0,0 +1,7 @@
import { alias } from "@blitzkit/core";
export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 };
export function tankIcon(id: number, size: "big" | "small" = "big") {
return alias("api", `/tanks/${id}/icons/${size}.webp`);
}
+51
View File
@@ -0,0 +1,51 @@
import { BlitzKitAPI } from "@blitzkit/core";
import locales from "@blitzkit/i18n/locales.json";
import { deburr } from "lodash-es";
import { I18nString } from "../protos";
export async function fetchTankNames(api: BlitzKitAPI) {
const [tankDefinitions, camouflageDefinitions] = await Promise.all([
api.tankDefinitions(),
api.camouflageDefinitions(),
]);
const tankDefinitionsArray = Object.values(tankDefinitions.tanks);
return await Promise.all(
tankDefinitionsArray.map(async (tank) => {
const searchableNameDeburr: I18nString = { locales: {} };
Object.entries(tank.name!).forEach(([key, value]) => {
searchableNameDeburr.locales[key] = deburr(value);
});
return {
id: tank.id,
name: tank.name,
searchableName: tank.name,
searchableNameDeburr,
camouflages: tank.camouflages
?.map((id) =>
locales.supported.map(
({ locale }) =>
camouflageDefinitions.camouflages[id]?.name!.locales[locale],
),
)
.flat()
.filter(Boolean)
.map(deburr)
.join(" "),
treeType: tank.type,
};
}),
);
}
export const SEARCH_KEYS = [
...locales.supported
.map(({ locale }) => [
`searchableName.locales.${locale}`,
`searchableNameDeburr.locales.${locale}`,
])
.flat(),
"camouflages",
];
@@ -14,7 +14,7 @@ export abstract class AbstractVFS {
abstract has(path: string): Promise<boolean>;
abstract raw(path: string): Promise<Uint8Array>;
abstract raw(path: string): Promise<Uint8Array<ArrayBuffer>>;
async resolve(path: string) {
const normalized = normalize(path);
@@ -46,7 +46,15 @@ export abstract class AbstractVFS {
return buffer;
}
abstract dir(path: string): Promise<string[]>;
protected abstract _dir(path: string): Promise<string[]>;
async dir(path: string) {
const files = await this._dir(path);
return files.map((file) =>
file.endsWith(".dvpl") ? file.slice(0, -5) : file,
);
}
async text(path: string) {
const file = await this.file(path);
@@ -63,7 +71,7 @@ export abstract class AbstractVFS {
return this.xmlParser.parse(file) as Type;
}
async _csv(text: string, options: Options = {}) {
protected async _csv(text: string, options: Options = {}) {
const parsed = parseCSV(text, options);
return parsed;
}
@@ -23,8 +23,12 @@ export class LocalVFS extends AbstractVFS {
return array;
}
async dir(path: string) {
return await readdir(`${this.base}/${path}`);
async _dir(path: string) {
try {
return await readdir(`${this.base}/${path}`);
} catch (_) {
return [];
}
}
dispose() {}
@@ -35,7 +35,7 @@ export class MixedVFS extends AbstractVFS {
throw new Error(`File not found: ${path}`);
}
async dir(path: string) {
async _dir(path: string) {
const files = new Set<string>();
for (const vfs of this.vfses) {
@@ -89,7 +89,7 @@ export class SteamVFS extends AbstractVFS {
return new Uint8Array(downloaded.file);
}
async dir(path: string) {
async _dir(path: string) {
const parentSegments = path.split("/").length;
const children = new Set<string>();
@@ -33,7 +33,7 @@ export class ZipVFS extends AbstractVFS {
return new Uint8Array(arrayBuffer);
}
async dir(path: string) {
async _dir(path: string) {
const parentSegments = path.split("/").length;
const children = new Set<string>();
+1 -1
View File
@@ -4,7 +4,7 @@ export * from "./imgur";
export * from "./math";
export * from "./patreon";
export * from "./protobuf";
export * from "./protos";
export * from "./statistics";
export * from "./streams";
export * from "./types";
export * from "./secrets";
-3
View File
@@ -1,3 +0,0 @@
export function copySign(a: number, b: number) {
return b >= 0 ? Math.abs(a) : -Math.abs(a);
}
-1
View File
@@ -3,7 +3,6 @@ export * from "./fisherYates";
export * from "./formatCompact";
export * from "./isNumber";
export * from "./NaNFallback";
export * from "./normalizeAngleDeg";
export * from "./normalizeAngleRad";
export * from "./normalToEuler";
export * from "./numberToRomanNumeral";
@@ -1,6 +0,0 @@
export function normalizeAngleDeg(angle: number) {
const newAngle = angle % 360;
if (newAngle > 360) return newAngle - 360;
if (newAngle < -360) return newAngle + 360;
return newAngle;
}
+5 -5
View File
@@ -1,11 +1,11 @@
import { secret } from "../secrets";
import { assertSecret } from '../blitzkit/assertSecret';
export function patreonLoginUrl() {
const params = new URLSearchParams({
response_type: "code",
client_id: secret("PUBLIC_PATREON_CLIENT_ID"),
redirect_uri: secret("PUBLIC_PATREON_REDIRECT_URI"),
scope: "identity",
response_type: 'code',
client_id: assertSecret(import.meta.env.PUBLIC_PATREON_CLIENT_ID),
redirect_uri: assertSecret(import.meta.env.PUBLIC_PATREON_REDIRECT_URI),
scope: 'identity',
});
return `https://www.patreon.com/oauth2/authorize?${params}`;
-5
View File
@@ -1,5 +0,0 @@
import { PassableSecretName } from "./secret";
export function hasSecret(name: PassableSecretName) {
return name in import.meta.env;
}
-3
View File
@@ -1,3 +0,0 @@
export * from "./secret";
export * from "./hasSecret";
export * from "./wellKnown";
-35
View File
@@ -1,35 +0,0 @@
import { hasSecret } from "./hasSecret";
import { WellKnownSecret } from "./wellKnown";
export enum SecretType {
String,
Number,
}
export type PassableSecretName = WellKnownSecret;
export function secret(
name: PassableSecretName,
type?: SecretType.String,
): string;
export function secret(
name: PassableSecretName,
type: SecretType.Number,
): number;
export function secret(name: PassableSecretName, type = SecretType.String) {
if (!hasSecret(name)) {
throw new Error(`Secret "${name}" not defined`);
}
const value = import.meta.env[name];
switch (type) {
case SecretType.String:
return String(value);
case SecretType.Number:
return Number(value);
}
}
-17
View File
@@ -1,17 +0,0 @@
export type WellKnownSecret =
// development set-up
| "TEMP"
// website
| "BUILD_CHUNK"
| "TEXTURE_CHUNKS"
// wotb
| "WOTB_SERVER"
| "WOTB_VERSION"
| "PUBLIC_WOTB_CLIENT"
| "WOTB_USMAP"
// google
| "GOOGLE_APPLICATION_CREDENTIALS"
| "PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID"
// patreon
| "PUBLIC_PATREON_CLIENT_ID"
| "PUBLIC_PATREON_REDIRECT_URI";
-51
View File
@@ -1,51 +0,0 @@
import { times } from 'lodash-es';
import { ReadStream, WriteStream } from './buffer';
export class DidsReadStream extends ReadStream {
dids() {
this.magic();
this.header();
return this.body();
}
magic() {
const magic = this.utf8(4);
if (magic !== 'DIDS') {
throw new Error(`Invalid DIDS magic number: "${magic}"`);
}
}
header() {
return {
version: this.uint16(),
};
}
body() {
const count = this.uint32();
return times(count, () => this.uint32());
}
}
export class DidsWriteStream extends WriteStream {
dids(ids: number[]) {
this.magic();
this.header();
this.body(ids);
return this;
}
magic() {
this.utf8('DIDS');
}
header() {
this.uint16(1);
}
body(ids: number[]) {
this.uint32(ids.length);
ids.forEach((id) => this.uint32(id));
}
}
+9 -11
View File
@@ -1,11 +1,9 @@
export * from './buffer';
export * from './bufferToBigInt';
export * from './dds';
export * from './dids';
export * from './pvr';
export * from './rts';
export * from './sc2';
export * from './scg';
export * from './scpg';
export * from './windows';
export * from "./buffer";
export * from "./bufferToBigInt";
export * from "./dds";
export * from "./pvr";
export * from "./rts";
export * from "./sc2";
export * from "./scg";
export * from "./scpg";
export * from "./windows";
@@ -0,0 +1,3 @@
export interface AvailableNationsYaml {
available_nations: string[];
}
@@ -0,0 +1,10 @@
export type BlitzModuleType = {
[key in "vehicle" | "engine" | "chassis" | "turret" | "gun"]:
| UnlocksInner
| UnlocksInner[];
};
interface UnlocksInner {
cost: number | string;
"#text": number;
}
+3
View File
@@ -0,0 +1,3 @@
export interface BlitzStrings {
[key: string]: string;
}
@@ -0,0 +1 @@
export type BlitzTankFilterDefinitionCategory = "clip";
+33
View File
@@ -0,0 +1,33 @@
export interface CamouflagesXml {
camouflages: Record<
string,
{
id: number;
userString: string;
description: string;
category: string;
group: string;
kind: string;
notInShop: boolean;
unlockCostCategory?: string;
vehicleFilter: {
include?: CamouflagesInclude | CamouflagesInclude[];
};
script: string[] | string;
icon?: string;
unlockPremium?: string;
unlockQuest?: string;
unlockClanLevel?: number;
unlockShopBundle?: string;
unlockTankRank?: number;
}
>;
}
type CamouflagesInclude =
| {
nations?: string;
}
| {
vehicle?: { name?: string; minLevel?: number; maxLevel?: number };
};
@@ -0,0 +1,7 @@
export type CamouflagesYaml = Record<
string,
{
userString?: string;
shortUserString?: string;
}
>;
@@ -0,0 +1,4 @@
export interface ChassisDefinitionsList {
nextAvailableId: number;
ids: Record<string, number>;
}
Loaded 100 of 894 files, more files were not shown because too many files have changed in this diff. Show more