Compare commits

..
35 Commits
Author SHA1 Message Date
tresabhi 39745dd475 infrastructure for inline vfs and clients 2026-07-31 20:48:33 -05:00
tresabhi d81d885767 reintroduce assets temporarily 2026-07-31 20:30:35 -05:00
tresabhi 901cb0fcc8 remove assets 2026-07-31 20:30:19 -05:00
tresabhi 7ee49c4026 remove prisma 2026-07-31 20:28:18 -05:00
tresabhi 4cff06110a remove app 2026-07-31 20:27:39 -05:00
tresabhi 251d40b0f4 fix typo 2026-07-31 20:23:39 -05:00
tresabhi ad15f83abb move to chunks 2026-07-31 20:22:12 -05:00
tresabhi c2a5b742a7 clean up root 2026-07-31 20:21:27 -05:00
tresabhi 514999e588 remove bot 2026-07-31 20:17:57 -05:00
tresabhi 50d21918b4 update logs 2026-07-31 20:14:03 -05:00
tresabhi 43b59e4f02 update workflows to work with production as the deploy branch 2026-07-31 20:12:54 -05:00
tresabhi d0834ed572 update equipments 2026-07-31 19:48:03 -05:00
tresabhi caf238dd32 add robust slugs 2026-07-31 13:43:53 -05:00
tresabhi 0a7a220af3 logs 2026-07-31 11:23:45 -05:00
tresabhi 434d1ede3c use transliteration 2026-07-31 11:21:50 -05:00
tresabhi 0f47208f5e add support for missing vfs0 dirs 2026-07-31 11:07:54 -05:00
tresabhi cb2d795461 fix url 2026-07-11 18:14:22 -05:00
tresabhi 6e934e0ff1 logs 2026-07-11 18:13:02 -05:00
tresabhi 375adadbe5 Merge branch 'main' into dev 2026-07-11 18:04:36 -05:00
tresabhi 082a756f1b auto scroll on drag in tier list 2026-07-11 18:02:49 -05:00
tresabhi 9fc8a8e2a2 more generous pitch transition zones 2026-07-11 17:41:14 -05:00
tresabhi 1413475679 bigger floor and more aggressive light decay 2026-07-11 17:38:11 -05:00
tresabhi 86504e685e softer shadows 2026-07-11 17:33:37 -05:00
tresabhi 9df81044d1 increasing lighting angle to cover larger tanks 2026-07-11 17:29:44 -05:00
tresabhi c5fd7591ea complete lighting animation even if canvas is unfocused 2026-07-11 17:29:15 -05:00
TrèsAbhi cd8e43f945 Merge pull request #256 from Hitjack007/mixer-crashing 2026-07-11 17:17:11 -05:00
tresabhi 8df112aab4 type-safe disposal 2026-07-11 17:16:52 -05:00
TrèsAbhi 94c04ce102 Merge pull request #255 from Hitjack007/external-ricochets 2026-07-11 17:14:10 -05:00
TrèsAbhi 3a170202ca Merge pull request #254 from Hitjack007/he-crash-fix 2026-07-11 17:10:49 -05:00
tresabhi 46937693a0 implement nullable refs in a type-safe manner 2026-07-11 17:09:38 -05:00
TrèsAbhi b391d0b036 Merge pull request #253 from Hitjack007/static-armor-uncapitalised-fix 2026-07-11 17:06:29 -05:00
Mark Greene 1061e003f7 Fix Safari crash on mixer texture disposal 2026-07-10 21:30:21 +12:00
Mark Greene a2b573abca fix ricochets on external modules 2026-07-10 17:51:32 +12:00
Mark Greene 4571d141e6 Fix crash on repeated HE shell clicks in Tankopedia armor viewer 2026-07-10 16:03:05 +12:00
Mark Greene d70225b03c fix static armor plate title to use localized strings
Was rendering the raw lowercase layerTypeNames key instead of resolving it through strings.website.tools.tankopedia.sandbox.dynamic.shot_card.element, same pattern already used in ShotDisplayCard.
2026-07-10 15:41:46 +12:00
829 changed files with 36206 additions and 12578 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
indentSize: 2
+83
View File
@@ -0,0 +1,83 @@
name: Assets
on:
push:
branches: production
workflow_dispatch:
inputs:
asset_repo:
type: choice
required: false
description: What asset repo to write to
options:
- blitzkit/assets
- blitzkit/assets-preview
- blitzkit/assets-dev
default: blitzkit/assets
asset_source:
type: string
required: false
description: "Archive URL or Steam to download (example: steam:APP_ID/DEPOT_ID or zip:URL_TO_ZIP)"
default: "steam:444200/444202"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.asset_repo }}
cancel-in-progress: true
env:
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
VIDEOS_SHEET: ${{ secrets.VIDEOS_SHEET }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
WOTB_DLC_CDN: ${{ secrets.WOTB_DLC_CDN }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_REPO: ${{ github.event.inputs.asset_repo != '' && github.event.inputs.asset_repo || secrets.PUBLIC_ASSET_REPO }}
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: true
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
PUBLIC_PATREON_CLIENT_ID: ${{ secrets.PUBLIC_PATREON_CLIENT_ID }}
PUBLIC_PATREON_REDIRECT_URI: ${{ secrets.PUBLIC_PATREON_REDIRECT_URI }}
PATREON_CLIENT_SECRET: ${{ secrets.PATREON_CLIENT_SECRET }}
PUBLIC_NITROPAY_ID: ${{ secrets.PUBLIC_NITROPAY_ID }}
PUBLIC_DEBUG_MISSING_I18N: false
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_FAST_USER: blitzkit
PUBLIC_ASSET_FAST_REPO: assets
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
jobs:
build_assets:
name: Build assets
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- run: bun run setup
- name: AppArmor hack
run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
- name: Install patches
run: bun run setup:patches --source=${{ github.event.inputs.asset_source || 'steam:444200/444202' }}
- name: Build assets
run: bun run build:assets --all-targets --source=${{ github.event.inputs.asset_source || 'steam:444200/444202' }}
+54
View File
@@ -0,0 +1,54 @@
name: Averages
on:
workflow_dispatch:
schedule:
- cron: "0 8 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: "true"
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Build averages
env:
PUBLIC_ASSET_BRANCH: main
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
VIDEOS_SHEET: ${{ secrets.VIDEOS_SHEET }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
run: bun run build:averages --production
+38
View File
@@ -0,0 +1,38 @@
name: Reviews
on:
workflow_dispatch:
schedule:
- cron: 0 0 * * 2
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Find reviews
run: bun run build:reviews
-95
View File
@@ -1,95 +0,0 @@
name: Website
on:
push:
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ 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
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
STEAM_APP: 3341250
STEAM_OS: windows
STEAM_FINGERPRINT: Manifest_UFSFiles_Win64.txt
# debug
DOTNET_EnableDiagnostics: 1
jobs:
build:
name: Build chunk ${{ matrix.chunk }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
chunk: [pages, api, media, models, textures]
steps:
- name: Free disk space
uses: jlumbroso/free-disk-space@main
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: Set up 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
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- name: Set up project
run: |
bun run setup
./scripts/setup_depot_downloader.sh
- name: Download Steam game
run: scripts/download_game.sh
- name: Enable core dumps
run: |
ulimit -c unlimited
- name: Build
run: bun run build-website
env:
BUILD_CHUNK: ${{ matrix.chunk }}
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
with:
name: chunk-${{ matrix.chunk }}
path: ${{ matrix.chunk == 'pages' && 'dist/website' || format('dist/website/{0}', matrix.chunk) }}
+72
View File
@@ -0,0 +1,72 @@
name: Website
on:
push:
workflow_dispatch:
schedule:
- cron: "0 4 * * *"
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
SERVICING: ${{ secrets.SERVICING }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
build:
name: Build website
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- uses: actions/configure-pages@v6
- run: bun run setup
- name: Build website
run: bun run build:website
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
with:
path: dist/website
deploy-github-pages:
if: ${{ github.ref == 'refs/heads/production' }}
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
needs: build
environment:
name: Production
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
+1 -3
View File
@@ -22,12 +22,11 @@ depots
public/sitemap*
/firebase.json
target
packages/website-ue/src/protos/*.ts
# packages/core/src/protos/*.ts
/.vercel
.astro
.vercel
/src
/packages/bot/src/prisma
dev-dist
# Logs
logs
@@ -53,4 +52,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
*.lscache
-3
View File
@@ -1,6 +1,3 @@
[submodule "packages/closed"]
path = packages/closed
url = https://github.com/blitzkit/closed.git
[submodule "packages/cue4parse"]
path = packages/cue4parse
url = https://github.com/tresabhi/CUE4Parse.git
-3
View File
@@ -1,3 +0,0 @@
{
"tabWidth": 2
}
+4
View File
@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}
+258
View File
@@ -0,0 +1,258 @@
{
"cSpell.words": [
"AABBOX",
"Abhi",
"absarc",
"accountinfo",
"adsense",
"aeson",
"alphatest",
"analyticsdata",
"analyticsreporting",
"APAC",
"apcr",
"appinfo",
"applypatches",
"artefacts",
"astrojs",
"astrolib",
"atgm",
"autocrop",
"Autoloader",
"Autoreloader",
"AYUV",
"battleresults",
"bbox",
"beforeinstallprompt",
"bepaint",
"betakrieg",
"BINORMAL",
"Binormals",
"bkni",
"BKRL",
"BKRTS",
"BKRTSMANIFEST",
"bkwd",
"blitzkit",
"blitzrinth",
"blitzstars",
"bufbuild",
"cacheable",
"Callouts",
"Camo",
"Carro",
"CCPAOPTOUT",
"CDON",
"clanid",
"classname",
"cleandist",
"closedkrieg",
"cockroackdb",
"Coef",
"Combattimento",
"cooldown",
"corelations",
"corsproxy",
"CUBEMAP",
"CUBETEXCOORD",
"customperiod",
"datasource",
"Dava",
"dbid",
"Ddpf",
"Ddsd",
"deadrail",
"debugflags",
"deburr",
"decalmask",
"decompressor",
"décor",
"dedup",
"defence",
"degressive",
"depotdownloader",
"devkrieg",
"diacriticless",
"DIDS",
"donotremove",
"drei",
"Droodles",
"dvpd",
"dvpl",
"DVPLCRC",
"dvpm",
"dxgi",
"easimon",
"echarts",
"Everytime",
"FASTNAME",
"favicons",
"flexdirection",
"fontsource",
"FOURCC",
"frameloop",
"fsformat",
"Fullscreen",
"fullstats",
"fuzzysort",
"gamemode",
"gameplay",
"glsl",
"GLTF",
"GSOR",
"gtag",
"haha",
"Herdbreaker",
"Hetz",
"highp",
"iife",
"imgur",
"immer",
"Inclusivity",
"JOINTINDEX",
"JOINTWEIGHT",
"kranvagn",
"krieg",
"Leaderboard",
"leaderboards",
"lerp",
"LINEARSIZE",
"linktarget",
"lmao",
"loadouts",
"loddist",
"lods",
"lossrate",
"Maddoxkkm",
"malum",
"mantlets",
"mediump",
"metallicness",
"metalness",
"minitelrose",
"Minotauro",
"MIPMAPCOUNT",
"mišek",
"mmnow",
"modeltransform",
"MURICA",
"nanostores",
"NEGATIVEX",
"NEGATIVEY",
"NEGATIVEZ",
"networkidle",
"NITROPAY",
"nivo",
"nmatname",
"noout",
"normalmap",
"oneof",
"oopsies",
"Oopsy",
"opentest",
"OPTML",
"otemp",
"outdir",
"outfile",
"ownedtanks",
"padder",
"partytown",
"patchnotes",
"PIXELFORMAT",
"playerachievements",
"playerinfo",
"playform",
"pngjs",
"POSITIVEX",
"POSITIVEY",
"POSITIVEZ",
"precompute",
"Promisable",
"proto",
"protobuf",
"protoc",
"protos",
"proxyclient",
"prunestrings",
"Pyogenics",
"ratingleaderboard",
"raycaster",
"Redirector",
"Relogin",
"researchable",
"Resvg",
"rizz",
"roboto",
"rtsc",
"RTSM",
"SCPG",
"scroller",
"searchclans",
"searchplayers",
"searchtanks",
"SHAREDEXP",
"SKLLD",
"sluggify",
"smoothstep",
"Smri",
"Specularity",
"SSAO",
"startcase",
"statkit",
"statsfull",
"steamapps",
"Stringifying",
"struct",
"structs",
"strvk",
"subnation",
"Synx",
"tankaverages",
"Tankerch",
"tankmen",
"tankopedia",
"tankopedias",
"tankstats",
"tauri",
"texconv",
"TEXCOORD",
"tresabhi",
"tungstentip",
"Turretless",
"TYPELESS",
"ubjson",
"Unicum",
"UNLOCALIZED",
"UNORM",
"unpickled",
"Unregistering",
"unrotate",
"unwrapper",
"userns",
"vercel",
"vickers",
"vitepress",
"Vovko",
"wargaming",
"webp",
"wgpkg",
"whitespaces",
"WHSS",
"winrate",
"wireframe",
"Woah",
"WOTB",
"wotblitz",
"wotbreplay",
"WOTINSPECTOR",
"xffn",
"xkdkw",
"XMLDVPL",
"YAMLDVPL",
"yolo",
"youtuber",
"youtubers",
"zoomable"
],
"typescript.tsdk": "node_modules\\typescript\\lib"
}
+33 -95
View File
@@ -1,105 +1,43 @@
# 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/). |
| `ALLOW_ROBOTS` | | `true` | Toggles between `Allow: /` and `Disallow: /` in the website's `/robots.txt`. This is usually `false` for all BlitzKit's subdomains. |
| `WOTB_GLOSSARY`, `WOTB_GLOSSARY_DOMAIN`, `WOTB_GLOSSARY_PATH` | | | |
| `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_BRANCH` | | `main` | The branch that the website is operating on. This is completely nominal. |
| `PUBLIC_GUIDE_WEBSITE` | [GuidesBlitz](https://guidesblitz.com/) | `https://guidesblitz.com` | The website that serves the encyclopedia-like content in BlitzKit. |
| `SERVICING` | | `false` | Wether the website is down for servicing. |
| `GH_TOKEN` | [GitHub Fine-Grained Tokens](https://github.com/settings/personal-access-tokens) | | The GitHub API key used for checking out private submodules and pushing content to the asset repository. |
| `DATABASE_URL` | [CockroachDB](https://www.cockroachlabs.com/) | | The CockroachDB-SQL database URL used to link Wargaming and Discord users. |
| `WOTB_DLC_CDN` | | | |
+10
View File
@@ -0,0 +1,10 @@
{
"folders": [
{
"path": ".",
},
{
"path": "C:/Program Files/WindowsApps/7458BE2C.WorldofTanksBlitz_11.1.256.0_x64__x4tje2y229k00/Data",
},
],
}
+493 -1312
View File
File diff suppressed because it is too large. Load diff
+1 -23
View File
@@ -1,27 +1,5 @@
{
"words": [
"gltf",
"amogos",
"Tankopedia",
"uasset",
"oodle",
"blitzkit",
"webp",
"hocs",
"cooldown",
"Lods",
"Skia",
"RMAO",
"Verts",
"drei",
"frameloop",
"pointermove",
"pointerup",
"pointerdown"
],
"userWords": [
"WOTB",
"USMAP",
"favicons"
"Tankopedia"
]
}
+20
View File
@@ -0,0 +1,20 @@
# BlitzKit 1.30.7
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features
- Dragging items in the Tier List now automatically scrolls the screen if you reach the top/bottom edge of the screen
## Fixes
- [Mark Greene] Clicking on the armor multiple times with HE no longer crashes BlitzKit
- [Mark Greene] Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration
- The lighting animation no longer stops prematurely when BlitzKit is unfocused
- The spot light angle has been increased to more evenly light up larger tanks
- BlitzKit once again uses soft shadows, reducing the pixelated shadows
- Fixed the weird patterns that formed on the tracks of tanks under self-shadows
- Increased the floor size and light decay to create the illusion that the floor is infinite
- [Mark Greene] Safari no longer crashes when unloading textures for unloaded tanks
- The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0
- [Mark Greene] Armor plate names in Static Armor are now properly capitalized in English and translated to other languages
+9
View File
@@ -0,0 +1,9 @@
# BlitzKit 1.30.8
Support for tanks with Chinese names.
## Fixes
- The URL generator for BlitzKit now handles names that have only Chinese characters
- The Mixed Virtual File System now handles non-existent directories correctly
- Updated equipments to match the game again
-11
View File
@@ -2,17 +2,6 @@
🎉 Services for everything and anything World of Tanks Blitz
## Bot
BlitzKit is available as a Discord bot and can be [installed very easily](/guide/invite).
- Track statistics
- Find inactive players in a clan
- Lookup owned tanks
- Create embeds for streams
- Track a player/tanks statistical evolution
- ...and a variety of other similar things
## Website
Provides a bunch of very helpful tools; https://blitzkit.app/
-3
View File
@@ -1,3 +0,0 @@
# Inviting the Bot
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
-13
View File
@@ -1,13 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.3.0",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}
+4 -7
View File
@@ -1,21 +1,18 @@
{
"name": "blitzkit",
"version": "2.0.0-beta.3",
"version": "1.30.8",
"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"
},
"type": "module",
"workspaces": [
"packages/*"
],
"dependencies": {
"prettier": "^3.8.3",
"lz4js": "^0.2.0",
"sharp": "^0.34.5",
"typescript": "^5.9.3"
}
}
+2 -3
View File
@@ -8,12 +8,11 @@
"@bufbuild/protobuf": "^2.9.0",
"@octokit/plugin-throttling": "^11.0.1",
"@octokit/rest": "^22.0.0",
"@prisma/client": "^6.16.2",
"@types/adm-zip": "^0.5.7",
"@types/bun": "^1.3.1",
"@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1",
"@types/three": "^0.184.1",
"@types/three": "^0.180.0",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^17.2.2",
@@ -21,7 +20,7 @@
"lodash-es": "^4.17.21",
"pickleparser": "^0.2.1",
"protobufjs": "^7.5.4",
"three": "^0.184.0",
"three": "^0.180.0",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1"
}
+89
View File
@@ -0,0 +1,89 @@
import { assertSecret, EventManager } from "../blitzkit";
import { patientFetch } from "../blitzkit/patientFetch";
import type { Region } from "./regions";
const RETRY_ERRORS = ["REQUEST_LIMIT_EXCEEDED", "SOURCE_NOT_AVAILABLE"];
export const retryAbleBlitzFetchEvent = new EventManager();
export const blitzFetchQueueAvailableEvent = new EventManager();
type BlitzResponse<Data extends object> =
| {
status: "error";
error: { field: string; message: string; code: 402; value: null };
}
| {
status: "ok";
data: Data;
};
const MAX_CALLS_PER_SECOND = 10;
const queue: { url: string; resolve: (data: object) => void }[] = [];
let inProgress = 0;
function decrement() {
inProgress--;
blitzFetchQueueAvailableEvent.emit(undefined);
manageQueue();
}
async function manageQueue() {
if (queue.length > 0 && inProgress < MAX_CALLS_PER_SECOND) {
inProgress++;
const timeout = setTimeout(decrement, 1000);
const request = queue.shift()!;
const data = (await patientFetch(request.url)
.then((response) => response.json())
.catch((error) => ({ status: "error", error }))) as BlitzResponse<
typeof request.resolve
>;
if (data.status === "ok") {
request.resolve(data.data);
} else {
if (RETRY_ERRORS.includes(data.error.message)) {
retryAbleBlitzFetchEvent.emit(undefined);
clearTimeout(timeout);
setTimeout(() => {
queue.push(request);
decrement();
}, 1000 / MAX_CALLS_PER_SECOND);
} else {
clearTimeout(timeout);
decrement();
throw new Error(`Wargaming response error status: "${data.status}"`, {
cause: `Message: "${data.error.message}"\nURL: "${request.url}"`,
});
}
}
}
}
export type FetchBlitzParams = Record<string, string | number | undefined>;
export function fetchBlitz<Data extends object>(
region: Region,
path: string,
params: FetchBlitzParams = {}
) {
return new Promise<Data>((resolve) => {
queue.push({
url: `https://api.wotblitz.${region}/wotb/${path}/?${Object.entries({
application_id: assertSecret(
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
),
...params,
})
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => `${key}=${encodeURIComponent(value!)}`)
.join("&")}`,
resolve(data) {
resolve(data as Data);
},
});
manageQueue();
});
}
+11
View File
@@ -0,0 +1,11 @@
import { assertSecret } from "../blitzkit";
import type { BlitzGlossary } from "../types";
export async function fetchGlossary(locale: string) {
const response = await fetch(
`${assertSecret(
import.meta.env.WOTB_GLOSSARY_DOMAIN
)}/${locale}${assertSecret(import.meta.env.WOTB_GLOSSARY_PATH)}`
);
return (await response.json()) as BlitzGlossary;
}
@@ -0,0 +1,35 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import type { Region } from "./regions";
interface IndividualAccountAchievements {
achievements: {
[achievement: string]: number;
};
max_series: {
[achievement: string]: number;
};
}
export interface AccountAchievements {
[id: number]: IndividualAccountAchievements;
}
export async function getAccountAchievements<
Ids extends number | number[],
ReturnType = Ids extends number
? IndividualAccountAchievements
: IndividualAccountAchievements[],
>(region: Region, ids: Ids) {
const object = await fetchBlitz<AccountAchievements>(
region,
"account/achievements",
{ account_id: normalizeIds(ids) }
);
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
+96
View File
@@ -0,0 +1,96 @@
import { BlitzStats } from "../statistics";
import { FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import { Region } from "./regions";
export interface SupplementaryStats {
WN8?: number;
tier?: number;
}
export interface ClanStats {
spotted: number;
max_frags_tank_id: number;
hits: number;
frags: number;
max_xp: number;
max_xp_tank_id: number;
wins: number;
losses: number;
capture_points: number;
battles: number;
damage_dealt: number;
damage_received: number;
max_frags: number;
shots: number;
frags8p: number;
xp: number;
win_and_survived: number;
survived_battles: number;
dropped_capture_points: number;
}
export interface RatingStats {
spotted: number;
calibration_battles_left: number;
hits: number;
frags: number;
recalibration_start_time: number;
mm_rating: number;
wins: number;
losses: number;
is_recalibration: boolean;
capture_points: number;
battles: number;
damage_dealt: number;
damage_received: number;
shots: number;
frags8p: number;
xp: number;
win_and_survived: number;
survived_battles: number;
dropped_capture_points: number;
}
export interface IndividualAccountInfo {
account_id: number;
created_at: number;
updated_at: number;
private: null;
last_battle_time: number;
nickname: string;
statistics: {
all: BlitzStats;
clan: ClanStats;
rating?: RatingStats;
frags: null | number;
};
}
export interface AccountInfo {
[accountId: number]: IndividualAccountInfo;
}
export async function getAccountInfo<
Ids extends number | number[],
ReturnType = Ids extends number
? IndividualAccountInfo
: IndividualAccountInfo[],
>(
region: Region,
ids: Ids,
extra: string[] = [],
params: FetchBlitzParams = {}
) {
const object = await fetchBlitz<AccountInfo>(region, "account/info", {
account_id: normalizeIds(ids),
extra: extra.length === 0 ? undefined : extra.join(","),
...params,
});
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
@@ -0,0 +1,46 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import type { Region } from "./regions";
type IndividualClanAccountInfo = {
account_id: number;
joined_at: number;
clan_id: number;
role: string;
account_name: string;
clan?: {
members_count: number;
name: string;
created_at: number;
tag: string;
clan_id: number;
emblem_set_id: number;
};
} | null;
interface ClanAccountInfo {
[accountId: number]: IndividualClanAccountInfo;
}
export async function getClanAccountInfo<
Ids extends number | number[],
ReturnType = Ids extends number
? IndividualClanAccountInfo
: IndividualClanAccountInfo[],
>(region: Region, ids: Ids, extra: string[] = []) {
const object = await fetchBlitz<ClanAccountInfo>(
region,
"clans/accountinfo",
{
account_id: normalizeIds(ids),
extra: extra.length === 0 ? undefined : extra.join(","),
}
);
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
+51
View File
@@ -0,0 +1,51 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import type { Region } from "./regions";
interface IndividualClanInfo {
recruiting_options: {
vehicles_level: number;
wins_ratio: number;
average_battles_per_day: number;
battles: number;
average_damage: number;
};
members_count: number;
name: string;
creator_name: string;
clan_id: number;
created_at: number;
updated_at: number;
leader_name: string;
members_ids: number[];
recruiting_policy: string;
tag: string;
is_clan_disbanded: boolean;
old_name: string;
emblem_set_id: number;
creator_id: number;
motto: string;
renamed_at: number;
old_tag: null;
leader_id: number;
description: string;
}
interface ClanInfo {
[clanId: number]: IndividualClanInfo;
}
export async function getClanInfo<
Ids extends number | number[],
ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[],
>(region: Region, ids: Ids) {
const object = await fetchBlitz<ClanInfo>(region, "clans/info", {
clan_id: normalizeIds(ids),
});
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
+16
View File
@@ -0,0 +1,16 @@
import type { TanksStats } from "../types";
import { type FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
import type { Region } from "./regions";
export async function getTankStats(
region: Region,
id: number,
params?: FetchBlitzParams
) {
const tankStats = await fetchBlitz<TanksStats>(region, "tanks/stats", {
account_id: id,
...params,
});
return tankStats[id];
}
+10
View File
@@ -3,13 +3,21 @@ export * from "./clans";
export * from "./classes";
export * from "./constants";
export * from "./equipmentPrice";
export * from "./fetchBlitz";
export * from "./fetchGlossary";
export * from "./generateStats";
export * from "./getAccountAchievements";
export * from "./getAccountInfo";
export * from "./getClanAccountInfo";
export * from "./getClanInfo";
export * from "./getLeagueFromScore";
export * from "./getRatingInfo";
export * from "./getRatingLeague";
export * from "./getRatingNeighbors";
export * from "./getTankStats";
export * from "./idToRegion";
export * from "./isOnGoingRatingSeason";
export * from "./isValidBlitzId";
export * from "./nations";
export * from "./normalizeExtra";
export * from "./normalizeIds";
@@ -19,5 +27,7 @@ export * from "./readDVPL";
export * from "./regions";
export * from "./regionSubdomainToRegion";
export * from "./regionToRegionSubdomain";
export * from "./searchClansAcrossRegions";
export * from "./searchCurrentRatingPlayers";
export * from "./searchPlayersAcrossRegions";
export * from "./toUniqueId";
+32
View File
@@ -0,0 +1,32 @@
import { assertSecret } from "../blitzkit";
import { idToRegion } from "./idToRegion";
import { Region } from "./regions";
export async function isValidBlitzId(id: number, token?: string) {
let region: Region;
try {
region = idToRegion(id);
} catch (error) {
return false;
}
const json = await fetch(
`https://api.wotblitz.${region}/wotb/account/info/?account_id=${id}&application_id=${assertSecret(
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
)}${
token === undefined ? "" : `&access_token=${token}`
}&fields=account_id%2C-account_id`
).then(
(response) =>
response.json() as Promise<
| {
status: "ok";
data: Record<number, {} | null>;
}
| { status: "error" }
>
);
return json.status === "ok" && json.data[id] !== null;
}
@@ -0,0 +1,51 @@
import type { ClanList } from "./clans";
import { fetchBlitz } from "./fetchBlitz";
import type { Region } from "./regions";
export async function searchClansAcrossRegions(search: string, limit = 25) {
const trimmed = search.trim();
if (trimmed.length < 3 && trimmed.length > 100) return [];
const normalizedLimit = Math.floor(limit / 3);
// TODO: remove splice once wargaming fixes the functionality of the limit felid
return (
await Promise.all([
fetchBlitz<ClanList>("com", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "com" as Region,
}))
.splice(0, normalizedLimit)
),
fetchBlitz<ClanList>("eu", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "eu" as Region,
}))
.splice(0, normalizedLimit)
),
fetchBlitz<ClanList>("asia", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "asia" as Region,
}))
.splice(0, normalizedLimit)
),
])
)
.filter(Boolean)
.flat();
}
@@ -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";
+5
View File
@@ -0,0 +1,5 @@
import { GameDefinitions } from "../../protos";
export abstract class BlitzKitAPI {
abstract game(): Promise<GameDefinitions>;
}
+9
View File
@@ -0,0 +1,9 @@
import { fetchPB } from "../../protobuf";
import { GameDefinitions } from "../../protos";
import { BlitzKitAPI } from "./base";
export class ClientBlitzKitAPI extends BlitzKitAPI {
game() {
return fetchPB("/api/definitions/game.pb", GameDefinitions);
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from "./base";
export * from "./client";
export * from "./server";
+69
View File
@@ -0,0 +1,69 @@
import {
AbstractVFS,
AvailableNationsYaml,
SquadBattleTypeStylesYaml,
} from "@blitzkit/core";
import { GameDefinitions } from "../../protos";
import { BlitzKitAPI } from "./base";
export class ServerBlitzKitAPI extends BlitzKitAPI {
vfs: AbstractVFS;
constructor(vfs: AbstractVFS) {
super();
this.vfs = vfs;
}
async game() {
const versionFile = await this.vfs.text("Data/version.txt");
const version = versionFile.split(" ")[0];
const availableNations = await this.vfs.yaml<AvailableNationsYaml>(
"Data/available_nations.yaml",
);
const nations = availableNations.available_nations;
const game: GameDefinitions = {
version,
nations,
gameModes: {},
roles: {},
};
const squadBattleTypeStyles =
await this.vfs.yaml<SquadBattleTypeStylesYaml>(
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`,
);
for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g,
)) {
const id = Number(match[1]);
const name = getString(match[2]);
gameModeNativeNames[match[3]] = id;
game.gameModes[id] = {
name,
};
}
Object.entries(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 game;
}
}
@@ -0,0 +1,4 @@
export function assertSecret<Type>(secret?: Type) {
if (secret === undefined) throw new Error("Missing secret");
return secret;
}
+5
View File
@@ -0,0 +1,5 @@
import { assertSecret } from "./assertSecret";
export function asset(path: string) {
return `${assertSecret(import.meta.env.PUBLIC_ASSET_BASE)}/${path}`;
}
@@ -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,23 @@
import { fetchPB } from '../../protobuf';
import { AverageDefinitions } from '../../protos';
import { asset } from '../asset';
export interface AverageDefinitionsManifest {
version: 1;
/**
* epoch in days
*/
latest: number;
}
export async function fetchAverageDefinitions() {
const manifestResponse = await fetch(asset('averages/manifest.json'));
const manifestJson =
(await manifestResponse.json()) as AverageDefinitionsManifest;
return await fetchPB(
asset(`averages/${manifestJson.latest}.pb`),
AverageDefinitions,
);
}
export * from './constants';
@@ -0,0 +1,7 @@
import { fetchPB } from '../protobuf';
import { CamouflageDefinitions } from '../protos';
import { asset } from './asset';
export function fetchCamouflageDefinitions() {
return fetchPB(asset('definitions/camouflages.pb'), CamouflageDefinitions);
}
@@ -0,0 +1,7 @@
import { fetchPB } from '../protobuf';
import { ConsumableDefinitions } from '../protos';
import { asset } from './asset';
export function fetchConsumableDefinitions() {
return fetchPB(asset('definitions/consumables.pb'), ConsumableDefinitions);
}
@@ -0,0 +1,12 @@
import { asset } from './asset';
export interface DiscoveredIdsDefinitions {
time: number;
chunks: number;
count: number;
}
export async function fetchDiscoveredIdsDefinitions() {
const response = await fetch(asset('ids/manifest.json'));
return response.json() as Promise<DiscoveredIdsDefinitions>;
}
@@ -0,0 +1,7 @@
import { fetchPB } from '../protobuf';
import { EquipmentDefinitions } from '../protos';
import { asset } from './asset';
export function fetchEquipmentDefinitions() {
return fetchPB(asset('definitions/equipment.pb'), EquipmentDefinitions);
}
+7
View File
@@ -0,0 +1,7 @@
import { fetchPB } from '../protobuf';
import { Gallery } from '../protos';
import { asset } from './asset';
export function fetchGallery() {
return fetchPB(asset('definitions/gallery.pb'), Gallery);
}
@@ -0,0 +1,49 @@
import { getRatingInfo } from "../blitz/getRatingInfo";
import { assertSecret } from "./assertSecret";
interface GitHubTrees {
tree: { path: string; url: string }[];
}
let cachedLatestArchivedSeasonNumber: number | null = null;
export async function getArchivedLatestSeasonNumber() {
if (cachedLatestArchivedSeasonNumber !== null) {
return cachedLatestArchivedSeasonNumber;
}
const ratingInfo = await getRatingInfo("com");
if (ratingInfo.detail) {
const regionsURL = (
(await fetch(
`https://api.github.com/repos/${assertSecret(
import.meta.env.PUBLIC_ASSET_REPO
)}/git/trees/main`
).then((response) => response.json())) as GitHubTrees
).tree.find(({ path }) => path === "regions")!.url;
const comURL = (
(await fetch(regionsURL).then((response) =>
response.json()
)) as GitHubTrees
).tree.find(({ path }) => path === "com")!.url;
const ratingURL = (
(await fetch(comURL).then((response) => response.json())) as {
tree: [{ url: string }];
}
).tree[0].url;
const latestSeason = Math.max(
...(
(await fetch(ratingURL).then((response) => response.json())) as {
tree: { path: string }[];
}
).tree.map(({ path }) => Number(path))
);
cachedLatestArchivedSeasonNumber = latestSeason;
} else {
cachedLatestArchivedSeasonNumber = ratingInfo.current_season - 1;
}
return cachedLatestArchivedSeasonNumber;
}
@@ -0,0 +1,23 @@
import type { Region } from "../blitz/regions";
import { RatingLeaderboard } from "../protos";
import { asset } from "./asset";
const cache: Record<Region, Record<number, RatingLeaderboard>> = {
asia: {},
eu: {},
com: {},
};
export async function getArchivedRatingLeaderboard(
region: Region,
season: number
) {
if (!cache[region][season]) {
const info = await fetch(asset(`regions/${region}/rating/${season}.pb`))
.then((response) => response.arrayBuffer())
.then((buffer) => RatingLeaderboard.decode(new Uint8Array(buffer)));
cache[region][season] = info;
}
return cache[region][season];
}
+29
View File
@@ -1,22 +1,51 @@
export * from "./assertSecret";
export * from "./asset";
export * from "./availableProvisions";
export * from "./averageDefinitions";
// export * from './camouflageDefinitions';
// export * from './checkConsumableProvisionInclusivity';
export * from "./coefficient";
export * from "./consumableDefinitions";
// export * from './context';
export * from "./api";
export * from "./createDefaultSkills";
export * from "./degressiveStat";
export * from "./discoveredIdDefinitions";
export * from "./emblemURL";
export * from "./equipmentDefinitions";
export * from "./eventManager";
export * from "./getArchivedLatestSeasonNumber";
export * from "./sluggify";
export * from "./vfs";
// export * from './getArchivedRatingInfo';
export * from "./getArchivedRatingLeaderboard";
// export * from './getArchivedRatingMidnightLeaderboard';
export * from "./getTimeDaysAgo";
export * from "./isExplosive";
export * from "./mapDefinitions";
export * from "./modelDefinitions";
export * from "./normalizeBoundingBox";
// export * from './parseBkni';
// export * from './patientFetch';
export * from "./patientFetchJSON";
export * from "./progressiveStat";
export * from "./provisionDefinitions";
export * from "./resolveDpm";
export * from "./resolvePenetrationCoefficient";
export * from "./reviews";
export * from "./skillDefinitions";
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 './withCORSProxy';
export * from "./createDefaultProvisions";
export * from "./gallery";
export * from "./metaSortTank";
export * from "./popularTanks";
export * from "./romanize";
export * from "./youtubers";
@@ -0,0 +1,7 @@
import { fetchPB } from '../protobuf';
import { MapDefinitions } from '../protos';
import { asset } from './asset';
export function fetchMapDefinitions() {
return fetchPB(asset('definitions/maps.pb'), MapDefinitions);
}
@@ -0,0 +1,7 @@
import { fetchPB } from '../protobuf';
import { ModelDefinitions } from '../protos';
import { asset } from './asset';
export function fetchModelDefinitions() {
return fetchPB(asset('definitions/models.pb'), ModelDefinitions);
}
@@ -0,0 +1,7 @@
import { fetchPB } from '../protobuf';
import { PopularTanks } from '../protos';
import { asset } from './asset';
export function fetchPopularTanks() {
return fetchPB(asset('definitions/popular-tanks.pb'), PopularTanks);
}
@@ -0,0 +1,7 @@
import { fetchPB } from '../protobuf';
import { ProvisionDefinitions } from '../protos';
import { asset } from './asset';
export function fetchProvisionDefinitions() {
return fetchPB(asset('definitions/provisions.pb'), ProvisionDefinitions);
}
@@ -8,12 +8,12 @@ export function resolvePenetrationCoefficient(
) {
const calibrated = calibratedShells
? type === ShellType.SHELL_TYPE_AP
? 1.08
? 1.06
: type === ShellType.SHELL_TYPE_APCR
? 1.05
? 1.06
: type === ShellType.SHELL_TYPE_HEAT
? 1.13
: 1.08
? 1.07
: 1.07
: 1;
const equalizer = (equalize ? equalizerObj?.penetration : undefined) ?? 1;
+7
View File
@@ -0,0 +1,7 @@
import { fetchPB } from '../protobuf';
import { Reviews } from '../protos';
import { asset } from './asset';
export function fetchReviews() {
return fetchPB(asset('definitions/reviews.pb'), Reviews);
}
@@ -0,0 +1,7 @@
import { fetchPB } from '../protobuf';
import { SkillDefinitions } from '../protos';
import { asset } from './asset';
export function fetchSkillDefinitions() {
return fetchPB(asset('definitions/skills.pb'), SkillDefinitions);
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { deburr } from "lodash-es";
import { slugify } from "transliteration";
const nonAlphanumericRegex = /[^a-z0-9]/g;
const multipleDashesRegex = /--+/g;
@@ -6,7 +6,7 @@ const trailingDashRegex = /-$/g;
const leadingDashRegex = /^-/g;
export function sluggify(value: string) {
let slug = deburr(value).toLowerCase();
let slug = slugify(value);
slug = slug.replaceAll(nonAlphanumericRegex, "-");
slug = slug.replaceAll(multipleDashesRegex, "-");
slug = slug.replaceAll(trailingDashRegex, "");
@@ -0,0 +1,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,9 @@
import { fetchPB } from '../../protobuf';
import { TankDefinitions } from '../../protos';
import { asset } from '../asset';
export function fetchTankDefinitions() {
return fetchPB(asset('definitions/tanks.pb'), TankDefinitions);
}
export * from './constants';
+7
View File
@@ -0,0 +1,7 @@
import { asset } from "./asset";
export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 };
export function tankIcon(id: number, size: "big" | "small" = "big") {
return asset(`icons/tanks/${size}/${id}.webp`);
}
+52
View File
@@ -0,0 +1,52 @@
import locales from "@blitzkit/i18n/locales.json";
import { deburr } from "lodash-es";
import { I18nString } from "../protos";
import { fetchCamouflageDefinitions } from "./camouflageDefinitions";
import { fetchTankDefinitions } from "./tankDefinitions";
export async function fetchTankNames() {
const [tankDefinitions, camouflageDefinitions] = await Promise.all([
fetchTankDefinitions(),
fetchCamouflageDefinitions(),
]);
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",
];
+5
View File
@@ -0,0 +1,5 @@
export * from "./abstract";
export * from "./local";
export * from "./mixed";
export * from "./steam";
export * from "./zip";
@@ -24,7 +24,11 @@ export class LocalVFS extends AbstractVFS {
}
async dir(path: string) {
return await readdir(`${this.base}/${path}`);
try {
return await readdir(`${this.base}/${path}`);
} catch (_) {
return [];
}
}
dispose() {}
File renamed without changes.
+1 -1
View File
@@ -4,7 +4,7 @@ export * from "./imgur";
export * from "./math";
export * from "./patreon";
export * from "./protobuf";
export * from "./protos";
export * from "./statistics";
export * from "./streams";
export * from "./types";
export * from "./secrets";
-3
View File
@@ -1,3 +0,0 @@
export function copySign(a: number, b: number) {
return b >= 0 ? Math.abs(a) : -Math.abs(a);
}
-1
View File
@@ -3,7 +3,6 @@ export * from "./fisherYates";
export * from "./formatCompact";
export * from "./isNumber";
export * from "./NaNFallback";
export * from "./normalizeAngleDeg";
export * from "./normalizeAngleRad";
export * from "./normalToEuler";
export * from "./numberToRomanNumeral";
@@ -1,6 +0,0 @@
export function normalizeAngleDeg(angle: number) {
const newAngle = angle % 360;
if (newAngle > 360) return newAngle - 360;
if (newAngle < -360) return newAngle + 360;
return newAngle;
}
+5 -5
View File
@@ -1,11 +1,11 @@
import { secret } from "../secrets";
import { assertSecret } from '../blitzkit/assertSecret';
export function patreonLoginUrl() {
const params = new URLSearchParams({
response_type: "code",
client_id: secret("PUBLIC_PATREON_CLIENT_ID"),
redirect_uri: secret("PUBLIC_PATREON_REDIRECT_URI"),
scope: "identity",
response_type: 'code',
client_id: assertSecret(import.meta.env.PUBLIC_PATREON_CLIENT_ID),
redirect_uri: assertSecret(import.meta.env.PUBLIC_PATREON_REDIRECT_URI),
scope: 'identity',
});
return `https://www.patreon.com/oauth2/authorize?${params}`;
-5
View File
@@ -1,5 +0,0 @@
import { PassableSecretName } from "./secret";
export function hasSecret(name: PassableSecretName) {
return name in import.meta.env;
}
-3
View File
@@ -1,3 +0,0 @@
export * from "./secret";
export * from "./hasSecret";
export * from "./wellKnown";
-35
View File
@@ -1,35 +0,0 @@
import { hasSecret } from "./hasSecret";
import { WellKnownSecret } from "./wellKnown";
export enum SecretType {
String,
Number,
}
export type PassableSecretName = WellKnownSecret;
export function secret(
name: PassableSecretName,
type?: SecretType.String,
): string;
export function secret(
name: PassableSecretName,
type: SecretType.Number,
): number;
export function secret(name: PassableSecretName, type = SecretType.String) {
if (!hasSecret(name)) {
throw new Error(`Secret "${name}" not defined`);
}
const value = import.meta.env[name];
switch (type) {
case SecretType.String:
return String(value);
case SecretType.Number:
return Number(value);
}
}
-17
View File
@@ -1,17 +0,0 @@
export type WellKnownSecret =
// development set-up
| "TEMP"
// website
| "BUILD_CHUNK"
| "TEXTURE_CHUNKS"
// wotb
| "WOTB_SERVER"
| "WOTB_VERSION"
| "PUBLIC_WOTB_CLIENT"
| "WOTB_USMAP"
// google
| "GOOGLE_APPLICATION_CREDENTIALS"
| "PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID"
// patreon
| "PUBLIC_PATREON_CLIENT_ID"
| "PUBLIC_PATREON_REDIRECT_URI";
@@ -0,0 +1,22 @@
import { fetchTankDefinitions } from '../blitzkit';
import type { BlitzStats } from './compositeStats/constants';
export async function getTierWeights(
stats: Record<number, BlitzStats>,
order: number[] = Object.keys(stats).map(parseInt),
) {
const tankDefinitions = await fetchTankDefinitions();
const tierWeights: TierWeightsRecord = {};
order.forEach((id) => {
const entry = tankDefinitions.tanks[id];
if (!entry) return;
if (!tierWeights[entry.tier]) tierWeights[entry.tier] = 0;
tierWeights[entry.tier]! += stats[id].battles;
});
return tierWeights;
}
export type TierWeightsRecord = Partial<Record<number, number>>;
+9 -8
View File
@@ -1,8 +1,9 @@
export * from "./calculateWN8";
export * from "./compositeStats";
export * from "./deltaBkrlBlitzStats";
export * from "./deltaTankStats";
export * from "./getBkniPercentile";
export * from "./getWN8Percentile";
export * from "./percentiles";
export * from "./sumAllStats";
export * from './calculateWN8';
export * from './compositeStats';
export * from './deltaBkrlBlitzStats';
export * from './deltaTankStats';
export * from './getBkniPercentile';
export * from './getTierWeights';
export * from './getWN8Percentile';
export * from './percentiles';
export * from './sumAllStats';
@@ -0,0 +1,3 @@
export interface AvailableNationsYaml {
available_nations: string[];
}
+4 -2
View File
@@ -1,2 +1,4 @@
export * from './glossary';
export * from './tanksStats';
export * from "./availableNationsYaml";
export * from "./glossary";
export * from "./squadBattleTypeStylesYaml";
export * from "./tanksStats";
@@ -0,0 +1,9 @@
export interface SquadBattleTypeStylesYaml {
Prototypes: {
components: {
UIDataLocalBindingsComponent: {
data: [string, string, string][];
};
};
}[];
}
-3
View File
@@ -1,3 +0,0 @@
[*.cs]
indent_style = space
indent_size = 2
-57
View File
@@ -1,57 +0,0 @@
## A streamlined .gitignore for modern .NET projects
## including temporary files, build results, and
## files generated by popular .NET tools. If you are
## developing with Visual Studio, the VS .gitignore
## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
## has more thorough IDE-specific entries.
##
## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# dotenv environment variables file
.env
# Others
~$*
*~
CodeCoverage/
# MSBuild Binary and Structured Log
*.binlog
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
-323
View File
@@ -1,323 +0,0 @@
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using BlitzKit.Game.Models;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Materials;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures;
using CUE4Parse_Conversion.UEFormat.Enums;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Component;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Assets.Objects.Properties;
using CUE4Parse.UE4.Objects.Engine;
using CUE4Parse.UE4.Objects.GameplayTags;
using CUE4Parse.UE4.Objects.UObject;
using Microsoft.JavaScript.NodeApi;
using SharpGLTF.Materials;
using SkiaSharp;
namespace BlitzKit.Game.JSExport;
[JSExport]
public partial class GameInterface
{
private readonly BlitzFileProvider provider;
public HashSet<string> files;
public int textureChunks;
public GameInterface(string directory, string map, string temp, int textureChunks)
{
provider = new(directory, map, temp);
files = [.. provider.Files.Keys];
this.textureChunks = textureChunks;
}
readonly Dictionary<string, (int, string)> texturePaths = [];
readonly string textureCache = $"{temp}textures.json";
[GeneratedRegex(@"\+ClientDiscoveryBindingSources\s*=\s*\(Type=\w+,URL=""(.+)""\)")]
private static partial Regex DiscoveryRegex();
public string[] DiscoveryUrls()
{
if (provider.TryGetGameFile("Blitz/Config/DefaultEngine.ini", out var file))
{
var bytes = file.Read();
var content = Encoding.UTF8.GetString(bytes);
var matches = DiscoveryRegex().Matches(content);
return [.. matches.Select(match => match.Groups[1].Value)];
}
throw new FileNotFoundException("Engine config not found");
}
public void DiscoverTextures(string[] tankTags)
{
if (File.Exists(textureCache))
{
Console.WriteLine("Found textures cache");
var content = File.ReadAllText(textureCache);
var cached = JsonSerializer.Deserialize<Dictionary<string, (int, string)>>(content);
foreach (var (key, value) in cached)
{
texturePaths[key] = value;
}
return;
}
Console.WriteLine("Textures cache not found, building...");
var options = new ExporterOptions() { TextureFormat = ETextureFormat.Jpeg };
foreach (var tag in tankTags)
{
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
foreach (var group in tankPartPrefixes.Values)
{
var dataTable = pda.Get<UDataTable>(group);
foreach (var row in dataTable.RowMap.Values)
{
var visualData = row.Get<UObject>("VisualData");
var meshSettings = visualData.Get<FStructFallback>("MeshSettings");
foreach (var property in meshSettings.Properties)
{
if (meshSettings.TryGet<UStaticMesh>(property.Name.Text, out var mesh))
{
mesh.TryConvert(out var convertedMesh);
var lod0 = convertedMesh.LODs.Find(lod => !lod.SkipLod);
if (lod0 == null || lod0.Sections == null)
{
continue;
}
foreach (var section in lod0.Sections.Value)
{
if (section.Material == null)
{
continue;
}
var materialInterface = section.Material.Load<UMaterialInterface>();
if (materialInterface == null)
{
continue;
}
var materialData = new MaterialData() { Parameters = new() };
materialInterface.GetParams(materialData.Parameters, options.MaterialFormat);
foreach (var parameterTexture in materialData.Parameters.Textures)
{
if (MonoGltf.knownChannels.TryGetValue(parameterTexture.Key, out var channel))
{
var path = parameterTexture.Value.GetPathName();
var modulus = MonoGltf.TextureModulus(path, textureChunks);
var name = Path.GetFileNameWithoutExtension(path);
texturePaths[name] = (modulus, path);
}
}
}
}
}
}
}
}
Console.WriteLine($"Discovered {texturePaths.Count} textures");
Directory.CreateDirectory(Path.GetDirectoryName(textureCache));
var json = JsonSerializer.Serialize(texturePaths);
File.WriteAllText(textureCache, json);
Console.WriteLine("Saved textures cache");
}
public List<(int, string)> Textures =>
[.. texturePaths.Select(texture => (texture.Value.Item1, texture.Key))];
public byte[] Texture(string name)
{
var (_, path) = texturePaths[name];
var texture = provider.LoadPackageObject<UTexture2D>(path);
var isRMAO = name.EndsWith("_RMAO");
var postProcess = isRMAO ? ImagePostProcess.RMAO : ImagePostProcess.None;
return provider.Image(texture, postProcess);
}
public HashSet<string> Files => files;
public byte[] TankBigIcon(string tag)
{
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
var icon = pda.Get<FSoftObjectPath>("BigIcon");
return provider.Image(icon);
}
[GeneratedRegex(@"T_UI_Flag_(\w+)_S")]
private static partial Regex FlagNameRegex();
public byte[] Flag(string nation)
{
var flagsBase = "Blitz/Content/UI/Textures/Flag/TankCard/";
foreach (var path in provider.Files.Keys)
{
if (!path.StartsWith(flagsBase))
{
continue;
}
var fileName = Path.GetFileNameWithoutExtension(path);
var match = FlagNameRegex().Match(fileName);
if (
match.Success
&& match.Groups[1].Value.Equals(nation, StringComparison.CurrentCultureIgnoreCase)
)
{
if (provider.TryLoadPackageObject<UTexture2D>($"{path}.{fileName}", out var uTexture))
{
CTexture? cTexture = uTexture.Decode(ETexturePlatform.DesktopMobile);
if (cTexture is null)
{
Console.WriteLine($"No flag found for {nation}");
return [];
}
SKBitmap bitmap = cTexture.ToSkBitmap();
SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, 80);
return data.ToArray();
}
Console.WriteLine($"No flag found for {nation}");
return [];
}
}
Console.WriteLine($"No flag found for {nation}");
return [];
}
public byte[] ConsumableIcon(string tag)
{
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
var icon = pda.Get<FSoftObjectPath>("Icon");
return provider.Image(icon);
}
public byte[] EquipmentIcon(string tag)
{
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
var icon = pda.Get<FSoftObjectPath>("Icon");
return provider.Image(icon);
}
readonly Dictionary<string, string> tankPartPrefixes = new()
{
{ "chassis", "DT_Chassis" },
{ "hull", "DT_Hulls" },
{ "turret", "DT_Turrets" },
{ "gun", "DT_Guns" },
};
public byte[] TankPart(string tag, string part)
{
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
UDataTable? dataTable = null;
foreach (var (prefix, name) in tankPartPrefixes)
{
if (!part.StartsWith(prefix))
{
continue;
}
dataTable = pda.Get<UDataTable>(name);
break;
}
if (dataTable == null)
{
Console.WriteLine($"Unknown tank part (no matching data table): {part}");
return [];
}
dataTable.TryGetDataTableRow(part, StringComparison.Ordinal, out var row);
if (row == null)
{
Console.WriteLine($"Unknown tank part (no matching row): {part}");
return [];
}
var visualData = row.Get<UObject>("VisualData");
var meshSettings = visualData.Get<FStructFallback>("MeshSettings");
// var meshes = CollectMeshes(meshSettings);
// var gltf = new MonoGltf(meshes);
var gltf = new MonoGltf(meshSettings, textureChunks);
return gltf.Write($"{tag}/{part}");
}
static List<(string, UObject)> CollectMeshes(FStructFallback settings)
{
var meshes = new List<(string, UObject)>();
foreach (var property in settings.Properties)
{
var key = property.Name.Text;
if (settings.TryGet<UStaticMesh>(key, out var staticMesh))
{
meshes.Add((key, staticMesh));
continue;
}
if (settings.TryGet<USkeletalMesh>(key, out var skeletalMesh))
{
meshes.Add((key, skeletalMesh));
continue;
}
if (settings.TryGet<FStructFallback[]>(key, out var array))
{
foreach (var item in array)
{
meshes.AddRange(CollectMeshes(item));
}
continue;
}
// Console.WriteLine(
// $"{key} -> {property.Tag} ({property.TagData}) [{property.PropertyTagFlags}]"
// );
}
return meshes;
}
}
-130
View File
@@ -1,130 +0,0 @@
using System.Text.RegularExpressions;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.Compression;
using CUE4Parse.FileProvider;
using CUE4Parse.MappingsProvider;
using CUE4Parse.UE4.AssetRegistry;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Exceptions;
using CUE4Parse.UE4.Objects.Engine;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.UE4.Readers;
using CUE4Parse.UE4.Versions;
using CUE4Parse.Utils;
using SkiaSharp;
namespace BlitzKit.Game.Models;
public enum ImagePostProcess
{
None,
RMAO,
}
public partial class BlitzFileProvider : DefaultFileProvider
{
[GeneratedRegex(@"\(GameplayTags=\(\(TagName=""(.+)""\)\)\)")]
private static partial Regex GameplayTagsRegex();
private readonly Dictionary<string, string> discovered = [];
public BlitzFileProvider(string directory, string map, string temp)
: base(
directory: directory,
searchOption: SearchOption.AllDirectories,
versions: new(EGame.GAME_UE5_5),
pathComparer: StringComparer.OrdinalIgnoreCase
)
{
MappingsContainer = new FileUsmapTypeMappingsProvider(map);
Initialize();
Mount();
// string oodlePath = $"{temp}/oodle.so";
// OodleHelper.DownloadOodleDll(ref oodlePath!);
OodleHelper.Initialize("../../stupid/oodle.so");
if (TryGetGameFile("Blitz/AssetRegistry.bin", out var file))
{
var bytes = file.Read();
var archive = new FByteArchive(file.Path, bytes, Versions);
var state = new FAssetRegistryState(archive);
foreach (var asset in state.PreallocatedAssetDataBuffers)
{
if (asset.TagsAndValues.TryGetValue("DiscoveryTags", out var tags))
{
var match = GameplayTagsRegex().Match(tags);
if (!match.Success)
{
continue;
}
var tag = match.Groups[1].Value;
discovered[tag] = asset.ObjectPath;
}
}
}
}
public T Discovered<T>(string path)
where T : UObject
{
return LoadPackageObject<T>(discovered[path]);
}
public UPrimaryDataAsset PDA(string path)
{
var name = Path.GetFileNameWithoutExtension(path);
var combined = $"{path}.{name}";
return LoadPackageObject<UPrimaryDataAsset>(combined);
}
public byte[] Image(FSoftObjectPath path)
{
var uTexture = path.Load<UTexture2D>();
return Image(uTexture);
}
private readonly SKEncodedImageFormat imageFormat = SKEncodedImageFormat.Webp;
private readonly int imageQuality = 80;
public byte[] Image(UTexture2D uTexture, ImagePostProcess postProcess = ImagePostProcess.None)
{
var cTexture = uTexture.Decode(ETexturePlatform.DesktopMobile);
ArgumentNullException.ThrowIfNull(cTexture);
var bitmap = cTexture.ToSkBitmap();
return Image(bitmap, postProcess);
}
private byte[] Image(SKBitmap bitmap, ImagePostProcess postProcess)
{
if (postProcess != ImagePostProcess.None)
{
for (int y = 0; y < bitmap.Height; y++)
{
for (int x = 0; x < bitmap.Width; x++)
{
var color = bitmap.GetPixel(x, y);
switch (postProcess)
{
case ImagePostProcess.RMAO:
bitmap.SetPixel(x, y, new SKColor(color.Blue, color.Red, color.Green, color.Alpha));
break;
}
}
}
}
var data = bitmap.Encode(imageFormat, imageQuality);
return data.ToArray();
}
}
-386
View File
@@ -1,386 +0,0 @@
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Materials;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Meshes.glTF;
using CUE4Parse_Conversion.Meshes.PSK;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.Meshes;
using SharpGLTF.Geometry;
using SharpGLTF.Geometry.VertexTypes;
using SharpGLTF.Materials;
using SharpGLTF.Memory;
using SharpGLTF.Scenes;
using SharpGLTF.Schema2;
namespace BlitzKit.Game.Models;
using ConfiguredMeshBuilder = MeshBuilder<
VertexPositionNormalTangent,
VertexColorXTextureX,
VertexEmpty
>;
public class MonoGltf
{
readonly SceneBuilder scene = new();
readonly Dictionary<string, MaterialBuilder> materialMap = [];
static readonly MaterialBuilder emptyMaterial = new("empty_material");
static readonly ExporterOptions options = new() { TextureFormat = ETextureFormat.Jpeg };
public static readonly Dictionary<string, KnownChannel[]> knownChannels = new()
{
{ "BaseColor", [KnownChannel.BaseColor] },
{ "Normal", [KnownChannel.Normal] },
{ "RMAO", [KnownChannel.MetallicRoughness, KnownChannel.Occlusion] },
// { "CDE", ?? },
};
static readonly byte[] stubBytes = File.ReadAllBytes("../game/stub/small.png");
static readonly float scaleCorrection = 0.01f;
readonly int textureChunks;
public MonoGltf(FStructFallback settings, int textureChunks)
{
var root = new NodeBuilder("root");
this.textureChunks = textureChunks;
Traverse(settings, root);
scene.AddNode(root);
}
void Traverse(FStructFallback settings, NodeBuilder parent)
{
foreach (var property in settings.Properties)
{
var key = property.Name.Text;
if (settings.TryGet<UStaticMesh>(key, out var staticMesh))
{
AddMesh(parent.CreateNode(key), staticMesh);
continue;
}
if (settings.TryGet<USkeletalMesh>(key, out var skeletalMesh))
{
AddMesh(parent.CreateNode(key), skeletalMesh);
continue;
}
if (settings.TryGet<FStructFallback[]>(key, out var array))
{
foreach (var item in array)
{
Traverse(item, parent.CreateNode(key));
}
continue;
}
if (key == "BoneTransform")
{
var transform = settings.Get<FStructFallback>(key);
if (transform.TryGet<FQuat>("Rotation", out var rotation))
{
parent.WithLocalRotation(SwapYZ(rotation));
}
if (transform.TryGet<FVector>("Translation", out var translation))
{
parent.WithLocalTranslation(SwapYZ(translation) * scaleCorrection);
}
if (transform.TryGet<FVector>("Scale3D", out var scale))
{
parent.WithLocalScale(SwapYZ(scale));
}
continue;
}
// Console.WriteLine(
// $"{key} -> {property.Tag} ({property.TagData}) [{property.PropertyTagFlags}]"
// );
}
}
void AddMesh(NodeBuilder meshNode, UObject mesh)
{
if (mesh is UStaticMesh staticMesh)
{
staticMesh.TryConvert(out var convertedStaticMesh);
var lod0 = convertedStaticMesh.LODs.Find(lod => !lod.SkipLod)!;
var i = 0;
foreach (var section in lod0.Sections!.Value)
{
var sectionNode = meshNode.CreateNode($"{i++}");
var configured = ParseSection(section, lod0, lod0.Verts!);
scene.AddRigidMesh(configured, sectionNode);
}
return;
}
if (mesh is USkeletalMesh skeletalMesh)
{
skeletalMesh.TryConvert(out var convertedSkeletalMesh);
var lod0 = convertedSkeletalMesh.LODs.Find(lod => !lod.SkipLod)!;
var i = 0;
foreach (var section in lod0.Sections!.Value)
{
var sectionNode = meshNode.CreateNode($"{i++}");
var configured = ParseSection(section, lod0, lod0.Verts!);
scene.AddRigidMesh(configured, sectionNode);
}
return;
}
}
ConfiguredMeshBuilder ParseSection(CMeshSection section, CBaseMeshLod lod, CMeshVertex[] vertices)
{
var meshBuilder = new ConfiguredMeshBuilder();
MaterialBuilder materialBuilder;
if (section.Material == null || section.MaterialName == null)
{
materialBuilder = emptyMaterial;
}
else if (materialMap.TryGetValue(section.MaterialName, out MaterialBuilder? value))
{
materialBuilder = value;
}
else
{
var materialInterface = section.Material.Load<UMaterialInterface>()!;
var materialData = new MaterialData() { Parameters = new() };
materialInterface.GetParams(materialData.Parameters, options.MaterialFormat);
materialBuilder = new();
materialMap.Add(section.MaterialName, materialBuilder);
foreach (var parameterTexture in materialData.Parameters.Textures)
{
if (knownChannels.TryGetValue(parameterTexture.Key, out var channels))
{
var path = TexturePath(parameterTexture.Value.GetPathName(), textureChunks);
var lastIndex = stubBytes.Length - 1;
stubBytes[lastIndex] = (byte)(++stubBytes[lastIndex] % 255);
var uniqueBytes = (byte[])stubBytes.Clone();
var stubImage = new MemoryImage(uniqueBytes);
foreach (var channel in channels)
{
materialBuilder.WithChannelImage(channel, stubImage);
materialBuilder.GetChannel(channel).Texture.PrimaryImage.Name = path;
}
}
}
}
var primitive = meshBuilder.UsePrimitive(materialBuilder);
for (int faceIndex = 0; faceIndex < section.NumFaces; faceIndex++)
{
var wedgeIndex = new int[3];
for (var k = 0; k < wedgeIndex.Length; k++)
{
wedgeIndex[k] = (int)lod.Indices!.Value[section.FirstIndex + faceIndex * 3 + k];
}
var vertex1 = vertices[wedgeIndex[0]];
var vertex2 = vertices[wedgeIndex[1]];
var vertex3 = vertices[wedgeIndex[2]];
var (v1, v2, v3) = PrepareTriangles(vertex1, vertex2, vertex3);
var (c1, c2, c3) = PrepareUVsAndTexCoords(lod, vertex1, vertex2, vertex3, wedgeIndex);
primitive.AddTriangle((v1, c1), (v2, c2), (v3, c3));
}
return meshBuilder;
}
public static int TextureModulus(string path, int textureChunks)
{
var name = Path.GetFileNameWithoutExtension(path);
var bytes = Encoding.UTF8.GetBytes(name);
var hash = SHA256.HashData(bytes);
var value = BitConverter.ToUInt32(hash, 0);
var modulus = value % textureChunks;
return (int)modulus;
}
public static string TexturePath(string path, int textureChunks)
{
var modulus = TextureModulus(path, textureChunks);
var name = Path.GetFileNameWithoutExtension(path);
return $"../../../textures-{modulus}/{name}.webp";
}
public byte[] Write(string name)
{
var model = scene.ToGltf2();
model.Asset.Copyright = null;
model.Asset.Generator = null;
var mappedTextures = new Dictionary<MemoryImage, string>();
foreach (var image in model.LogicalImages)
{
mappedTextures[image.Content] = image.Name!;
image.Name = null;
}
var stream = new MemoryStream();
var context = WriteContext
.CreateFromStream(stream)
.WithSettingsFrom(
new()
{
ImageWriting = ResourceWriteMode.SatelliteFile,
ImageWriteCallback = (context, assetName, image) =>
{
return mappedTextures[image];
},
}
);
context.WriteBinarySchema2(name, model);
return stream.ToArray();
}
private static (
VertexPositionNormalTangent,
VertexPositionNormalTangent,
VertexPositionNormalTangent
) PrepareTriangles(CMeshVertex vert1, CMeshVertex vert2, CMeshVertex vert3)
{
var v1 = new VertexPositionNormalTangent(
SwapYZ(vert1.Position * scaleCorrection),
SwapYZAndNormalize((FVector)vert1.Normal),
SwapYZAndNormalize((Vector4)vert1.Tangent)
);
var v2 = new VertexPositionNormalTangent(
SwapYZ(vert2.Position * scaleCorrection),
SwapYZAndNormalize((FVector)vert2.Normal),
SwapYZAndNormalize((Vector4)vert2.Tangent)
);
var v3 = new VertexPositionNormalTangent(
SwapYZ(vert3.Position * scaleCorrection),
SwapYZAndNormalize((FVector)vert3.Normal),
SwapYZAndNormalize((Vector4)vert3.Tangent)
);
return (v1, v2, v3);
}
public static FVector SwapYZ(FVector vec)
{
var res = new FVector(vec.X, vec.Z, vec.Y);
return res;
}
public static Quaternion SwapYZ(Quaternion q)
{
return Quaternion.Normalize(new Quaternion(q.X, q.Z, q.Y, -q.W));
}
public static FVector SwapYZAndNormalize(FVector vec)
{
var res = SwapYZ(vec);
res.Normalize();
return res;
}
public static Vector4 SwapYZAndNormalize(Vector4 vec)
{
return Vector4.Normalize(new Vector4(vec.X, vec.Z, vec.Y, vec.W));
}
public static (
VertexColorXTextureX,
VertexColorXTextureX,
VertexColorXTextureX
) PrepareUVsAndTexCoords(
CBaseMeshLod lod,
CMeshVertex vert1,
CMeshVertex vert2,
CMeshVertex vert3,
int[] indices
)
{
return PrepareUVsAndTexCoords(
lod.VertexColors ?? new FColor[lod.NumVerts],
vert1,
vert2,
vert3,
lod.ExtraUV!.Value,
indices
);
}
public static (
VertexColorXTextureX,
VertexColorXTextureX,
VertexColorXTextureX
) PrepareUVsAndTexCoords(
FColor[] colors,
CMeshVertex vert1,
CMeshVertex vert2,
CMeshVertex vert3,
FMeshUVFloat[][] uvs,
int[] indices
)
{
var (uvs1, uvs2, uvs3) = PrepareUVs(vert1, vert2, vert3, uvs, indices);
var c1 = new VertexColorXTextureX((Vector4)colors[indices[0]] / 255, uvs1);
var c2 = new VertexColorXTextureX((Vector4)colors[indices[1]] / 255, uvs2);
var c3 = new VertexColorXTextureX((Vector4)colors[indices[2]] / 255, uvs3);
return (c1, c2, c3);
}
private static (List<Vector2>, List<Vector2>, List<Vector2>) PrepareUVs(
CMeshVertex vert1,
CMeshVertex vert2,
CMeshVertex vert3,
FMeshUVFloat[][] uvs,
int[] indices
)
{
var uvs1 = new List<Vector2>() { (Vector2)vert1.UV };
var uvs2 = new List<Vector2>() { (Vector2)vert2.UV };
var uvs3 = new List<Vector2>() { (Vector2)vert3.UV };
foreach (var uv in uvs)
{
uvs1.Add((Vector2)uv[indices[0]]);
uvs2.Add((Vector2)uv[indices[1]]);
uvs3.Add((Vector2)uv[indices[2]]);
}
return (uvs1, uvs2, uvs3);
}
}
-18
View File
@@ -1,18 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>BlitzKit.Game</RootNamespace>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NodeApiAssemblyJSModuleType>esm</NodeApiAssemblyJSModuleType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../cue4parse/CUE4Parse/CUE4Parse.csproj" />
<ProjectReference Include="../cue4parse/CUE4Parse-Conversion/CUE4Parse-Conversion.csproj" />
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.9.19" />
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.9.19" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
</ItemGroup>
</Project>
-3
View File
@@ -1,3 +0,0 @@
<Solution>
<Project Path="game.csproj" />
</Solution>
-12
View File
@@ -1,12 +0,0 @@
{
"name": "@blitzkit/game",
"private": true,
"type": "module",
"main": "src/index.ts",
"scripts": {
"build": "dotnet build"
},
"dependencies": {
"node-api-dotnet": "^0.9.19"
}
}
-1
View File
@@ -1 +0,0 @@
export * from "../bin/Debug/net9.0/linux-x64/game.js";
Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 B

+2
View File
@@ -1,2 +1,4 @@
export * from "./literals";
export * from "./strings";
export * from "./unwrapper";
export * from "./wrapper";
+8 -37
View File
@@ -1,45 +1,16 @@
export function literals(
string: string,
literals?: Record<string, string | number | undefined>,
literals?: Record<string, string | number | undefined>
) {
for (const key in literals) {
string = string.replaceAll(`{${key}}`, `${literals[key]}`);
const mutated = string.replaceAll(`{${key}}`, `${literals[key]}`);
if (import.meta.env.DEV && mutated === string) {
console.warn(`Could not find literal {${key}} in string "${string}"`);
}
string = mutated;
}
return string;
}
export function literalsArray<Type, Literal = Type | string>(
string: string,
literals?: Record<string, Literal>,
) {
if (!literals) return [string];
let array: (string | Literal)[] = [string];
for (const key in literals) {
const value = literals[key];
const next: (string | Literal)[] = [];
for (const chunk of array) {
if (typeof chunk !== "string") {
next.push(chunk);
continue;
}
const parts = chunk.split(`{${key}}`);
for (let index = 0; index < parts.length; index++) {
if (parts[index]) next.push(parts[index]);
if (index < parts.length - 1) {
next.push(value);
}
}
}
array = next;
}
return array;
}
+67 -2
View File
@@ -1,3 +1,68 @@
import type en from "../strings/en.json";
import { merge } from "ts-deepmerge";
import locales from "../locales.json";
import en from "../strings/en.json";
import es from "../strings/es.json";
import fr from "../strings/fr.json";
import ja from "../strings/ja.json";
import pl from "../strings/pl.json";
import pt from "../strings/pt.json";
import ru from "../strings/ru.json";
import uk from "../strings/uk.json";
import zh from "../strings/zh.json";
export type Strings = typeof en;
export type BlitzKitStrings = typeof en;
const stringsRaw: Record<string, TranslationTree> = {
en,
es,
fr,
ja,
pl,
pt,
ru,
uk,
zh,
};
export const STRINGS: Record<string, BlitzKitStrings> = {};
for (const locale in stringsRaw) {
if (locale === locales.default) {
STRINGS[locales.default] = stringsRaw[locales.default] as BlitzKitStrings;
continue;
}
const localeStrings = stringsRaw[locale];
STRINGS[locale] = merge(
stringsRaw[locales.default],
localeStrings,
) as BlitzKitStrings;
}
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<string, string> = {
en: "en",
es: "es",
fr: "fr",
ja: "ja",
pt: "pt",
ru: "ru",
uk: "uk",
zh: "zh-Hans",
pl: "pl",
};
export const SUPPORTED_LOCALE_FLAGS: Record<string, string> = {
en: "🇺🇸",
es: "🇪🇸",
fr: "🇫🇷",
ja: "🇯🇵",
pt: "🇵🇹",
ru: "🇷🇺",
uk: "🇺🇦",
zh: "🇨🇳",
pl: "🇵🇱",
};
export type TranslationTree = { [key: string]: TranslationNode };
export type TranslationNode = string | TranslationTree;
+12
View File
@@ -0,0 +1,12 @@
import { I18nString } from "../../core/src";
import locales from "../locales.json";
export function unwrapper(locale = locales.default) {
return function (i18nString: I18nString) {
if (locale in i18nString.locales) {
return i18nString.locales[locale];
}
return i18nString.locales[locales.default];
};
}
+15
View File
@@ -0,0 +1,15 @@
import locales from "../locales.json";
import { type BlitzKitStrings, STRINGS } from "./strings";
export function wrapper(slicer: (strings: BlitzKitStrings) => string) {
const defaultString = slicer(STRINGS[locales.default] as BlitzKitStrings);
const object = { [locales.default]: defaultString };
for (const { locale } of locales.supported) {
const string = slicer(STRINGS[locale] as BlitzKitStrings);
if (string !== defaultString) object[locale] = string;
}
return object;
}
Loaded 100 of 829 files, more files were not shown because too many files have changed in this diff. Show more