mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
Compare commits
190
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1551cfe2b9 | ||
|
|
f355cb9f76 | ||
|
|
e42576b005 | ||
|
|
f68198475e | ||
|
|
896ae20dce | ||
|
|
b8fdc5424d | ||
|
|
204c3eacc1 | ||
|
|
4d0f21e10b | ||
|
|
d3202e8607 | ||
|
|
a438eb704f | ||
|
|
1569de4475 | ||
|
|
0f022f8295 | ||
|
|
75c21a5129 | ||
|
|
5092d43e8e | ||
|
|
56d5d5e956 | ||
|
|
70eca3293b | ||
|
|
4f7ee54ef7 | ||
|
|
269df9586d | ||
|
|
3152c18840 | ||
|
|
52630596b8 | ||
|
|
7e184b8be0 | ||
|
|
877ba9408d | ||
|
|
8dc9a6b46d | ||
|
|
b3e439669b | ||
|
|
08c909fbe2 | ||
|
|
a44e2e9866 | ||
|
|
bae2f253bc | ||
|
|
d485a463dd | ||
|
|
3da1ed4d6c | ||
|
|
051e9ec74a | ||
|
|
486171f4db | ||
|
|
72de42a9ab | ||
|
|
404e6625c0 | ||
|
|
32e4bf3fd2 | ||
|
|
0d1088fefb | ||
|
|
1a17d607fb | ||
|
|
dcda627518 | ||
|
|
c130da7f0e | ||
|
|
e2df4d35b8 | ||
|
|
7739dbd0a3 | ||
|
|
9b7aa13d61 | ||
|
|
84ecaec5d3 | ||
|
|
01a5f1bd47 | ||
|
|
de0433ca02 | ||
|
|
cf76ae7136 | ||
|
|
ea21636c4e | ||
|
|
dafa834d28 | ||
|
|
f1a1c78588 | ||
|
|
14d16e5daf | ||
|
|
b52c1fc2d9 | ||
|
|
f45b24f7ab | ||
|
|
6ae175e5e0 | ||
|
|
0df27c38a1 | ||
|
|
e27cc3f6ed | ||
|
|
968c3957d0 | ||
|
|
577d2323a8 | ||
|
|
625c6cc649 | ||
|
|
9bc753b512 | ||
|
|
fb34e6ce2f | ||
|
|
f882ea29e2 | ||
|
|
62f68f1f10 | ||
|
|
8c9a41d72b | ||
|
|
5bda754b6e | ||
|
|
c5bb537bf2 | ||
|
|
c59779bedb | ||
|
|
5059e382b3 | ||
|
|
953570461c | ||
|
|
6d527a7170 | ||
|
|
ffd74de79f | ||
|
|
7c9ef9e5e0 | ||
|
|
cd10d9ab4a | ||
|
|
578d4a3ff2 | ||
|
|
0cd01fa258 | ||
|
|
21b37ced8d | ||
|
|
81856276b0 | ||
|
|
389052c692 | ||
|
|
09edd39a06 | ||
|
|
5c3077033e | ||
|
|
ef84d5b3b5 | ||
|
|
d29f91afbc | ||
|
|
ab87282103 | ||
|
|
408417ca68 | ||
|
|
b171ce2011 | ||
|
|
58d0d92d4f | ||
|
|
754f39f623 | ||
|
|
10c8ac95a4 | ||
|
|
b367866a22 | ||
|
|
b26c5137f3 | ||
|
|
13cda7aec0 | ||
|
|
d1b8bfe244 | ||
|
|
734db0957e | ||
|
|
050cb7a834 | ||
|
|
6dbcbc2afc | ||
|
|
484cb8fcb5 | ||
|
|
e09b407e61 | ||
|
|
b8742bae66 | ||
|
|
af58b4614d | ||
|
|
db6c4679b7 | ||
|
|
8169dfd85d | ||
|
|
e6d7f78ea4 | ||
|
|
f1f29b85b7 | ||
|
|
f587bc0983 | ||
|
|
7ef4e0b9af | ||
|
|
b52919ebfe | ||
|
|
84290b1c0e | ||
|
|
24297dcae8 | ||
|
|
c7ee3dbf7e | ||
|
|
dd769a9aa5 | ||
|
|
645cfacecb | ||
|
|
d2041d0ee6 | ||
|
|
ed7dd76ee5 | ||
|
|
bc2b9f1267 | ||
|
|
851a1108d2 | ||
|
|
f09663e1c0 | ||
|
|
6a05f11001 | ||
|
|
36e8a20cd5 | ||
|
|
7630cc52d3 | ||
|
|
23d2ca9a42 | ||
|
|
6e759e52c9 | ||
|
|
85883cd2c5 | ||
|
|
9e4d9c7968 | ||
|
|
9968b2c83d | ||
|
|
be0804ae59 | ||
|
|
129a2c52f9 | ||
|
|
996c9c05df | ||
|
|
36dbb24572 | ||
|
|
aaae8fb8ff | ||
|
|
e5e747f7a0 | ||
|
|
487d149587 | ||
|
|
48c4a4c76d | ||
|
|
33ebbbeb65 | ||
|
|
1a7a1223d5 | ||
|
|
9813459310 | ||
|
|
94c3608ca7 | ||
|
|
7e9be14a9a | ||
|
|
5f773931bc | ||
|
|
b2fd85a5b0 | ||
|
|
b060e65af5 | ||
|
|
8a3e57637d | ||
|
|
90ad923672 | ||
|
|
59108d7637 | ||
|
|
f85d097c16 | ||
|
|
ed92440b94 | ||
|
|
d41d45abae | ||
|
|
32a7db238b | ||
|
|
2dc23ba09d | ||
|
|
30522bfda0 | ||
|
|
ed31f40876 | ||
|
|
6ad64c9bd9 | ||
|
|
bd7c952478 | ||
|
|
f7180f0e1a | ||
|
|
b0611d7d72 | ||
|
|
244968bbbf | ||
|
|
fd8448b0a6 | ||
|
|
17c54be1c8 | ||
|
|
39745dd475 | ||
|
|
d81d885767 | ||
|
|
901cb0fcc8 | ||
|
|
7ee49c4026 | ||
|
|
4cff06110a | ||
|
|
251d40b0f4 | ||
|
|
ad15f83abb | ||
|
|
c2a5b742a7 | ||
|
|
514999e588 | ||
|
|
50d21918b4 | ||
|
|
43b59e4f02 | ||
|
|
d0834ed572 | ||
|
|
caf238dd32 | ||
|
|
0a7a220af3 | ||
|
|
434d1ede3c | ||
|
|
0f47208f5e | ||
|
|
cb2d795461 | ||
|
|
6e934e0ff1 | ||
|
|
375adadbe5 | ||
|
|
082a756f1b | ||
|
|
9fc8a8e2a2 | ||
|
|
1413475679 | ||
|
|
86504e685e | ||
|
|
9df81044d1 | ||
|
|
c5fd7591ea | ||
|
|
cd8e43f945 | ||
|
|
8df112aab4 | ||
|
|
94c04ce102 | ||
|
|
3a170202ca | ||
|
|
46937693a0 | ||
|
|
b391d0b036 | ||
|
|
1061e003f7 | ||
|
|
a2b573abca | ||
|
|
4571d141e6 | ||
|
|
d70225b03c |
No files matched your search
@@ -1 +0,0 @@
|
||||
indentSize: 2
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"tabWidth": 2
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+258
@@ -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"
|
||||
}
|
||||
@@ -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` | | | |
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".",
|
||||
},
|
||||
{
|
||||
"path": "C:/Program Files/WindowsApps/7458BE2C.WorldofTanksBlitz_11.1.256.0_x64__x4tje2y229k00/Data",
|
||||
},
|
||||
],
|
||||
}
|
||||
+3
-24
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
# BlitzKit 1.31.1
|
||||
|
||||
This update adds capabilities to release BlitzKit OpenTest and Preview.
|
||||
@@ -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/
|
||||
|
||||
@@ -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.
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "1.3.0",
|
||||
"commands": [
|
||||
"csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"robots": false,
|
||||
"canonical": "https://blitzkit.app",
|
||||
|
||||
"paths": {
|
||||
"default": null,
|
||||
"api": "api"
|
||||
},
|
||||
"nodes": {},
|
||||
"aliases": {
|
||||
"api": "/api",
|
||||
"static": "https://static.blitzkit.app"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
+1
-1
Submodule packages/closed updated: 45c8f7115c...f89f0e0772.
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)!;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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];
|
||||
// }
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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`);
|
||||
}
|
||||
@@ -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",
|
||||
];
|
||||
+11
-3
@@ -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>();
|
||||
|
||||
@@ -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";
|
||||
@@ -1,3 +0,0 @@
|
||||
export function copySign(a: number, b: number) {
|
||||
return b >= 0 ? Math.abs(a) : -Math.abs(a);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}`;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { PassableSecretName } from "./secret";
|
||||
|
||||
export function hasSecret(name: PassableSecretName) {
|
||||
return name in import.meta.env;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./secret";
|
||||
export * from "./hasSecret";
|
||||
export * from "./wellKnown";
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface BlitzStrings {
|
||||
[key: string]: string;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type BlitzTankFilterDefinitionCategory = "clip";
|
||||
@@ -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 895 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user