Compare commits

..
231 Commits
Author SHA1 Message Date
TrèsAbhi 04805675dd Merge pull request #274 from blitzkit/main-i18n
New Crowdin updates
2026-10-02 15:38:11 -07:00
tresabhi 4028df0886 dash 2026-10-02 17:37:42 -05:00
TrèsAbhi 126e91411a New translations en.json (Russian)
[ci skip]
2026-10-02 15:35:49 -07:00
TrèsAbhi 24ffd70394 New translations en.json (Portuguese)
[ci skip]
2026-10-02 15:35:48 -07:00
TrèsAbhi 8c8b36f0d5 New translations en.json (Polish)
[ci skip]
2026-10-02 15:35:46 -07:00
TrèsAbhi 38c595ebce New translations en.json (Japanese)
[ci skip]
2026-10-02 15:35:45 -07:00
TrèsAbhi 66a6aece7c New translations en.json (Spanish)
[ci skip]
2026-10-02 15:35:43 -07:00
TrèsAbhi 35ce79a421 New translations en.json (French)
[ci skip]
2026-10-02 15:35:41 -07:00
TrèsAbhi 8e8da15254 New translations en.json (Chinese Simplified)
[ci skip]
2026-10-02 15:35:40 -07:00
TrèsAbhi eea358c4e1 New translations en.json (Ukrainian)
[ci skip]
2026-10-02 15:35:38 -07:00
tresabhi ecfd50f05b logs 2026-10-02 17:35:12 -05:00
TrèsAbhi 7458cf51dc Merge pull request #263 from Hitjack007/peekabo-dpm 2026-10-02 15:24:40 -07:00
TrèsAbhi 3b0af110d2 Merge branch 'main' into peekabo-dpm 2026-10-02 15:22:06 -07:00
TrèsAbhi 91da72e8b4 New Crowdin updates (#268) 2026-10-02 17:08:10 -05:00
TrèsAbhi 6319d6fbd0 Merge pull request #269 from Hitjack007/equaliser-beta-flag 2026-10-02 15:05:56 -07:00
tresabhi a5321ebde3 game mode banner with string ids 2026-10-02 17:05:25 -05:00
tresabhi 74ca01f978 remove string 2026-10-02 17:01:43 -05:00
tresabhi b7ac038ace Merge branch 'main' of https://github.com/blitzkit/blitzkit 2026-10-02 16:56:05 -05:00
tresabhi 1a2caf120b remove some dead code 2026-10-02 16:55:53 -05:00
TrèsAbhi 377bbabb02 Merge pull request #270 from Hitjack007/contact-shadows-fix 2026-10-02 14:55:05 -07:00
TrèsAbhi a4c6f0a1e2 Merge pull request #272 from Hitjack007/moving-camo-duplicate
Clarify shooting camo labels
2026-10-02 14:37:28 -07:00
TrèsAbhi 802f04bbe6 Merge pull request #273 from Hitjack007/consumable-armor-view 2026-10-02 14:34:28 -07:00
tresabhi f7db1280cf use useRef instead of useState 2026-10-02 16:34:12 -05:00
TrèsAbhi 0477887d68 Merge branch 'main' into consumable-armor-view 2026-10-02 14:20:38 -07:00
Mark Greene 0941c23a3c Fix dynamic armor overlay vanishing on re-render
PrimaryArmorSceneComponent rebuilt its ShaderMaterial every render with null uniforms that were only set once on mount. Create it once with useState instead.
2026-10-02 19:24:43 +13:00
Mark Greene d44c184025 Clarify shooting camo labels 2026-10-02 18:49:23 +13:00
Mark Greene 73975a5d83 Fix contact shadows growing across modes
Armor left gl.autoClear off after its frame, so ContactShadows accumulated onto an uncleared target. Restore it at the end of the frame.
2026-10-02 18:34:50 +13:00
Mark Greene 7f9ee1f907 Remove equalizer beta badge 2026-10-02 18:27:10 +13:00
tresabhi 9537349664 add dependabot 2026-09-30 15:49:19 -05:00
tresabhi 8793c9a9c9 simplify readme 2026-09-30 15:44:51 -05:00
tresabhi 4d57b43912 logs 2026-09-30 15:41:08 -05:00
tresabhi ee146b8ed9 fix wgpkg dirs 2026-09-30 13:51:13 -05:00
TrèsAbhi 763ecf22b0 New Crowdin updates (#266) 2026-09-24 16:25:00 -05:00
Mark Greene 281e4abec7 Add peekaboo DPM stat to Tankopedia firepower
Adds a **Peekaboo DPM** entry nested under DPM for single-shot guns, estimating damage per minute when only firing after resetting camouflage (or waiting out the reload, whichever is slower), assuming at least 10 seconds between shots.

- Shots per minute are capped at `60/10` for reloads faster than 10s, and fall back to `60/reload` otherwise
- The result is multiplied by the currently selected shell's damage and rounded
- An (i) icon next to the label opens a popover explaining the calculation
- The row is hidden for auto-loader/auto-reloader guns
- No percentile ranking bar is shown, since no dataset exists yet for this new stat
2026-09-25 09:22:08 +12:00
TrèsAbhi 2beb084b67 Merge pull request #267 from Hitjack007/fix/session-missing-average-crash 2026-09-24 14:07:50 -07:00
tresabhi f7ca622906 correct assertions 2026-09-24 16:07:12 -05:00
tresabhi 424d9944ad smarter trigger 2026-09-23 17:12:53 -05:00
tresabhi 8af41b91b5 logs 2026-09-23 16:32:19 -05:00
tresabhi 347efa75fe add compatibility for string game mode ids 2026-09-23 16:31:35 -05:00
Mark Greene 88643d366d Fix: skip session delta entries with no tank definition or average
The /session/ page crashed when a tracked player's tank stats included
a tank with no matching tank definition or no average stats (WN8
baseline). The delta memo dereferenced `tank.id` on a possibly missing
definition and forced `average?.mu!` through as non-null, so a missing
average threw inside `compositeStats`/`calculateWN8` uncaught, breaking
the page on every load while the bad state persisted in localStorage.

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

No files matched your search

-1
View File
@@ -1 +0,0 @@
indentSize: 2
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "bun"
directory: "/"
schedule:
interval: "daily"
+45
View File
@@ -0,0 +1,45 @@
name: Discover IDs
on:
push:
branches: production
schedule:
- cron: 0 */6 * * *
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
discover:
name: Discover IDs
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: production
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Setup Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Discover IDs
run: bun run discover-ids
env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
GH_TOKEN: ${{ secrets.IDS_TOKEN }}
+127 -46
View File
@@ -2,46 +2,98 @@ name: Website
on:
push:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
inputs:
branch:
description: Branch
type: choice
required: true
default: production
options:
- production
- main
environment:
description: Environment
type: choice
required: true
default: production
options:
- development
- beta
- production
- preview
- opentest
client_provider:
description: Client provider
type: choice
required: true
default: steam
options:
- steam
- zip
- wgpkg
client_source:
description: Client source
type: string
required: true
default: 444200/windows
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ inputs.environment || github.ref }}
cancel-in-progress: true
env:
# blitz
WOTB_SERVER: ${{ secrets.WOTB_SERVER }}
WOTB_VERSION: ${{ secrets.WOTB_VERSION }}
PUBLIC_WOTB_CLIENT: ../../temp/game
WOTB_USMAP: ../closed/blitz.usmap
# google
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
# steam
# client
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
STEAM_APP: 3341250
STEAM_OS: windows
STEAM_FINGERPRINT: Manifest_UFSFiles_Win64.txt
CLIENT_PROVIDER: ${{ inputs.client_provider || 'steam' }}
CLIENT_SOURCE: ${{ inputs.client_source || '444200/windows' }}
CLIENT_DIR: ../../temp/game
# debug
DOTNET_EnableDiagnostics: 1
# blitzkit
PUBLIC_ENVIRONMENT: ${{ inputs.environment || 'production' }}
PUBLIC_PROMOTE_OPENTEST: false
PUBLIC_STATIC_ASSET_BASE: https://assets.blitzkit.app
PUBLIC_GUIDE_WEBSITE: https://guidesblitz.com
# blitz
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
# google
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
# wargaming
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
jobs:
build:
name: Build chunk ${{ matrix.chunk }}
name: Build website
runs-on: ubuntu-latest
strategy:
fail-fast: false
fail-fast: true
matrix:
chunk: [pages, api, media, models, textures]
chunk: [default, api]
steps:
- name: Free disk space
@@ -50,46 +102,75 @@ 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
# Deploy if:
# 1. Running cron from main using content from production to target production
# 2. Running manually and targeting non-production
if: >-
${{
github.ref == 'refs/heads/production' ||
(inputs.environment != '' && inputs.environment != 'production')
}}
env:
TOKEN: ${{ secrets.GH_NODE_DEPLOY_TOKEN }}
CHUNK: ${{ matrix.chunk }}
run: |
ENV_FILE="environments/${PUBLIC_ENVIRONMENT}.json"
TARGET_REPO=$(jq -r ".nodes.\"$CHUNK\"" "$ENV_FILE")
if [ "$TARGET_REPO" == "null" ] || [ -z "$TARGET_REPO" ]; then
exit 1
fi
curl \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
"https://api.github.com/repos/$TARGET_REPO/dispatches" \
-d "{
\"event_type\": \"deploy-pages\",
\"client_payload\": {
\"source_repo\": \"${{ github.repository }}\",
\"run_id\": \"${{ github.run_id }}\",
\"artifact_id\": \"${{ steps.upload.outputs.artifact-id }}\",
\"chunk\": \"$CHUNK\"
}
}"
+2 -4
View File
@@ -1,7 +1,7 @@
node_modules
_node_modules
package-lock.json
*.env
.env
/test-results/
/playwright-report/
/playwright/.cache/
@@ -22,12 +22,11 @@ depots
public/sitemap*
/firebase.json
target
packages/website-ue/src/protos/*.ts
# packages/core/src/protos/*.ts
/.vercel
.astro
.vercel
/src
/packages/bot/src/prisma
dev-dist
# Logs
logs
@@ -53,4 +52,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
*.lscache
-3
View File
@@ -1,6 +1,3 @@
[submodule "packages/closed"]
path = packages/closed
url = https://github.com/blitzkit/closed.git
[submodule "packages/cue4parse"]
path = packages/cue4parse
url = https://github.com/tresabhi/CUE4Parse.git
-3
View File
@@ -1,3 +0,0 @@
{
"tabWidth": 2
}
+4
View File
@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}
+258
View File
@@ -0,0 +1,258 @@
{
"cSpell.words": [
"AABBOX",
"Abhi",
"absarc",
"accountinfo",
"adsense",
"aeson",
"alphatest",
"analyticsdata",
"analyticsreporting",
"APAC",
"apcr",
"appinfo",
"applypatches",
"artefacts",
"astrojs",
"astrolib",
"atgm",
"autocrop",
"Autoloader",
"Autoreloader",
"AYUV",
"battleresults",
"bbox",
"beforeinstallprompt",
"bepaint",
"betakrieg",
"BINORMAL",
"Binormals",
"bkni",
"BKRL",
"BKRTS",
"BKRTSMANIFEST",
"bkwd",
"blitzkit",
"blitzrinth",
"blitzstars",
"bufbuild",
"cacheable",
"Callouts",
"Camo",
"Carro",
"CCPAOPTOUT",
"CDON",
"clanid",
"classname",
"cleandist",
"closedkrieg",
"cockroackdb",
"Coef",
"Combattimento",
"cooldown",
"corelations",
"corsproxy",
"CUBEMAP",
"CUBETEXCOORD",
"customperiod",
"datasource",
"Dava",
"dbid",
"Ddpf",
"Ddsd",
"deadrail",
"debugflags",
"deburr",
"decalmask",
"decompressor",
"décor",
"dedup",
"defence",
"degressive",
"depotdownloader",
"devkrieg",
"diacriticless",
"DIDS",
"donotremove",
"drei",
"Droodles",
"dvpd",
"dvpl",
"DVPLCRC",
"dvpm",
"dxgi",
"easimon",
"echarts",
"Everytime",
"FASTNAME",
"favicons",
"flexdirection",
"fontsource",
"FOURCC",
"frameloop",
"fsformat",
"Fullscreen",
"fullstats",
"fuzzysort",
"gamemode",
"gameplay",
"glsl",
"GLTF",
"GSOR",
"gtag",
"haha",
"Herdbreaker",
"Hetz",
"highp",
"iife",
"imgur",
"immer",
"Inclusivity",
"JOINTINDEX",
"JOINTWEIGHT",
"kranvagn",
"krieg",
"Leaderboard",
"leaderboards",
"lerp",
"LINEARSIZE",
"linktarget",
"lmao",
"loadouts",
"loddist",
"lods",
"lossrate",
"Maddoxkkm",
"malum",
"mantlets",
"mediump",
"metallicness",
"metalness",
"minitelrose",
"Minotauro",
"MIPMAPCOUNT",
"mišek",
"mmnow",
"modeltransform",
"MURICA",
"nanostores",
"NEGATIVEX",
"NEGATIVEY",
"NEGATIVEZ",
"networkidle",
"NITROPAY",
"nivo",
"nmatname",
"noout",
"normalmap",
"oneof",
"oopsies",
"Oopsy",
"opentest",
"OPTML",
"otemp",
"outdir",
"outfile",
"ownedtanks",
"padder",
"partytown",
"patchnotes",
"PIXELFORMAT",
"playerachievements",
"playerinfo",
"playform",
"pngjs",
"POSITIVEX",
"POSITIVEY",
"POSITIVEZ",
"precompute",
"Promisable",
"proto",
"protobuf",
"protoc",
"protos",
"proxyclient",
"prunestrings",
"Pyogenics",
"ratingleaderboard",
"raycaster",
"Redirector",
"Relogin",
"researchable",
"Resvg",
"rizz",
"roboto",
"rtsc",
"RTSM",
"SCPG",
"scroller",
"searchclans",
"searchplayers",
"searchtanks",
"SHAREDEXP",
"SKLLD",
"sluggify",
"smoothstep",
"Smri",
"Specularity",
"SSAO",
"startcase",
"statkit",
"statsfull",
"steamapps",
"Stringifying",
"struct",
"structs",
"strvk",
"subnation",
"Synx",
"tankaverages",
"Tankerch",
"tankmen",
"tankopedia",
"tankopedias",
"tankstats",
"tauri",
"texconv",
"TEXCOORD",
"tresabhi",
"tungstentip",
"Turretless",
"TYPELESS",
"ubjson",
"Unicum",
"UNLOCALIZED",
"UNORM",
"unpickled",
"Unregistering",
"unrotate",
"unwrapper",
"userns",
"vercel",
"vickers",
"vitepress",
"Vovko",
"wargaming",
"webp",
"wgpkg",
"whitespaces",
"WHSS",
"winrate",
"wireframe",
"Woah",
"WOTB",
"wotblitz",
"wotbreplay",
"WOTINSPECTOR",
"xffn",
"xkdkw",
"XMLDVPL",
"YAMLDVPL",
"yolo",
"youtuber",
"youtubers",
"zoomable"
],
"typescript.tsdk": "node_modules\\typescript\\lib"
}
+3 -101
View File
@@ -1,105 +1,7 @@
# 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).
### Prerequisites
Before you touch any code, you'll need the following.
#### Linux
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.
#### Windows (Optional)
...
#### 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
...
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.
+10
View File
@@ -0,0 +1,10 @@
{
"folders": [
{
"path": ".",
},
{
"path": "C:/Program Files/WindowsApps/7458BE2C.WorldofTanksBlitz_11.1.256.0_x64__x4tje2y229k00/Data",
},
],
}
+536 -1315
View File
File diff suppressed because it is too large. Load diff
+3 -24
View File
@@ -1,27 +1,6 @@
{
"words": [
"gltf",
"amogos",
"Tankopedia",
"uasset",
"oodle",
"blitzkit",
"webp",
"hocs",
"cooldown",
"Lods",
"Skia",
"RMAO",
"Verts",
"drei",
"frameloop",
"pointermove",
"pointerup",
"pointerdown"
],
"userWords": [
"WOTB",
"USMAP",
"favicons"
"gamemodes",
"Tankopedia"
]
}
}
+20
View File
@@ -0,0 +1,20 @@
# BlitzKit 1.30.7
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features
- Dragging items in the Tier List now automatically scrolls the screen if you reach the top/bottom edge of the screen
## Fixes
- [Mark Greene] Clicking on the armor multiple times with HE no longer crashes BlitzKit
- [Mark Greene] Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration
- The lighting animation no longer stops prematurely when BlitzKit is unfocused
- The spot light angle has been increased to more evenly light up larger tanks
- BlitzKit once again uses soft shadows, reducing the pixelated shadows
- Fixed the weird patterns that formed on the tracks of tanks under self-shadows
- Increased the floor size and light decay to create the illusion that the floor is infinite
- [Mark Greene] Safari no longer crashes when unloading textures for unloaded tanks
- The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0
- [Mark Greene] Armor plate names in Static Armor are now properly capitalized in English and translated to other languages
+9
View File
@@ -0,0 +1,9 @@
# BlitzKit 1.30.8
Support for tanks with Chinese names.
## Fixes
- The URL generator for BlitzKit now handles names that have only Chinese characters
- The Mixed Virtual File System now handles non-existent directories correctly
- Updated equipments to match the game again
+40
View File
@@ -0,0 +1,40 @@
# BlitzKit 1.31.0
This update massively upgrades BlitzKit's graphical fidelity (while making it faster) and updates the way BlitzKit deploys updates, speeding up future versions.
## Features
- Improved the performance of the Tank Sandbox in Tankopedia
- Reduced the number of lights by 25%
- Reduced the resolution of shadows by 50%
- Improved the graphical fidelity of tanks in the Tankopedia
- Added soft contact shadows below tanks
- Added sky and ground reflected lighting to the top and bottom of tanks
- Removed the fog that used to darken tanks when zoomed far away
- Made BlitzKitLand™ fade out circularly to hide the sharp edges
- Pointed the lights 1.5m above the ground
- This genuinely makes BlitzKit almost photo-realistic in my opinion
- Non-PBR tanks are now lit up brighter to accommodate for their dark textures
## Fixes
- Fixed a bug where the ambient occlusion map (a measure of how dark a part of a tank gets due to low light bounces) was applied to the color of the tank directly, making many tanks appear dark even in direct light
- Tanks with chinese characters now generate urls through transliteration
## Known Issues
- The flags found behind the selected tank in the tech tree section of the Tankopedia is offset incorrectly
## Technical Features
- BlitzKit can now calculate and store performance statistics and account details for up 10,000,000,000 players
- This will help make the BlitzKit Performance tool more accurate, and play well with an upcoming, unannounced major feature
## Technical Changes
- The `production` branch now deploys updates to `blitzkit.app` instead of the `main` branch
- `robots.txt` now disables indexing of BlitzKit variants based on the environment
- The website is now compiled in "chunks" of which the `api` chunk now takes on the task of mangling what was previously stored in `assets3`
- `assets3` will be removed in `1.31.1`, please migrate to the new `api` if you use any BlitzKit assets
- Removed the `bot` and `app` packages
- Removed Prisma
+3
View File
@@ -0,0 +1,3 @@
# BlitzKit 1.31.1
This update adds capabilities to release BlitzKit OpenTest and Preview.
+3
View File
@@ -0,0 +1,3 @@
# BlitzKit 1.31.2
This update fixes an issue that occurred with Wargaming chose to declare the class of a tank in the game code with a tag that isn't the first in the list.
+3
View File
@@ -0,0 +1,3 @@
# BlitzKit 1.31.3
This update updates BlitzKit to work with the game's new ID system for game modes.
+25
View File
@@ -0,0 +1,25 @@
# BlitzKit 1.31.4
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features
- [Mark Greene] Added Peekaboo DPM
- Your Peekaboo DPM is lower than your normal DPM if your reload is faster than 10s as you'll have to wait for the remaining 10s for you to get unspotted before you can poke and shoot
## Fixes
- [Mark Greene] BlitzKit Session now properly handles tanks removed from the game
- [Mark Greene] Removed the Beta badge from the Equalizer button
- Updated the background image for the Equalizer button to the actual banner of the new game mode
- [Mark Greene] The armor no longer disappears when you turn on Dynamic Armor System
- [Mark Greene] Camouflage values in Tankopedia are now more descriptive when shooting
- [Mark Greene] The contact shadows on the ground no longer grow & darken when switching between tabs
## Technical Fixes
- BlitzKit now handles the new ID system for game-modes in WoTB
- The directory which preview versions of the game are downloaded to are now created properly
- This was the last bug standing in the way of BlitzKit Previews
- Welcome back, BlitzKit Previews!
- Removed some dead code from the BlitzKit model mangler
-11
View File
@@ -2,17 +2,6 @@
🎉 Services for everything and anything World of Tanks Blitz
## Bot
BlitzKit is available as a Discord bot and can be [installed very easily](/guide/invite).
- Track statistics
- Find inactive players in a clan
- Lookup owned tanks
- Create embeds for streams
- Track a player/tanks statistical evolution
- ...and a variety of other similar things
## Website
Provides a bunch of very helpful tools; https://blitzkit.app/
-3
View File
@@ -1,3 +0,0 @@
# Inviting the Bot
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
-13
View File
@@ -1,13 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.3.0",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-beta.blitzkit.app",
"api": "blitzkit/deploy-node-api.beta.blitzkit.app"
},
"aliases": {
"api": "https://api.beta.blitzkit.app",
"static": "https://static.beta.blitzkit.app"
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": "api"
},
"nodes": {},
"aliases": {
"api": "/api",
"static": "https://static.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-opentest.blitzkit.app",
"api": "blitzkit/deploy-node-api.opentest.blitzkit.app"
},
"aliases": {
"api": "https://api.opentest.blitzkit.app",
"static": "https://static.opentest.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-preview.blitzkit.app",
"api": "blitzkit/deploy-node-api.preview.blitzkit.app"
},
"aliases": {
"api": "https://api.preview.blitzkit.app",
"static": "https://static.preview.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": true,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-blitzkit.app",
"api": "blitzkit/deploy-node-api.blitzkit.app"
},
"aliases": {
"api": "https://api.blitzkit.app",
"static": "https://static.blitzkit.app"
}
}
+10 -7
View File
@@ -1,21 +1,24 @@
{
"name": "blitzkit",
"version": "2.0.0-beta.3",
"version": "1.31.4",
"license": "BlitzKit License",
"private": true,
"scripts": {
"setup": "chmod a+x scripts/*.sh && ./scripts/setup.sh",
"dev": "./scripts/dev.sh",
"build-game": "./scripts/build_game.sh",
"build-website": "./scripts/build_website.sh",
"discover-servers": "./scripts/discover_servers.sh"
"dev": "cd packages/website && bun run dev",
"build": "cd packages/website && bun run build",
"discover-ids": "cd packages/scripts && bun run discover-ids",
"build-protos": "cd packages/scripts && bun run build-protos",
"record-performance": "cd packages/scripts && bun run record-performance",
"download-client": "cd packages/scripts && bun run download-client"
},
"type": "module",
"workspaces": [
"packages/*"
],
"dependencies": {
"prettier": "^3.8.3",
"@gltf-transform/extensions": "^4.4.2",
"lz4js": "^0.2.0",
"sharp": "^0.34.5",
"typescript": "^5.9.3"
}
}
+3 -4
View File
@@ -8,12 +8,11 @@
"@bufbuild/protobuf": "^2.9.0",
"@octokit/plugin-throttling": "^11.0.1",
"@octokit/rest": "^22.0.0",
"@prisma/client": "^6.16.2",
"@types/adm-zip": "^0.5.7",
"@types/bun": "^1.3.1",
"@types/bun": "^1.3.14",
"@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1",
"@types/three": "^0.184.1",
"@types/three": "^0.180.0",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^17.2.2",
@@ -21,7 +20,7 @@
"lodash-es": "^4.17.21",
"pickleparser": "^0.2.1",
"protobufjs": "^7.5.4",
"three": "^0.184.0",
"three": "^0.180.0",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1"
}
+89
View File
@@ -0,0 +1,89 @@
import { assertSecret, EventManager } from "../blitzkit";
import { patientFetch } from "../blitzkit/patientFetch";
import type { Region } from "./regions";
const RETRY_ERRORS = ["REQUEST_LIMIT_EXCEEDED", "SOURCE_NOT_AVAILABLE"];
export const retryAbleBlitzFetchEvent = new EventManager();
export const blitzFetchQueueAvailableEvent = new EventManager();
type BlitzResponse<Data extends object> =
| {
status: "error";
error: { field: string; message: string; code: 402; value: null };
}
| {
status: "ok";
data: Data;
};
const MAX_CALLS_PER_SECOND = 10;
const queue: { url: string; resolve: (data: object) => void }[] = [];
let inProgress = 0;
function decrement() {
inProgress--;
blitzFetchQueueAvailableEvent.emit(undefined);
manageQueue();
}
async function manageQueue() {
if (queue.length > 0 && inProgress < MAX_CALLS_PER_SECOND) {
inProgress++;
const timeout = setTimeout(decrement, 1000);
const request = queue.shift()!;
const data = (await patientFetch(request.url)
.then((response) => response.json())
.catch((error) => ({ status: "error", error }))) as BlitzResponse<
typeof request.resolve
>;
if (data.status === "ok") {
request.resolve(data.data);
} else {
if (RETRY_ERRORS.includes(data.error.message)) {
retryAbleBlitzFetchEvent.emit(undefined);
clearTimeout(timeout);
setTimeout(() => {
queue.push(request);
decrement();
}, 1000 / MAX_CALLS_PER_SECOND);
} else {
clearTimeout(timeout);
decrement();
throw new Error(`Wargaming response error status: "${data.status}"`, {
cause: `Message: "${data.error.message}"\nURL: "${request.url}"`,
});
}
}
}
}
export type FetchBlitzParams = Record<string, string | number | undefined>;
export function fetchBlitz<Data extends object>(
region: Region,
path: string,
params: FetchBlitzParams = {}
) {
return new Promise<Data>((resolve) => {
queue.push({
url: `https://api.wotblitz.${region}/wotb/${path}/?${Object.entries({
application_id: assertSecret(
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
),
...params,
})
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => `${key}=${encodeURIComponent(value!)}`)
.join("&")}`,
resolve(data) {
resolve(data as Data);
},
});
manageQueue();
});
}
+11
View File
@@ -0,0 +1,11 @@
import { assertSecret } from "../blitzkit";
import type { BlitzGlossary } from "../types";
export async function fetchGlossary(locale: string) {
const response = await fetch(
`${assertSecret(
import.meta.env.WOTB_GLOSSARY_DOMAIN,
)}/${locale}${assertSecret(import.meta.env.WOTB_GLOSSARY_PATH)}`,
);
return (await response.json()) as BlitzGlossary;
}
@@ -0,0 +1,35 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import type { Region } from "./regions";
interface IndividualAccountAchievements {
achievements: {
[achievement: string]: number;
};
max_series: {
[achievement: string]: number;
};
}
export interface AccountAchievements {
[id: number]: IndividualAccountAchievements;
}
export async function getAccountAchievements<
Ids extends number | number[],
ReturnType = Ids extends number
? IndividualAccountAchievements
: IndividualAccountAchievements[],
>(region: Region, ids: Ids) {
const object = await fetchBlitz<AccountAchievements>(
region,
"account/achievements",
{ account_id: normalizeIds(ids) }
);
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
+96
View File
@@ -0,0 +1,96 @@
import { BlitzStats } from "../statistics";
import { FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import { Region } from "./regions";
export interface SupplementaryStats {
WN8?: number;
tier?: number;
}
export interface ClanStats {
spotted: number;
max_frags_tank_id: number;
hits: number;
frags: number;
max_xp: number;
max_xp_tank_id: number;
wins: number;
losses: number;
capture_points: number;
battles: number;
damage_dealt: number;
damage_received: number;
max_frags: number;
shots: number;
frags8p: number;
xp: number;
win_and_survived: number;
survived_battles: number;
dropped_capture_points: number;
}
export interface RatingStats {
spotted: number;
calibration_battles_left: number;
hits: number;
frags: number;
recalibration_start_time: number;
mm_rating: number;
wins: number;
losses: number;
is_recalibration: boolean;
capture_points: number;
battles: number;
damage_dealt: number;
damage_received: number;
shots: number;
frags8p: number;
xp: number;
win_and_survived: number;
survived_battles: number;
dropped_capture_points: number;
}
export interface IndividualAccountInfo {
account_id: number;
created_at: number;
updated_at: number;
private: null;
last_battle_time: number;
nickname: string;
statistics: {
all: BlitzStats;
clan: ClanStats;
rating?: RatingStats;
frags: null | number;
};
}
export interface AccountInfo {
[accountId: number]: IndividualAccountInfo;
}
export async function getAccountInfo<
Ids extends number | number[],
ReturnType = Ids extends number
? IndividualAccountInfo
: IndividualAccountInfo[],
>(
region: Region,
ids: Ids,
extra: string[] = [],
params: FetchBlitzParams = {}
) {
const object = await fetchBlitz<AccountInfo>(region, "account/info", {
account_id: normalizeIds(ids),
extra: extra.length === 0 ? undefined : extra.join(","),
...params,
});
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
@@ -0,0 +1,46 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import type { Region } from "./regions";
type IndividualClanAccountInfo = {
account_id: number;
joined_at: number;
clan_id: number;
role: string;
account_name: string;
clan?: {
members_count: number;
name: string;
created_at: number;
tag: string;
clan_id: number;
emblem_set_id: number;
};
} | null;
interface ClanAccountInfo {
[accountId: number]: IndividualClanAccountInfo;
}
export async function getClanAccountInfo<
Ids extends number | number[],
ReturnType = Ids extends number
? IndividualClanAccountInfo
: IndividualClanAccountInfo[],
>(region: Region, ids: Ids, extra: string[] = []) {
const object = await fetchBlitz<ClanAccountInfo>(
region,
"clans/accountinfo",
{
account_id: normalizeIds(ids),
extra: extra.length === 0 ? undefined : extra.join(","),
}
);
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
+51
View File
@@ -0,0 +1,51 @@
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import type { Region } from "./regions";
interface IndividualClanInfo {
recruiting_options: {
vehicles_level: number;
wins_ratio: number;
average_battles_per_day: number;
battles: number;
average_damage: number;
};
members_count: number;
name: string;
creator_name: string;
clan_id: number;
created_at: number;
updated_at: number;
leader_name: string;
members_ids: number[];
recruiting_policy: string;
tag: string;
is_clan_disbanded: boolean;
old_name: string;
emblem_set_id: number;
creator_id: number;
motto: string;
renamed_at: number;
old_tag: null;
leader_id: number;
description: string;
}
interface ClanInfo {
[clanId: number]: IndividualClanInfo;
}
export async function getClanInfo<
Ids extends number | number[],
ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[],
>(region: Region, ids: Ids) {
const object = await fetchBlitz<ClanInfo>(region, "clans/info", {
clan_id: normalizeIds(ids),
});
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
}
}
@@ -1,5 +0,0 @@
import { LEAGUES } from "./rating";
export function getLeagueFromScore(score: number) {
return LEAGUES.find(({ minScore }) => score >= minScore)!;
}
-42
View File
@@ -1,42 +0,0 @@
import { context } from "../blitzkit/context";
import { patientFetch } from "../blitzkit/patientFetch";
import type { RatingLeague, RatingReward } from "./rating";
import type { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export type RatingInfo =
| {
detail: undefined;
title: string;
icon: string | null;
start_at: string;
finish_at: string;
current_season: number;
updated_at: string;
count: number;
rewards: RatingReward[];
leagues: RatingLeague[];
}
| { detail: { error: string } };
const cache: Partial<Record<Region, RatingInfo>> = {};
export async function getRatingInfo(region: Region) {
if (cache[region]) return cache[region]!;
const response = await patientFetch(
context === "website"
? `/api/${region}/rating/current/info`
: `https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
undefined
);
const data = (await response.json()) as RatingInfo;
cache[region] = data;
return data;
}
@@ -1,29 +0,0 @@
import { context } from "../blitzkit/context";
import { patientFetch } from "../blitzkit/patientFetch";
import type { RatingPlayer } from "../statistics";
import type { LeagueTop } from "./rating";
import type { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export async function getRatingLeague(
region: Region,
league: number,
usePatientFetcher = false
) {
const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: "no-store" })
: fetch(url, { cache: "no-store" });
const response = await fetcher(
context === "website"
? `/api/${region}/rating/current/league/${league}`
: `https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`
);
const json = await response.json();
return context === "website"
? (json as RatingPlayer[])
: (json as LeagueTop).result;
}
@@ -1,27 +0,0 @@
import { patientFetch } from "../blitzkit/patientFetch";
import { withCORSProxy } from "../blitzkit/withCORSProxy";
import type { RatingNeighbors } from "./rating";
import type { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export async function getRatingNeighbors(
region: Region,
id: number,
count: number,
usePatientFetcher = false
) {
const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: "no-store" })
: fetch(url, { cache: "no-store" });
const response = await fetcher(
withCORSProxy(
`https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`
)
);
const json = (await response.json()) as RatingNeighbors;
return json;
}
+16
View File
@@ -0,0 +1,16 @@
import type { TanksStats } from "../types";
import { type FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
import type { Region } from "./regions";
export async function getTankStats(
region: Region,
id: number,
params?: FetchBlitzParams
) {
const tankStats = await fetchBlitz<TanksStats>(region, "tanks/stats", {
account_id: id,
...params,
});
return tankStats[id];
}
+10 -6
View File
@@ -3,13 +3,16 @@ export * from "./clans";
export * from "./classes";
export * from "./constants";
export * from "./equipmentPrice";
export * from "./fetchBlitz";
export * from "./fetchGlossary";
export * from "./generateStats";
export * from "./getLeagueFromScore";
export * from "./getRatingInfo";
export * from "./getRatingLeague";
export * from "./getRatingNeighbors";
export * from "./getAccountAchievements";
export * from "./getAccountInfo";
export * from "./getClanAccountInfo";
export * from "./getClanInfo";
export * from "./getTankStats";
export * from "./idToRegion";
export * from "./isOnGoingRatingSeason";
export * from "./isValidBlitzId";
export * from "./nations";
export * from "./normalizeExtra";
export * from "./normalizeIds";
@@ -19,5 +22,6 @@ export * from "./readDVPL";
export * from "./regions";
export * from "./regionSubdomainToRegion";
export * from "./regionToRegionSubdomain";
export * from "./searchCurrentRatingPlayers";
export * from "./searchClansAcrossRegions";
export * from "./searchPlayersAcrossRegions";
export * from "./toUniqueId";
@@ -1,6 +0,0 @@
import { getRatingInfo } from "./getRatingInfo";
export async function isOnGoingRatingSeason() {
const ratingInfo = await getRatingInfo("com");
return ratingInfo.detail === undefined;
}
+32
View File
@@ -0,0 +1,32 @@
import { assertSecret } from "../blitzkit";
import { idToRegion } from "./idToRegion";
import { Region } from "./regions";
export async function isValidBlitzId(id: number, token?: string) {
let region: Region;
try {
region = idToRegion(id);
} catch (error) {
return false;
}
const json = await fetch(
`https://api.wotblitz.${region}/wotb/account/info/?account_id=${id}&application_id=${assertSecret(
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
)}${
token === undefined ? "" : `&access_token=${token}`
}&fields=account_id%2C-account_id`
).then(
(response) =>
response.json() as Promise<
| {
status: "ok";
data: Record<number, {} | null>;
}
| { status: "error" }
>
);
return json.status === "ok" && json.data[id] !== null;
}
@@ -0,0 +1,51 @@
import type { ClanList } from "./clans";
import { fetchBlitz } from "./fetchBlitz";
import type { Region } from "./regions";
export async function searchClansAcrossRegions(search: string, limit = 25) {
const trimmed = search.trim();
if (trimmed.length < 3 && trimmed.length > 100) return [];
const normalizedLimit = Math.floor(limit / 3);
// TODO: remove splice once wargaming fixes the functionality of the limit felid
return (
await Promise.all([
fetchBlitz<ClanList>("com", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "com" as Region,
}))
.splice(0, normalizedLimit)
),
fetchBlitz<ClanList>("eu", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "eu" as Region,
}))
.splice(0, normalizedLimit)
),
fetchBlitz<ClanList>("asia", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: "asia" as Region,
}))
.splice(0, normalizedLimit)
),
])
)
.filter(Boolean)
.flat();
}
@@ -1,40 +0,0 @@
import { context } from "../blitzkit/context";
import { type Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
type CurrentRatingPlayerSearch = Record<
number,
{
spa_id: number;
nickname: string;
clan_tag: string;
} & (
| {
skip: true;
}
| {
skip: false;
mmr: number;
season_number: number;
calibrationBattlesLeft: number;
number: number;
updated_at: string;
neighbors: [];
}
)
>;
export async function searchCurrentRatingPlayers(region: Region, name: string) {
const encodedSearch = encodeURIComponent(name);
const response = await fetch(
context === "website"
? `/api/${region}/rating/current/search/${encodedSearch}`
: `https://${regionToRegionSubdomain(
region
)}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`,
{ cache: "no-store" }
);
const accountList = (await response.json()) as CurrentRatingPlayerSearch;
return accountList;
}
@@ -0,0 +1,2 @@
export const usernamePattern = /^[a-zA-Z0-9_]{3,24}$/;
export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/;
@@ -0,0 +1,64 @@
import { fetchBlitz } from "../fetchBlitz";
import type { Region } from "../regions";
import { usernamePattern } from "./constants";
export interface AccountListItem {
nickname: string;
account_id: number;
}
export type AccountList = AccountListItem[];
export type AccountListWithServer = (AccountListItem & {
region: Region;
})[];
export async function searchPlayersAcrossRegions(search: string, limit = 25) {
const trimmed = search.trim();
if (trimmed.length < 3 && trimmed.length > 100) return [];
const normalizedLimit = Math.floor(limit / 3);
if (usernamePattern.test(trimmed)) {
return (
await Promise.all([
fetchBlitz<AccountList>("com", "account/list", {
search,
limit: normalizedLimit,
}).then(
(value) =>
value &&
value.map((account) => ({
...account,
region: "com" as Region,
}))
),
fetchBlitz<AccountList>("eu", "account/list", {
search,
limit: normalizedLimit,
}).then(
(value) =>
value &&
value.map((account) => ({
...account,
region: "eu" as Region,
}))
),
fetchBlitz<AccountList>("asia", "account/list", {
search,
limit: normalizedLimit,
}).then(
(value) =>
value &&
value.map((account) => ({
...account,
region: "asia" as Region,
}))
),
])
)
.filter(Boolean)
.flat() as AccountListWithServer;
} else {
return [] as AccountListWithServer;
}
}
export * from "./constants";
+13
View File
@@ -0,0 +1,13 @@
import { environmentConfig } from "@blitzkit/core";
export function alias(alias: string, path: string) {
const environment = environmentConfig();
if (!(alias in environment.aliases)) {
throw new Error(`Invalid alias "${alias}"`);
}
const base = environment.aliases[alias];
return `${base}${path}`;
}
+25
View File
@@ -0,0 +1,25 @@
import {
CamouflageDefinitions,
ConsumableDefinitions,
EquipmentDefinitions,
Gallery,
GameDefinitions,
MapDefinitions,
ModelDefinitions,
ProvisionDefinitions,
SkillDefinitions,
TankDefinitions,
} from "@blitzkit/core";
export abstract class BlitzKitAPI {
abstract skillDefinitions(): Promise<SkillDefinitions>;
abstract provisionDefinitions(): Promise<ProvisionDefinitions>;
abstract consumableDefinitions(): Promise<ConsumableDefinitions>;
abstract equipmentDefinitions(): Promise<EquipmentDefinitions>;
abstract mapDefinitions(): Promise<MapDefinitions>;
abstract modelDefinitions(): Promise<ModelDefinitions>;
abstract camouflageDefinitions(): Promise<CamouflageDefinitions>;
abstract tankDefinitions(): Promise<TankDefinitions>;
abstract gameDefinitions(): Promise<GameDefinitions>;
abstract galleryDefinitions(): Promise<Gallery>;
}
+69
View File
@@ -0,0 +1,69 @@
import { alias } from "@blitzkit/core";
import { fetchPB } from "../../protobuf";
import {
CamouflageDefinitions,
ConsumableDefinitions,
EquipmentDefinitions,
Gallery,
GameDefinitions,
MapDefinitions,
ModelDefinitions,
ProvisionDefinitions,
SkillDefinitions,
TankDefinitions,
} from "../../protos";
import { BlitzKitAPI } from "./base";
export class ClientBlitzKitAPI extends BlitzKitAPI {
gameDefinitions() {
return fetchPB(alias("api", "/definitions/game.pb"), GameDefinitions);
}
consumableDefinitions() {
return fetchPB(
alias("api", "/definitions/consumables.pb"),
ConsumableDefinitions,
);
}
tankDefinitions() {
return fetchPB(alias("api", "/definitions/tanks.pb"), TankDefinitions);
}
camouflageDefinitions() {
return fetchPB(
alias("api", "/definitions/camouflage.pb"),
CamouflageDefinitions,
);
}
provisionDefinitions() {
return fetchPB(
alias("api", "/definitions/provisions.pb"),
ProvisionDefinitions,
);
}
modelDefinitions() {
return fetchPB(alias("api", "/definitions/models.pb"), ModelDefinitions);
}
skillDefinitions() {
return fetchPB(alias("api", "/definitions/skills.pb"), SkillDefinitions);
}
equipmentDefinitions() {
return fetchPB(
alias("api", "/definitions/equipment.pb"),
EquipmentDefinitions,
);
}
galleryDefinitions() {
return fetchPB(alias("api", "/definitions/gallery.pb"), Gallery);
}
mapDefinitions() {
return fetchPB(alias("api", "/definitions/maps.pb"), MapDefinitions);
}
}
@@ -0,0 +1,304 @@
import {
BlitzCrewType,
BlitzModuleType,
BlitzStrings,
BlitzTankClass,
BlitzTankFilterDefinitionCategory,
CamouflagesXml,
CamouflagesYaml,
CombatRolesYaml,
ConsumablesCommon,
ConsumableTankCategoryFilterCategory,
CrewType,
I18nString,
MapsYaml,
ModuleType,
OptionalDevices,
OptionalDeviceSlots,
ProvisionsCommon,
ShellKind,
ShellType,
SquadBattleTypeStylesYaml,
TankClass,
TankmenAvatar,
toUniqueId,
VehicleDefinitionList,
} from "@blitzkit/core";
import { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import { parse as parseYaml } from "yaml";
import { BlitzKitAPI } from "../base";
export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
protected vfs: AbstractVFS;
protected nationSlugDiscriminators = {
china: "cn",
european: "eu",
france: "fr",
germany: "de",
japan: "jp",
other: "ot",
uk: "gb",
usa: "us",
ussr: "ru",
};
protected blitzModuleTypeToBlitzkit: Record<
keyof BlitzModuleType,
ModuleType
> = {
chassis: ModuleType.MODULE_TYPE_TRACKS,
engine: ModuleType.MODULE_TYPE_ENGINE,
gun: ModuleType.MODULE_TYPE_GUN,
turret: ModuleType.MODULE_TYPE_TURRET,
vehicle: ModuleType.MODULE_TYPE_VEHICLE,
};
protected blitzkitCrewTypeToBlitz: Record<CrewType, BlitzCrewType> = {
[CrewType.CREW_TYPE_COMMANDER]: "commander",
[CrewType.CREW_TYPE_DRIVER]: "driver",
[CrewType.CREW_TYPE_GUNNER]: "gunner",
[CrewType.CREW_TYPE_LOADER]: "loader",
[CrewType.CREW_TYPE_RADIOMAN]: "radioman",
};
protected blitzCrewTypeToBlitzkit: Record<BlitzCrewType, CrewType> = {
commander: CrewType.CREW_TYPE_COMMANDER,
driver: CrewType.CREW_TYPE_DRIVER,
gunner: CrewType.CREW_TYPE_GUNNER,
loader: CrewType.CREW_TYPE_LOADER,
radioman: CrewType.CREW_TYPE_RADIOMAN,
};
protected blitzTankClassToBlitzkit: Record<BlitzTankClass, TankClass> = {
lightTank: TankClass.TANK_CLASS_LIGHT,
"AT-SPG": TankClass.TANK_CLASS_TANK_DESTROYER,
heavyTank: TankClass.TANK_CLASS_HEAVY,
mediumTank: TankClass.TANK_CLASS_MEDIUM,
};
protected blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = {
ARMOR_PIERCING: ShellType.SHELL_TYPE_AP,
ARMOR_PIERCING_CR: ShellType.SHELL_TYPE_APCR,
HIGH_EXPLOSIVE: ShellType.SHELL_TYPE_HE,
HOLLOW_CHARGE: ShellType.SHELL_TYPE_HEAT,
};
protected blitzTankFilterDefinitionCategoryToBlitzkit: Record<
BlitzTankFilterDefinitionCategory,
ConsumableTankCategoryFilterCategory
> = {
clip: ConsumableTankCategoryFilterCategory.CONSUMABLE_TANK_CATEGORY_FILTER_CATEGORY_CLIP,
};
protected botPattern = /^.+((tutorial_bot(\d+)?)|(TU))$/;
protected stringsI18n: Record<string, Record<string, string>> = {};
protected nationsDir?: string[];
protected tankStringIdMap: Record<string, number> = {};
protected consumablesCommon: ConsumablesCommon = {};
protected provisionsCommon: ProvisionsCommon = {};
// TODO: suffix with "yaml"s and "xml"s
protected optionalDevices?: { root: OptionalDevices };
protected optionalDeviceSlots?: { root: OptionalDeviceSlots };
protected tankmenAvatar?: { root: TankmenAvatar };
protected mapsYaml?: MapsYaml;
protected camouflagesXml?: { root: CamouflagesXml };
protected camouflagesYaml?: CamouflagesYaml;
protected squadBattleTypeStyles?: SquadBattleTypeStylesYaml;
protected gameTypeSelectorStyles?: SquadBattleTypeStylesYaml;
protected combatRoles?: CombatRolesYaml;
protected tierEqualizer?: string[][];
constructor(vfs: AbstractVFS) {
super();
this.vfs = vfs;
}
async init() {
console.log("Initializing virtual file system...");
await this.vfs.init();
console.log("Fetching game data...");
this.nationsDir = await this.vfs
.dir("Data/XML/item_defs/vehicles")
.then((files) => files.filter((nation) => nation !== "common"));
this.optionalDevices = await this.vfs.xml<{ root: OptionalDevices }>(
"Data/XML/item_defs/vehicles/common/optional_devices.xml",
);
this.optionalDeviceSlots = await this.vfs.xml<{
root: OptionalDeviceSlots;
}>("Data/XML/item_defs/vehicles/common/optional_device_slots.xml");
this.tankmenAvatar = await this.vfs.xml<{ root: TankmenAvatar }>(
"Data/XML/item_defs/tankmen/avatar.xml",
);
this.mapsYaml = await this.vfs.yaml<MapsYaml>("Data/maps.yaml");
this.camouflagesXml = await this.vfs.xml<{ root: CamouflagesXml }>(
"Data/XML/item_defs/vehicles/common/camouflages.xml",
);
this.camouflagesYaml = await this.vfs.yaml<CamouflagesYaml>(
"Data/camouflages.yaml",
);
this.squadBattleTypeStyles = await this.vfs.yaml<SquadBattleTypeStylesYaml>(
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`,
);
this.gameTypeSelectorStyles =
await this.vfs.yaml<SquadBattleTypeStylesYaml>(
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`,
);
this.combatRoles = await this.vfs.yaml<CombatRolesYaml>(
`Data/XML/item_defs/vehicles/common/combat_roles.yaml`,
);
this.tierEqualizer = await this.vfs.csv(
// spell:disable-next-line
`Data/XML/item_defs/vehicles/common/tier_equializer.csv`,
{ delimiter: ";" },
);
console.log("Fetching game localizations...");
if (import.meta.env.DEV) {
locales.supported = [locales.supported[0]];
}
let fetchedLocalizations = 0;
await Promise.all(
locales.supported.map(async ({ locale }) => {
const blitzLocale = SUPPORTED_LOCALE_BLITZ_MAP[locale];
const cache = await fetch(
`https://stufficons.wgcdn.co/localizations/${blitzLocale}.yaml`,
)
.then((response) => response.text())
.then((string) => parseYaml(string) as BlitzStrings);
const preInstalled = await this.vfs.yaml<BlitzStrings>(
`Data/Strings/${blitzLocale}.yaml`,
);
this.stringsI18n[locale] = {
...cache,
...preInstalled,
};
console.log(
`Fetched localizations for ${locale} (${++fetchedLocalizations}/${locales.supported.length})`,
);
}),
);
console.log("Fetching consumable and provision data...");
for (const match of (
await this.vfs.text(
`Data/XML/item_defs/vehicles/common/consumables/list.xml`,
)
).matchAll(/<items path="(.+)\.xml"\/>/g)) {
if (match[1] === "prototypes") continue;
Object.assign(
this.consumablesCommon,
(
await this.vfs.xml<{ root: ConsumablesCommon }>(
`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`,
)
).root,
);
}
for (const match of (
await this.vfs.text(
`Data/XML/item_defs/vehicles/common/provisions/list.xml`,
)
).matchAll(/<items path="(.+)\.xml"\/>/g)) {
if (match[1] === "prototypes") continue;
Object.assign(
this.provisionsCommon,
(
await this.vfs.xml<{ root: ConsumablesCommon }>(
`Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`,
)
).root,
);
}
for (const nation of this.nationsDir!) {
const tankList = await this.vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
);
for (const tankKey in tankList.root) {
if (this.botPattern.test(tankKey)) continue;
const tank = tankList.root[tankKey];
const tankId = toUniqueId(nation, tank.id);
this.tankStringIdMap[`${nation}:${tankKey}`] = tankId;
}
}
return this;
}
protected getString(name: string) {
const collection: Record<string, string> = {
[locales.default]: this.stringsI18n[locales.default][name],
};
for (const { locale } of locales.supported) {
const localizedString = this.stringsI18n[locale][name];
if (
localizedString === undefined ||
localizedString === collection[locales.default]
) {
continue;
}
collection[locale] = localizedString;
}
return { locales: collection } satisfies I18nString;
}
}
export function Cache(disableInDev = false) {
const cache = new WeakMap<object, Map<string, Promise<unknown>>>();
return function <This, Arguments extends any[], Return>(
target: (this: This, ...args: Arguments) => Promise<Return>,
// TODO: evaluate if we need context to make the error trace easier
// context: ClassMethodDecoratorContext<
// This,
// (this: This, ...args: Arguments) => Promise<Return>
// >,
) {
return async function replacementMethod(this: This, ...args: Arguments) {
const key = args.join("-");
let thisCache: Map<string, Promise<Return>>;
if (cache.has(this as object)) {
thisCache = cache.get(this as object) as Map<string, Promise<Return>>;
} else {
thisCache = new Map();
cache.set(this as object, thisCache);
}
const disabled = disableInDev && import.meta.env.DEV;
if (thisCache.has(key) && !disabled) {
return thisCache.get(key)!;
}
const promise = (async () => {
try {
return await target.apply(this, args);
} catch (error) {
thisCache!.delete(key);
throw error;
}
})();
thisCache.set(key, promise);
return promise;
};
};
}
@@ -0,0 +1,67 @@
import { Avatar, fetchGlossary, Gallery } from "@blitzkit/core";
import locales from "@blitzkit/i18n/locales.json";
import { extname } from "path";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI9 } from "./9_gameDefinitions";
export abstract class ServerBlitzKitAPI10 extends ServerBlitzKitAPI9 {
@Cache()
async galleryDefinitions() {
const avatars: Record<string, { avatar: Avatar; url: string }> = {};
await Promise.all(
locales.supported.map(async (supported) => {
const glossary = await fetchGlossary(
supported.blitz ?? supported.locale,
);
console.log(
`Found ${Object.keys(glossary).length} things for ${supported.locale}`,
);
for (const key in glossary) {
const glossaryEntry = glossary[key];
if (
!key.startsWith("avatar") ||
key.endsWith("_part") ||
glossaryEntry.image_url === null
) {
continue;
}
if (key in avatars) {
avatars[key].avatar.name!.locales[supported.locale] =
glossaryEntry.title;
} else {
const extension = extname(glossaryEntry.image_url);
avatars[key] = {
url: glossaryEntry.image_url,
avatar: {
id: key,
name: { locales: { [supported.locale]: glossaryEntry.title } },
extension,
},
};
}
}
}),
);
const gallery: Gallery = { avatars: [] };
await Promise.all(
Object.entries(avatars).map(async ([key, { url, avatar }]) => {
if (!(locales.default in avatars[key].avatar.name!.locales)) {
console.warn(`Avatar ${key} has no ${locales.default} name`);
return;
}
gallery.avatars.push(avatar);
}),
);
return gallery;
}
}
@@ -0,0 +1,729 @@
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, string> = {};
const squadBattleTypeGameModeNativeNameMatches =
this.squadBattleTypeStyles!.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/eGameMode\.(\w+) -> "(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) {
gameModeNativeNames[match[3]] = match[1];
}
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 };
}
const blitzTankClass = tankTags.find(
(tag) => tag in this.blitzTankClassToBlitzkit,
);
if (blitzTankClass === undefined) {
throw new Error(`Unknown tank class for ${nation}:${tankKey}`);
}
const tankClass =
this.blitzTankClassToBlitzkit[blitzTankClass as BlitzTankClass];
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: tankClass,
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,324 @@
import {
Armor,
BlitzCrewType,
ChassisDefinitionsList,
Crew,
GunDefinitionsList,
ModelDefinitions,
TankParameters,
toUniqueId,
TurretDefinitionsList,
Vector3,
VehicleDefinitionArmor,
VehicleDefinitionList,
VehicleDefinitions,
} from "@blitzkit/core";
import { parse as parsePath } from "path";
import { Vector3Tuple } from "three";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI2 } from "./2_camouflageDefinitions";
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 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 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(
/eGameMode\.(\w+) -> "(battleType\/([a-zA-Z]+))"/g,
)) {
const id = match[1];
const name = this.getString(match[2]);
gameDefinitions.gameModes[id] = {
name,
};
}
Object.entries(this.combatRoles!).forEach(([, value]) => {
gameDefinitions.roles[value.id] = { provisions: [], consumables: [] };
value.default_abilities.forEach((ability) => {
if (ability in consumableNativeNames) {
gameDefinitions.roles[value.id].consumables.push(
consumableNativeNames[ability],
);
} else if (ability in provisionNativeNames) {
gameDefinitions.roles[value.id].provisions.push(
provisionNativeNames[ability],
);
} else throw new Error(`Unknown ability ${ability}`);
});
});
return gameDefinitions;
}
}
@@ -0,0 +1,3 @@
import { ServerBlitzKitAPI10 } from "@blitzkit/core/src/blitzkit/api/server/10_galleryDefinitions";
export class ServerBlitzKitAPI extends ServerBlitzKitAPI10 {}
@@ -0,0 +1,4 @@
export function assertSecret(secret?: string) {
if (secret === undefined) throw new Error("Missing secret");
return secret;
}
@@ -0,0 +1,22 @@
import { emptyAllStats } from '../../blitz/constants';
import { Samples } from '../../protos';
import type { BlitzkitStats } from '../../statistics';
export const emptyAverageDefinitionsAllStats: BlitzkitStats = {
...emptyAllStats,
battle_life_time: 0,
};
export const averageDefinitionsAllStatsKeys = Object.keys(
emptyAverageDefinitionsAllStats,
) as (keyof BlitzkitStats)[];
export const emptySamples: Samples = {
d_1: 0,
d_7: 0,
d_30: 0,
d_60: 0,
d_90: 0,
d_120: 0,
total: 0,
};
@@ -0,0 +1,17 @@
import { staticAsset } from "@blitzkit/core";
import { fetchPB } from "../../protobuf";
import { AverageDefinitions } from "../../protos";
export interface AverageDefinitionsManifest {
version: 1;
/**
* epoch in days
*/
latest: number;
}
export async function fetchAverageDefinitions() {
return await fetchPB(staticAsset("averages.pb"), AverageDefinitions);
}
export * from "./constants";
@@ -0,0 +1,12 @@
import { staticAsset } from "@blitzkit/core";
export interface DiscoveredIdsDefinitions {
time: number;
chunks: number;
count: number;
}
export async function fetchDiscoveredIdsDefinitions() {
const response = await fetch(staticAsset("ids/manifest.json"));
return response.json() as Promise<DiscoveredIdsDefinitions>;
}
+47
View File
@@ -0,0 +1,47 @@
import { assertSecret } from "@blitzkit/core";
interface EnvironmentConfig {
robots: boolean;
canonical: string;
paths: Record<string, string | null>;
nodes: Record<string, string>;
aliases: Record<string, string>;
}
let hasReadEnvironments = false;
const environments: Record<string, EnvironmentConfig> = {};
/**
* This is set up this way to avoid errors in runtimes where
* `import.meta.glob` does not exist.
*/
function readEnvironments() {
if (hasReadEnvironments) return;
hasReadEnvironments = true;
const glob = import.meta.glob("../../../../environments/*.json", {
eager: true,
import: "default",
}) as Record<string, EnvironmentConfig>;
const root = "../../../../environments";
for (const key in glob) {
const trimmed = key.slice(root.length + 1, -5);
environments[trimmed] = glob[key];
delete environments[key];
}
}
export function environmentConfig() {
readEnvironments();
const environment = assertSecret(import.meta.env.PUBLIC_ENVIRONMENT);
if (!(environment in environments)) {
throw new Error(`Invalid environment "${environment}"`);
}
return environments[environment];
}
@@ -1,30 +0,0 @@
// import { RatingInfo, Region, asset } from '@blitzkit/core';
// const cache: Record<
// Region,
// Record<
// number,
// RatingInfo & {
// detail: undefined;
// }
// >
// > = {
// com: {},
// eu: {},
// asia: {},
// };
// export async function getArchivedRatingInfo(region: Region, season: number) {
// if (!cache[region][season]) {
// const info = await fetch(
// asset(`regions/${region}/rating/${season}/info.cdon.lz4`),
// )
// .then((response) => response.arrayBuffer())
// .then((buffer) =>
// superDecompress<RatingInfo & { detail: undefined }>(buffer),
// );
// cache[region][season] = info;
// }
// return cache[region][season];
// }
+15 -3
View File
@@ -1,22 +1,34 @@
export * from "./alias";
export * from "./assertSecret";
export * from "./availableProvisions";
export * from "./averageDefinitions";
export * from "./coefficient";
export * from "./createDefaultProvisions";
export * from "./createDefaultSkills";
export * from "./degressiveStat";
export * from "./discoveredIdDefinitions";
export * from "./emblemURL";
export * from "./environment";
export * from "./eventManager";
export * from "./getTimeDaysAgo";
export * from "./isExplosive";
export * from "./metaSortTank";
export * from "./normalizeBoundingBox";
export * from "./patientFetchJSON";
export * from "./popularTanks";
export * from "./progressiveStat";
export * from "./resolveDpm";
export * from "./resolvePenetrationCoefficient";
export * from "./reviews";
export * from "./romanize";
export * from "./sluggify";
export * from "./staticAsset";
export * from "./sum";
export * from "./tankDefinitions";
export * from "./tankIcon";
export * from "./tankNames";
export * from "./unionBoundingBox";
export * from "./uniqueGuns";
export * from "./unwrapBlitzkitResponse";
export * from "./votes";
export * from "./createDefaultProvisions";
export * from "./metaSortTank";
export * from "./romanize";
export * from "./youtubers";
@@ -0,0 +1,7 @@
import { staticAsset } from "@blitzkit/core";
import { fetchPB } from "../protobuf";
import { PopularTanks } from "../protos";
export function fetchPopularTanks() {
return fetchPB(staticAsset("definitions/popular-tanks.pb"), PopularTanks);
}
@@ -8,12 +8,12 @@ export function resolvePenetrationCoefficient(
) {
const calibrated = calibratedShells
? type === ShellType.SHELL_TYPE_AP
? 1.08
? 1.06
: type === ShellType.SHELL_TYPE_APCR
? 1.05
? 1.06
: type === ShellType.SHELL_TYPE_HEAT
? 1.13
: 1.08
? 1.07
: 1.07
: 1;
const equalizer = (equalize ? equalizerObj?.penetration : undefined) ?? 1;
+7
View File
@@ -0,0 +1,7 @@
import { staticAsset } from "@blitzkit/core";
import { fetchPB } from "../protobuf";
import { Reviews } from "../protos";
export function fetchReviews() {
return fetchPB(staticAsset("definitions/reviews.pb"), Reviews);
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { deburr } from "lodash-es";
import { slugify } from "transliteration";
const nonAlphanumericRegex = /[^a-z0-9]/g;
const multipleDashesRegex = /--+/g;
@@ -6,7 +6,7 @@ const trailingDashRegex = /-$/g;
const leadingDashRegex = /^-/g;
export function sluggify(value: string) {
let slug = deburr(value).toLowerCase();
let slug = slugify(value);
slug = slug.replaceAll(nonAlphanumericRegex, "-");
slug = slug.replaceAll(multipleDashesRegex, "-");
slug = slug.replaceAll(trailingDashRegex, "");
@@ -0,0 +1,5 @@
import { assertSecret } from "./assertSecret";
export function staticAsset(path: string) {
return `${assertSecret(import.meta.env.PUBLIC_STATIC_ASSET_BASE)}/${path}`;
}
@@ -0,0 +1,74 @@
import { times } from "lodash-es";
import { CrewType, ShellType, TankClass, TankType } from "../../protos";
export const SHELL_NAMES: Record<ShellType, string> = {
[ShellType.SHELL_TYPE_AP]: "AP",
[ShellType.SHELL_TYPE_APCR]: "APCR",
[ShellType.SHELL_TYPE_HEAT]: "HEAT",
[ShellType.SHELL_TYPE_HE]: "HE",
};
export const CREW_MEMBER_NAMES = {
[CrewType.CREW_TYPE_COMMANDER]: "commander",
[CrewType.CREW_TYPE_DRIVER]: "driver",
[CrewType.CREW_TYPE_GUNNER]: "gunner",
[CrewType.CREW_TYPE_LOADER]: "loader",
[CrewType.CREW_TYPE_RADIOMAN]: "radioman",
} as const;
/**
* @deprecated use svg
*/
export const TANK_ICONS: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/BIHSEH0.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/CSNha5V.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/wvf3ltm.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/ECeqlZa.png",
};
/**
* @deprecated use svg
*/
export const TANK_ICONS_PREMIUM: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/TCu3EdR.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/zdkpTRb.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/3z7eHX6.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/P3vbmyA.png",
};
/**
* @deprecated use svg
*/
export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/WTjeirB.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/EwhtKkU.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/u8YDMBh.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/8xRf3nc.png",
};
export const TIERS = times(10, (index) => index + 1);
export const TIER_ROMAN_NUMERALS: Record<number, string> = {
1: "I",
2: "II",
3: "III",
4: "IV",
5: "V",
6: "VI",
7: "VII",
8: "VIII",
9: "IX",
10: "X",
};
export const flags: Record<string, string> = {
ussr: "<:ussr:1218421042033197197>",
germany: "🇩🇪",
usa: "🇺🇸",
china: "🇨🇳",
uk: "🇬🇧",
france: "🇫🇷",
japan: "🇯🇵",
european: "🇪🇺",
other: "<:other:1218421572243558482>",
};
export const TREE_TYPE_ICONS: Record<TankType, Record<TankClass, string>> = {
[TankType.TANK_TYPE_RESEARCHABLE]: TANK_ICONS,
[TankType.TANK_TYPE_PREMIUM]: TANK_ICONS_PREMIUM,
[TankType.TANK_TYPE_COLLECTOR]: TANK_ICONS_COLLECTOR,
};
@@ -0,0 +1 @@
export * from "./constants";
+7
View File
@@ -0,0 +1,7 @@
import { alias } from "@blitzkit/core";
export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 };
export function tankIcon(id: number, size: "big" | "small" = "big") {
return alias("api", `/tanks/${id}/icons/${size}.webp`);
}
+51
View File
@@ -0,0 +1,51 @@
import { BlitzKitAPI } from "@blitzkit/core";
import locales from "@blitzkit/i18n/locales.json";
import { deburr } from "lodash-es";
import { I18nString } from "../protos";
export async function fetchTankNames(api: BlitzKitAPI) {
const [tankDefinitions, camouflageDefinitions] = await Promise.all([
api.tankDefinitions(),
api.camouflageDefinitions(),
]);
const tankDefinitionsArray = Object.values(tankDefinitions.tanks);
return await Promise.all(
tankDefinitionsArray.map(async (tank) => {
const searchableNameDeburr: I18nString = { locales: {} };
Object.entries(tank.name!).forEach(([key, value]) => {
searchableNameDeburr.locales[key] = deburr(value);
});
return {
id: tank.id,
name: tank.name,
searchableName: tank.name,
searchableNameDeburr,
camouflages: tank.camouflages
?.map((id) =>
locales.supported.map(
({ locale }) =>
camouflageDefinitions.camouflages[id]?.name!.locales[locale],
),
)
.flat()
.filter(Boolean)
.map(deburr)
.join(" "),
treeType: tank.type,
};
}),
);
}
export const SEARCH_KEYS = [
...locales.supported
.map(({ locale }) => [
`searchableName.locales.${locale}`,
`searchableNameDeburr.locales.${locale}`,
])
.flat(),
"camouflages",
];
@@ -14,7 +14,7 @@ export abstract class AbstractVFS {
abstract has(path: string): Promise<boolean>;
abstract raw(path: string): Promise<Uint8Array>;
abstract raw(path: string): Promise<Uint8Array<ArrayBuffer>>;
async resolve(path: string) {
const normalized = normalize(path);
@@ -46,7 +46,15 @@ export abstract class AbstractVFS {
return buffer;
}
abstract dir(path: string): Promise<string[]>;
protected abstract _dir(path: string): Promise<string[]>;
async dir(path: string) {
const files = await this._dir(path);
return files.map((file) =>
file.endsWith(".dvpl") ? file.slice(0, -5) : file,
);
}
async text(path: string) {
const file = await this.file(path);
@@ -63,7 +71,7 @@ export abstract class AbstractVFS {
return this.xmlParser.parse(file) as Type;
}
async _csv(text: string, options: Options = {}) {
protected async _csv(text: string, options: Options = {}) {
const parsed = parseCSV(text, options);
return parsed;
}
@@ -23,8 +23,12 @@ export class LocalVFS extends AbstractVFS {
return array;
}
async dir(path: string) {
return await readdir(`${this.base}/${path}`);
async _dir(path: string) {
try {
return await readdir(`${this.base}/${path}`);
} catch (_) {
return [];
}
}
dispose() {}
@@ -35,7 +35,7 @@ export class MixedVFS extends AbstractVFS {
throw new Error(`File not found: ${path}`);
}
async dir(path: string) {
async _dir(path: string) {
const files = new Set<string>();
for (const vfs of this.vfses) {
@@ -89,7 +89,7 @@ export class SteamVFS extends AbstractVFS {
return new Uint8Array(downloaded.file);
}
async dir(path: string) {
async _dir(path: string) {
const parentSegments = path.split("/").length;
const children = new Set<string>();
@@ -33,7 +33,7 @@ export class ZipVFS extends AbstractVFS {
return new Uint8Array(arrayBuffer);
}
async dir(path: string) {
async _dir(path: string) {
const parentSegments = path.split("/").length;
const children = new Set<string>();
+1 -1
View File
@@ -4,7 +4,7 @@ export * from "./imgur";
export * from "./math";
export * from "./patreon";
export * from "./protobuf";
export * from "./protos";
export * from "./statistics";
export * from "./streams";
export * from "./types";
export * from "./secrets";
-3
View File
@@ -1,3 +0,0 @@
export function copySign(a: number, b: number) {
return b >= 0 ? Math.abs(a) : -Math.abs(a);
}
-1
View File
@@ -3,7 +3,6 @@ export * from "./fisherYates";
export * from "./formatCompact";
export * from "./isNumber";
export * from "./NaNFallback";
export * from "./normalizeAngleDeg";
export * from "./normalizeAngleRad";
export * from "./normalToEuler";
export * from "./numberToRomanNumeral";
@@ -1,6 +0,0 @@
export function normalizeAngleDeg(angle: number) {
const newAngle = angle % 360;
if (newAngle > 360) return newAngle - 360;
if (newAngle < -360) return newAngle + 360;
return newAngle;
}
+5 -5
View File
@@ -1,11 +1,11 @@
import { secret } from "../secrets";
import { assertSecret } from '../blitzkit/assertSecret';
export function patreonLoginUrl() {
const params = new URLSearchParams({
response_type: "code",
client_id: secret("PUBLIC_PATREON_CLIENT_ID"),
redirect_uri: secret("PUBLIC_PATREON_REDIRECT_URI"),
scope: "identity",
response_type: 'code',
client_id: assertSecret(import.meta.env.PUBLIC_PATREON_CLIENT_ID),
redirect_uri: assertSecret(import.meta.env.PUBLIC_PATREON_REDIRECT_URI),
scope: 'identity',
});
return `https://www.patreon.com/oauth2/authorize?${params}`;
+40 -81
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.11.2
// protoc v5.28.2
// protoc-gen-ts_proto v2.7.7
// protoc v3.19.6
// source: average_definitions.proto
/* eslint-disable */
@@ -64,7 +64,7 @@ function createBaseAverageDefinitions(): AverageDefinitions {
export const AverageDefinitions: MessageFns<AverageDefinitions> = {
encode(message: AverageDefinitions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
globalThis.Object.entries(message.averages).forEach(([key, value]: [string, AverageDefinitionsEntry]) => {
Object.entries(message.averages).forEach(([key, value]) => {
AverageDefinitions_AveragesEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join();
});
if (message.samples !== undefined) {
@@ -122,13 +122,10 @@ export const AverageDefinitions: MessageFns<AverageDefinitions> = {
fromJSON(object: any): AverageDefinitions {
return {
averages: isObject(object.averages)
? (globalThis.Object.entries(object.averages) as [string, any][]).reduce(
(acc: { [key: number]: AverageDefinitionsEntry }, [key, value]: [string, any]) => {
acc[globalThis.Number(key)] = AverageDefinitionsEntry.fromJSON(value);
return acc;
},
{},
)
? Object.entries(object.averages).reduce<{ [key: number]: AverageDefinitionsEntry }>((acc, [key, value]) => {
acc[globalThis.Number(key)] = AverageDefinitionsEntry.fromJSON(value);
return acc;
}, {})
: {},
samples: isSet(object.samples) ? Samples.fromJSON(object.samples) : undefined,
time: isSet(object.time) ? globalThis.Number(object.time) : 0,
@@ -138,7 +135,7 @@ export const AverageDefinitions: MessageFns<AverageDefinitions> = {
toJSON(message: AverageDefinitions): unknown {
const obj: any = {};
if (message.averages) {
const entries = globalThis.Object.entries(message.averages) as [string, AverageDefinitionsEntry][];
const entries = Object.entries(message.averages);
if (entries.length > 0) {
obj.averages = {};
entries.forEach(([k, v]) => {
@@ -160,8 +157,8 @@ export const AverageDefinitions: MessageFns<AverageDefinitions> = {
},
fromPartial<I extends Exact<DeepPartial<AverageDefinitions>, I>>(object: I): AverageDefinitions {
const message = createBaseAverageDefinitions();
message.averages = (globalThis.Object.entries(object.averages ?? {}) as [string, AverageDefinitionsEntry][]).reduce(
(acc: { [key: number]: AverageDefinitionsEntry }, [key, value]: [string, AverageDefinitionsEntry]) => {
message.averages = Object.entries(object.averages ?? {}).reduce<{ [key: number]: AverageDefinitionsEntry }>(
(acc, [key, value]) => {
if (value !== undefined) {
acc[globalThis.Number(key)] = AverageDefinitionsEntry.fromPartial(value);
}
@@ -620,59 +617,25 @@ export const AverageDefinitionsAllStats: MessageFns<AverageDefinitionsAllStats>
fromJSON(object: any): AverageDefinitionsAllStats {
return {
battles: isSet(object.battles) ? globalThis.Number(object.battles) : 0,
capture_points: isSet(object.capturePoints)
? globalThis.Number(object.capturePoints)
: isSet(object.capture_points)
? globalThis.Number(object.capture_points)
: 0,
damage_dealt: isSet(object.damageDealt)
? globalThis.Number(object.damageDealt)
: isSet(object.damage_dealt)
? globalThis.Number(object.damage_dealt)
: 0,
damage_received: isSet(object.damageReceived)
? globalThis.Number(object.damageReceived)
: isSet(object.damage_received)
? globalThis.Number(object.damage_received)
: 0,
dropped_capture_points: isSet(object.droppedCapturePoints)
? globalThis.Number(object.droppedCapturePoints)
: isSet(object.dropped_capture_points)
capture_points: isSet(object.capture_points) ? globalThis.Number(object.capture_points) : 0,
damage_dealt: isSet(object.damage_dealt) ? globalThis.Number(object.damage_dealt) : 0,
damage_received: isSet(object.damage_received) ? globalThis.Number(object.damage_received) : 0,
dropped_capture_points: isSet(object.dropped_capture_points)
? globalThis.Number(object.dropped_capture_points)
: 0,
frags: isSet(object.frags) ? globalThis.Number(object.frags) : 0,
frags8p: isSet(object.frags8p) ? globalThis.Number(object.frags8p) : 0,
hits: isSet(object.hits) ? globalThis.Number(object.hits) : 0,
losses: isSet(object.losses) ? globalThis.Number(object.losses) : 0,
max_frags: isSet(object.maxFrags)
? globalThis.Number(object.maxFrags)
: isSet(object.max_frags)
? globalThis.Number(object.max_frags)
: 0,
max_xp: isSet(object.maxXp)
? globalThis.Number(object.maxXp)
: isSet(object.max_xp)
? globalThis.Number(object.max_xp)
: 0,
max_frags: isSet(object.max_frags) ? globalThis.Number(object.max_frags) : 0,
max_xp: isSet(object.max_xp) ? globalThis.Number(object.max_xp) : 0,
shots: isSet(object.shots) ? globalThis.Number(object.shots) : 0,
spotted: isSet(object.spotted) ? globalThis.Number(object.spotted) : 0,
survived_battles: isSet(object.survivedBattles)
? globalThis.Number(object.survivedBattles)
: isSet(object.survived_battles)
? globalThis.Number(object.survived_battles)
: 0,
win_and_survived: isSet(object.winAndSurvived)
? globalThis.Number(object.winAndSurvived)
: isSet(object.win_and_survived)
? globalThis.Number(object.win_and_survived)
: 0,
survived_battles: isSet(object.survived_battles) ? globalThis.Number(object.survived_battles) : 0,
win_and_survived: isSet(object.win_and_survived) ? globalThis.Number(object.win_and_survived) : 0,
wins: isSet(object.wins) ? globalThis.Number(object.wins) : 0,
xp: isSet(object.xp) ? globalThis.Number(object.xp) : 0,
battle_life_time: isSet(object.battleLifeTime)
? globalThis.Number(object.battleLifeTime)
: isSet(object.battle_life_time)
? globalThis.Number(object.battle_life_time)
: 0,
battle_life_time: isSet(object.battle_life_time) ? globalThis.Number(object.battle_life_time) : 0,
};
},
@@ -682,16 +645,16 @@ export const AverageDefinitionsAllStats: MessageFns<AverageDefinitionsAllStats>
obj.battles = message.battles;
}
if (message.capture_points !== 0) {
obj.capturePoints = message.capture_points;
obj.capture_points = message.capture_points;
}
if (message.damage_dealt !== 0) {
obj.damageDealt = message.damage_dealt;
obj.damage_dealt = message.damage_dealt;
}
if (message.damage_received !== 0) {
obj.damageReceived = message.damage_received;
obj.damage_received = message.damage_received;
}
if (message.dropped_capture_points !== 0) {
obj.droppedCapturePoints = message.dropped_capture_points;
obj.dropped_capture_points = message.dropped_capture_points;
}
if (message.frags !== 0) {
obj.frags = message.frags;
@@ -706,10 +669,10 @@ export const AverageDefinitionsAllStats: MessageFns<AverageDefinitionsAllStats>
obj.losses = message.losses;
}
if (message.max_frags !== 0) {
obj.maxFrags = message.max_frags;
obj.max_frags = message.max_frags;
}
if (message.max_xp !== 0) {
obj.maxXp = message.max_xp;
obj.max_xp = message.max_xp;
}
if (message.shots !== 0) {
obj.shots = message.shots;
@@ -718,10 +681,10 @@ export const AverageDefinitionsAllStats: MessageFns<AverageDefinitionsAllStats>
obj.spotted = message.spotted;
}
if (message.survived_battles !== 0) {
obj.survivedBattles = message.survived_battles;
obj.survived_battles = message.survived_battles;
}
if (message.win_and_survived !== 0) {
obj.winAndSurvived = message.win_and_survived;
obj.win_and_survived = message.win_and_survived;
}
if (message.wins !== 0) {
obj.wins = message.wins;
@@ -730,7 +693,7 @@ export const AverageDefinitionsAllStats: MessageFns<AverageDefinitionsAllStats>
obj.xp = message.xp;
}
if (message.battle_life_time !== 0) {
obj.battleLifeTime = message.battle_life_time;
obj.battle_life_time = message.battle_life_time;
}
return obj;
},
@@ -866,16 +829,12 @@ export const Samples: MessageFns<Samples> = {
fromJSON(object: any): Samples {
return {
d_1: isSet(object.d1) ? globalThis.Number(object.d1) : isSet(object.d_1) ? globalThis.Number(object.d_1) : 0,
d_7: isSet(object.d7) ? globalThis.Number(object.d7) : isSet(object.d_7) ? globalThis.Number(object.d_7) : 0,
d_30: isSet(object.d30) ? globalThis.Number(object.d30) : isSet(object.d_30) ? globalThis.Number(object.d_30) : 0,
d_60: isSet(object.d60) ? globalThis.Number(object.d60) : isSet(object.d_60) ? globalThis.Number(object.d_60) : 0,
d_90: isSet(object.d90) ? globalThis.Number(object.d90) : isSet(object.d_90) ? globalThis.Number(object.d_90) : 0,
d_120: isSet(object.d120)
? globalThis.Number(object.d120)
: isSet(object.d_120)
? globalThis.Number(object.d_120)
: 0,
d_1: isSet(object.d_1) ? globalThis.Number(object.d_1) : 0,
d_7: isSet(object.d_7) ? globalThis.Number(object.d_7) : 0,
d_30: isSet(object.d_30) ? globalThis.Number(object.d_30) : 0,
d_60: isSet(object.d_60) ? globalThis.Number(object.d_60) : 0,
d_90: isSet(object.d_90) ? globalThis.Number(object.d_90) : 0,
d_120: isSet(object.d_120) ? globalThis.Number(object.d_120) : 0,
total: isSet(object.total) ? globalThis.Number(object.total) : 0,
};
},
@@ -883,22 +842,22 @@ export const Samples: MessageFns<Samples> = {
toJSON(message: Samples): unknown {
const obj: any = {};
if (message.d_1 !== 0) {
obj.d1 = Math.round(message.d_1);
obj.d_1 = Math.round(message.d_1);
}
if (message.d_7 !== 0) {
obj.d7 = Math.round(message.d_7);
obj.d_7 = Math.round(message.d_7);
}
if (message.d_30 !== 0) {
obj.d30 = Math.round(message.d_30);
obj.d_30 = Math.round(message.d_30);
}
if (message.d_60 !== 0) {
obj.d60 = Math.round(message.d_60);
obj.d_60 = Math.round(message.d_60);
}
if (message.d_90 !== 0) {
obj.d90 = Math.round(message.d_90);
obj.d_90 = Math.round(message.d_90);
}
if (message.d_120 !== 0) {
obj.d120 = Math.round(message.d_120);
obj.d_120 = Math.round(message.d_120);
}
if (message.total !== 0) {
obj.total = Math.round(message.total);
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.11.2
// protoc v5.28.2
// protoc-gen-ts_proto v2.7.7
// protoc v3.19.6
// source: camouflage_definitions.proto
/* eslint-disable */
@@ -32,7 +32,7 @@ function createBaseCamouflageDefinitions(): CamouflageDefinitions {
export const CamouflageDefinitions: MessageFns<CamouflageDefinitions> = {
encode(message: CamouflageDefinitions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
globalThis.Object.entries(message.camouflages).forEach(([key, value]: [string, Camouflage]) => {
Object.entries(message.camouflages).forEach(([key, value]) => {
CamouflageDefinitions_CamouflagesEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join();
});
return writer;
@@ -68,13 +68,10 @@ export const CamouflageDefinitions: MessageFns<CamouflageDefinitions> = {
fromJSON(object: any): CamouflageDefinitions {
return {
camouflages: isObject(object.camouflages)
? (globalThis.Object.entries(object.camouflages) as [string, any][]).reduce(
(acc: { [key: number]: Camouflage }, [key, value]: [string, any]) => {
acc[globalThis.Number(key)] = Camouflage.fromJSON(value);
return acc;
},
{},
)
? Object.entries(object.camouflages).reduce<{ [key: number]: Camouflage }>((acc, [key, value]) => {
acc[globalThis.Number(key)] = Camouflage.fromJSON(value);
return acc;
}, {})
: {},
};
},
@@ -82,7 +79,7 @@ export const CamouflageDefinitions: MessageFns<CamouflageDefinitions> = {
toJSON(message: CamouflageDefinitions): unknown {
const obj: any = {};
if (message.camouflages) {
const entries = globalThis.Object.entries(message.camouflages) as [string, Camouflage][];
const entries = Object.entries(message.camouflages);
if (entries.length > 0) {
obj.camouflages = {};
entries.forEach(([k, v]) => {
@@ -98,8 +95,8 @@ export const CamouflageDefinitions: MessageFns<CamouflageDefinitions> = {
},
fromPartial<I extends Exact<DeepPartial<CamouflageDefinitions>, I>>(object: I): CamouflageDefinitions {
const message = createBaseCamouflageDefinitions();
message.camouflages = (globalThis.Object.entries(object.camouflages ?? {}) as [string, Camouflage][]).reduce(
(acc: { [key: number]: Camouflage }, [key, value]: [string, Camouflage]) => {
message.camouflages = Object.entries(object.camouflages ?? {}).reduce<{ [key: number]: Camouflage }>(
(acc, [key, value]) => {
if (value !== undefined) {
acc[globalThis.Number(key)] = Camouflage.fromPartial(value);
}
@@ -266,16 +263,8 @@ export const Camouflage: MessageFns<Camouflage> = {
return {
id: isSet(object.id) ? globalThis.Number(object.id) : 0,
name: isSet(object.name) ? I18nString.fromJSON(object.name) : undefined,
tank_name: isSet(object.tankName)
? I18nString.fromJSON(object.tankName)
: isSet(object.tank_name)
? I18nString.fromJSON(object.tank_name)
: undefined,
tank_name_full: isSet(object.tankNameFull)
? I18nString.fromJSON(object.tankNameFull)
: isSet(object.tank_name_full)
? I18nString.fromJSON(object.tank_name_full)
: undefined,
tank_name: isSet(object.tank_name) ? I18nString.fromJSON(object.tank_name) : undefined,
tank_name_full: isSet(object.tank_name_full) ? I18nString.fromJSON(object.tank_name_full) : undefined,
};
},
@@ -288,10 +277,10 @@ export const Camouflage: MessageFns<Camouflage> = {
obj.name = I18nString.toJSON(message.name);
}
if (message.tank_name !== undefined) {
obj.tankName = I18nString.toJSON(message.tank_name);
obj.tank_name = I18nString.toJSON(message.tank_name);
}
if (message.tank_name_full !== undefined) {
obj.tankNameFull = I18nString.toJSON(message.tank_name_full);
obj.tank_name_full = I18nString.toJSON(message.tank_name_full);
}
return obj;
},
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.11.2
// protoc v5.28.2
// protoc-gen-ts_proto v2.7.7
// protoc v3.19.6
// source: consumable_definitions.proto
/* eslint-disable */
@@ -88,7 +88,7 @@ function createBaseConsumableDefinitions(): ConsumableDefinitions {
export const ConsumableDefinitions: MessageFns<ConsumableDefinitions> = {
encode(message: ConsumableDefinitions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
globalThis.Object.entries(message.consumables).forEach(([key, value]: [string, Consumable]) => {
Object.entries(message.consumables).forEach(([key, value]) => {
ConsumableDefinitions_ConsumablesEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join();
});
return writer;
@@ -124,13 +124,10 @@ export const ConsumableDefinitions: MessageFns<ConsumableDefinitions> = {
fromJSON(object: any): ConsumableDefinitions {
return {
consumables: isObject(object.consumables)
? (globalThis.Object.entries(object.consumables) as [string, any][]).reduce(
(acc: { [key: number]: Consumable }, [key, value]: [string, any]) => {
acc[globalThis.Number(key)] = Consumable.fromJSON(value);
return acc;
},
{},
)
? Object.entries(object.consumables).reduce<{ [key: number]: Consumable }>((acc, [key, value]) => {
acc[globalThis.Number(key)] = Consumable.fromJSON(value);
return acc;
}, {})
: {},
};
},
@@ -138,7 +135,7 @@ export const ConsumableDefinitions: MessageFns<ConsumableDefinitions> = {
toJSON(message: ConsumableDefinitions): unknown {
const obj: any = {};
if (message.consumables) {
const entries = globalThis.Object.entries(message.consumables) as [string, Consumable][];
const entries = Object.entries(message.consumables);
if (entries.length > 0) {
obj.consumables = {};
entries.forEach(([k, v]) => {
@@ -154,8 +151,8 @@ export const ConsumableDefinitions: MessageFns<ConsumableDefinitions> = {
},
fromPartial<I extends Exact<DeepPartial<ConsumableDefinitions>, I>>(object: I): ConsumableDefinitions {
const message = createBaseConsumableDefinitions();
message.consumables = (globalThis.Object.entries(object.consumables ?? {}) as [string, Consumable][]).reduce(
(acc: { [key: number]: Consumable }, [key, value]: [string, Consumable]) => {
message.consumables = Object.entries(object.consumables ?? {}).reduce<{ [key: number]: Consumable }>(
(acc, [key, value]) => {
if (value !== undefined) {
acc[globalThis.Number(key)] = Consumable.fromPartial(value);
}
@@ -357,11 +354,7 @@ export const Consumable: MessageFns<Consumable> = {
name: isSet(object.name) ? I18nString.fromJSON(object.name) : undefined,
cooldown: isSet(object.cooldown) ? globalThis.Number(object.cooldown) : 0,
duration: isSet(object.duration) ? globalThis.Number(object.duration) : 0,
game_mode_exclusive: isSet(object.gameModeExclusive)
? globalThis.Boolean(object.gameModeExclusive)
: isSet(object.game_mode_exclusive)
? globalThis.Boolean(object.game_mode_exclusive)
: false,
game_mode_exclusive: isSet(object.game_mode_exclusive) ? globalThis.Boolean(object.game_mode_exclusive) : false,
include: globalThis.Array.isArray(object?.include)
? object.include.map((e: any) => ConsumableTankFilter.fromJSON(e))
: [],
@@ -386,7 +379,7 @@ export const Consumable: MessageFns<Consumable> = {
obj.duration = message.duration;
}
if (message.game_mode_exclusive !== false) {
obj.gameModeExclusive = message.game_mode_exclusive;
obj.game_mode_exclusive = message.game_mode_exclusive;
}
if (message.include?.length) {
obj.include = message.include.map((e) => ConsumableTankFilter.toJSON(e));
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.11.2
// protoc v5.28.2
// protoc-gen-ts_proto v2.7.7
// protoc v3.19.6
// source: equipment_definitions.proto
/* eslint-disable */
@@ -45,10 +45,10 @@ function createBaseEquipmentDefinitions(): EquipmentDefinitions {
export const EquipmentDefinitions: MessageFns<EquipmentDefinitions> = {
encode(message: EquipmentDefinitions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
globalThis.Object.entries(message.presets).forEach(([key, value]: [string, EquipmentPreset]) => {
Object.entries(message.presets).forEach(([key, value]) => {
EquipmentDefinitions_PresetsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join();
});
globalThis.Object.entries(message.equipments).forEach(([key, value]: [string, Equipment]) => {
Object.entries(message.equipments).forEach(([key, value]) => {
EquipmentDefinitions_EquipmentsEntry.encode({ key: key as any, value }, writer.uint32(18).fork()).join();
});
return writer;
@@ -95,22 +95,16 @@ export const EquipmentDefinitions: MessageFns<EquipmentDefinitions> = {
fromJSON(object: any): EquipmentDefinitions {
return {
presets: isObject(object.presets)
? (globalThis.Object.entries(object.presets) as [string, any][]).reduce(
(acc: { [key: string]: EquipmentPreset }, [key, value]: [string, any]) => {
acc[key] = EquipmentPreset.fromJSON(value);
return acc;
},
{},
)
? Object.entries(object.presets).reduce<{ [key: string]: EquipmentPreset }>((acc, [key, value]) => {
acc[key] = EquipmentPreset.fromJSON(value);
return acc;
}, {})
: {},
equipments: isObject(object.equipments)
? (globalThis.Object.entries(object.equipments) as [string, any][]).reduce(
(acc: { [key: number]: Equipment }, [key, value]: [string, any]) => {
acc[globalThis.Number(key)] = Equipment.fromJSON(value);
return acc;
},
{},
)
? Object.entries(object.equipments).reduce<{ [key: number]: Equipment }>((acc, [key, value]) => {
acc[globalThis.Number(key)] = Equipment.fromJSON(value);
return acc;
}, {})
: {},
};
},
@@ -118,7 +112,7 @@ export const EquipmentDefinitions: MessageFns<EquipmentDefinitions> = {
toJSON(message: EquipmentDefinitions): unknown {
const obj: any = {};
if (message.presets) {
const entries = globalThis.Object.entries(message.presets) as [string, EquipmentPreset][];
const entries = Object.entries(message.presets);
if (entries.length > 0) {
obj.presets = {};
entries.forEach(([k, v]) => {
@@ -127,7 +121,7 @@ export const EquipmentDefinitions: MessageFns<EquipmentDefinitions> = {
}
}
if (message.equipments) {
const entries = globalThis.Object.entries(message.equipments) as [string, Equipment][];
const entries = Object.entries(message.equipments);
if (entries.length > 0) {
obj.equipments = {};
entries.forEach(([k, v]) => {
@@ -143,8 +137,8 @@ export const EquipmentDefinitions: MessageFns<EquipmentDefinitions> = {
},
fromPartial<I extends Exact<DeepPartial<EquipmentDefinitions>, I>>(object: I): EquipmentDefinitions {
const message = createBaseEquipmentDefinitions();
message.presets = (globalThis.Object.entries(object.presets ?? {}) as [string, EquipmentPreset][]).reduce(
(acc: { [key: string]: EquipmentPreset }, [key, value]: [string, EquipmentPreset]) => {
message.presets = Object.entries(object.presets ?? {}).reduce<{ [key: string]: EquipmentPreset }>(
(acc, [key, value]) => {
if (value !== undefined) {
acc[key] = EquipmentPreset.fromPartial(value);
}
@@ -152,8 +146,8 @@ export const EquipmentDefinitions: MessageFns<EquipmentDefinitions> = {
},
{},
);
message.equipments = (globalThis.Object.entries(object.equipments ?? {}) as [string, Equipment][]).reduce(
(acc: { [key: number]: Equipment }, [key, value]: [string, Equipment]) => {
message.equipments = Object.entries(object.equipments ?? {}).reduce<{ [key: number]: Equipment }>(
(acc, [key, value]) => {
if (value !== undefined) {
acc[globalThis.Number(key)] = Equipment.fromPartial(value);
}
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.11.2
// protoc v5.28.2
// protoc-gen-ts_proto v2.7.7
// protoc v3.19.6
// source: gallery.proto
/* eslint-disable */
@@ -7,7 +7,7 @@ import "i18n.proto";
message GameDefinitions {
required string version = 1;
repeated string nations = 2;
map<uint32, GameMode> gameModes = 3;
map<string, GameMode> gameModes = 3;
map<uint32, GameModeRole> roles = 4;
}
+34 -43
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.11.2
// protoc v5.28.2
// protoc-gen-ts_proto v2.7.7
// protoc v3.19.6
// source: game_definitions.proto
/* eslint-disable */
@@ -13,12 +13,12 @@ export const protobufPackage = "blitzkit";
export interface GameDefinitions {
version: string;
nations: string[];
gameModes: { [key: number]: GameMode };
gameModes: { [key: string]: GameMode };
roles: { [key: number]: GameModeRole };
}
export interface GameDefinitions_GameModesEntry {
key: number;
key: string;
value: GameMode | undefined;
}
@@ -48,10 +48,10 @@ export const GameDefinitions: MessageFns<GameDefinitions> = {
for (const v of message.nations) {
writer.uint32(18).string(v!);
}
globalThis.Object.entries(message.gameModes).forEach(([key, value]: [string, GameMode]) => {
Object.entries(message.gameModes).forEach(([key, value]) => {
GameDefinitions_GameModesEntry.encode({ key: key as any, value }, writer.uint32(26).fork()).join();
});
globalThis.Object.entries(message.roles).forEach(([key, value]: [string, GameModeRole]) => {
Object.entries(message.roles).forEach(([key, value]) => {
GameDefinitions_RolesEntry.encode({ key: key as any, value }, writer.uint32(34).fork()).join();
});
return writer;
@@ -116,22 +116,16 @@ export const GameDefinitions: MessageFns<GameDefinitions> = {
version: isSet(object.version) ? globalThis.String(object.version) : "",
nations: globalThis.Array.isArray(object?.nations) ? object.nations.map((e: any) => globalThis.String(e)) : [],
gameModes: isObject(object.gameModes)
? (globalThis.Object.entries(object.gameModes) as [string, any][]).reduce(
(acc: { [key: number]: GameMode }, [key, value]: [string, any]) => {
acc[globalThis.Number(key)] = GameMode.fromJSON(value);
return acc;
},
{},
)
? Object.entries(object.gameModes).reduce<{ [key: string]: GameMode }>((acc, [key, value]) => {
acc[key] = GameMode.fromJSON(value);
return acc;
}, {})
: {},
roles: isObject(object.roles)
? (globalThis.Object.entries(object.roles) as [string, any][]).reduce(
(acc: { [key: number]: GameModeRole }, [key, value]: [string, any]) => {
acc[globalThis.Number(key)] = GameModeRole.fromJSON(value);
return acc;
},
{},
)
? Object.entries(object.roles).reduce<{ [key: number]: GameModeRole }>((acc, [key, value]) => {
acc[globalThis.Number(key)] = GameModeRole.fromJSON(value);
return acc;
}, {})
: {},
};
},
@@ -145,7 +139,7 @@ export const GameDefinitions: MessageFns<GameDefinitions> = {
obj.nations = message.nations;
}
if (message.gameModes) {
const entries = globalThis.Object.entries(message.gameModes) as [string, GameMode][];
const entries = Object.entries(message.gameModes);
if (entries.length > 0) {
obj.gameModes = {};
entries.forEach(([k, v]) => {
@@ -154,7 +148,7 @@ export const GameDefinitions: MessageFns<GameDefinitions> = {
}
}
if (message.roles) {
const entries = globalThis.Object.entries(message.roles) as [string, GameModeRole][];
const entries = Object.entries(message.roles);
if (entries.length > 0) {
obj.roles = {};
entries.forEach(([k, v]) => {
@@ -172,36 +166,33 @@ export const GameDefinitions: MessageFns<GameDefinitions> = {
const message = createBaseGameDefinitions();
message.version = object.version ?? "";
message.nations = object.nations?.map((e) => e) || [];
message.gameModes = (globalThis.Object.entries(object.gameModes ?? {}) as [string, GameMode][]).reduce(
(acc: { [key: number]: GameMode }, [key, value]: [string, GameMode]) => {
message.gameModes = Object.entries(object.gameModes ?? {}).reduce<{ [key: string]: GameMode }>(
(acc, [key, value]) => {
if (value !== undefined) {
acc[globalThis.Number(key)] = GameMode.fromPartial(value);
}
return acc;
},
{},
);
message.roles = (globalThis.Object.entries(object.roles ?? {}) as [string, GameModeRole][]).reduce(
(acc: { [key: number]: GameModeRole }, [key, value]: [string, GameModeRole]) => {
if (value !== undefined) {
acc[globalThis.Number(key)] = GameModeRole.fromPartial(value);
acc[key] = GameMode.fromPartial(value);
}
return acc;
},
{},
);
message.roles = Object.entries(object.roles ?? {}).reduce<{ [key: number]: GameModeRole }>((acc, [key, value]) => {
if (value !== undefined) {
acc[globalThis.Number(key)] = GameModeRole.fromPartial(value);
}
return acc;
}, {});
return message;
},
};
function createBaseGameDefinitions_GameModesEntry(): GameDefinitions_GameModesEntry {
return { key: 0, value: undefined };
return { key: "", value: undefined };
}
export const GameDefinitions_GameModesEntry: MessageFns<GameDefinitions_GameModesEntry> = {
encode(message: GameDefinitions_GameModesEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.key !== 0) {
writer.uint32(8).uint32(message.key);
if (message.key !== "") {
writer.uint32(10).string(message.key);
}
if (message.value !== undefined) {
GameMode.encode(message.value, writer.uint32(18).fork()).join();
@@ -217,11 +208,11 @@ export const GameDefinitions_GameModesEntry: MessageFns<GameDefinitions_GameMode
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
if (tag !== 10) {
break;
}
message.key = reader.uint32();
message.key = reader.string();
continue;
}
case 2: {
@@ -243,15 +234,15 @@ export const GameDefinitions_GameModesEntry: MessageFns<GameDefinitions_GameMode
fromJSON(object: any): GameDefinitions_GameModesEntry {
return {
key: isSet(object.key) ? globalThis.Number(object.key) : 0,
key: isSet(object.key) ? globalThis.String(object.key) : "",
value: isSet(object.value) ? GameMode.fromJSON(object.value) : undefined,
};
},
toJSON(message: GameDefinitions_GameModesEntry): unknown {
const obj: any = {};
if (message.key !== 0) {
obj.key = Math.round(message.key);
if (message.key !== "") {
obj.key = message.key;
}
if (message.value !== undefined) {
obj.value = GameMode.toJSON(message.value);
@@ -266,7 +257,7 @@ export const GameDefinitions_GameModesEntry: MessageFns<GameDefinitions_GameMode
object: I,
): GameDefinitions_GameModesEntry {
const message = createBaseGameDefinitions_GameModesEntry();
message.key = object.key ?? 0;
message.key = object.key ?? "";
message.value = (object.value !== undefined && object.value !== null)
? GameMode.fromPartial(object.value)
: undefined;
+14 -20
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.11.2
// protoc v5.28.2
// protoc-gen-ts_proto v2.7.7
// protoc v3.19.6
// source: i18n.proto
/* eslint-disable */
@@ -24,7 +24,7 @@ function createBaseI18nString(): I18nString {
export const I18nString: MessageFns<I18nString> = {
encode(message: I18nString, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
globalThis.Object.entries(message.locales).forEach(([key, value]: [string, string]) => {
Object.entries(message.locales).forEach(([key, value]) => {
I18nString_LocalesEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join();
});
return writer;
@@ -60,13 +60,10 @@ export const I18nString: MessageFns<I18nString> = {
fromJSON(object: any): I18nString {
return {
locales: isObject(object.locales)
? (globalThis.Object.entries(object.locales) as [string, any][]).reduce(
(acc: { [key: string]: string }, [key, value]: [string, any]) => {
acc[key] = globalThis.String(value);
return acc;
},
{},
)
? Object.entries(object.locales).reduce<{ [key: string]: string }>((acc, [key, value]) => {
acc[key] = String(value);
return acc;
}, {})
: {},
};
},
@@ -74,7 +71,7 @@ export const I18nString: MessageFns<I18nString> = {
toJSON(message: I18nString): unknown {
const obj: any = {};
if (message.locales) {
const entries = globalThis.Object.entries(message.locales) as [string, string][];
const entries = Object.entries(message.locales);
if (entries.length > 0) {
obj.locales = {};
entries.forEach(([k, v]) => {
@@ -90,15 +87,12 @@ export const I18nString: MessageFns<I18nString> = {
},
fromPartial<I extends Exact<DeepPartial<I18nString>, I>>(object: I): I18nString {
const message = createBaseI18nString();
message.locales = (globalThis.Object.entries(object.locales ?? {}) as [string, string][]).reduce(
(acc: { [key: string]: string }, [key, value]: [string, string]) => {
if (value !== undefined) {
acc[key] = globalThis.String(value);
}
return acc;
},
{},
);
message.locales = Object.entries(object.locales ?? {}).reduce<{ [key: string]: string }>((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = globalThis.String(value);
}
return acc;
}, {});
return message;
},
};
+14 -20
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.11.2
// protoc v5.28.2
// protoc-gen-ts_proto v2.7.7
// protoc v3.19.6
// source: map_definitions.proto
/* eslint-disable */
@@ -30,7 +30,7 @@ function createBaseMapDefinitions(): MapDefinitions {
export const MapDefinitions: MessageFns<MapDefinitions> = {
encode(message: MapDefinitions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
globalThis.Object.entries(message.maps).forEach(([key, value]: [string, MapDefinition]) => {
Object.entries(message.maps).forEach(([key, value]) => {
MapDefinitions_MapsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join();
});
return writer;
@@ -66,13 +66,10 @@ export const MapDefinitions: MessageFns<MapDefinitions> = {
fromJSON(object: any): MapDefinitions {
return {
maps: isObject(object.maps)
? (globalThis.Object.entries(object.maps) as [string, any][]).reduce(
(acc: { [key: number]: MapDefinition }, [key, value]: [string, any]) => {
acc[globalThis.Number(key)] = MapDefinition.fromJSON(value);
return acc;
},
{},
)
? Object.entries(object.maps).reduce<{ [key: number]: MapDefinition }>((acc, [key, value]) => {
acc[globalThis.Number(key)] = MapDefinition.fromJSON(value);
return acc;
}, {})
: {},
};
},
@@ -80,7 +77,7 @@ export const MapDefinitions: MessageFns<MapDefinitions> = {
toJSON(message: MapDefinitions): unknown {
const obj: any = {};
if (message.maps) {
const entries = globalThis.Object.entries(message.maps) as [string, MapDefinition][];
const entries = Object.entries(message.maps);
if (entries.length > 0) {
obj.maps = {};
entries.forEach(([k, v]) => {
@@ -96,15 +93,12 @@ export const MapDefinitions: MessageFns<MapDefinitions> = {
},
fromPartial<I extends Exact<DeepPartial<MapDefinitions>, I>>(object: I): MapDefinitions {
const message = createBaseMapDefinitions();
message.maps = (globalThis.Object.entries(object.maps ?? {}) as [string, MapDefinition][]).reduce(
(acc: { [key: number]: MapDefinition }, [key, value]: [string, MapDefinition]) => {
if (value !== undefined) {
acc[globalThis.Number(key)] = MapDefinition.fromPartial(value);
}
return acc;
},
{},
);
message.maps = Object.entries(object.maps ?? {}).reduce<{ [key: number]: MapDefinition }>((acc, [key, value]) => {
if (value !== undefined) {
acc[globalThis.Number(key)] = MapDefinition.fromPartial(value);
}
return acc;
}, {});
return message;
},
};
Loaded 100 of 915 files, more files were not shown because too many files have changed in this diff. Show more