diff --git a/packages/core/src/blitz/fetchBlitz.ts b/packages/core/src/blitz/fetchBlitz.ts deleted file mode 100644 index a285a18a2..000000000 --- a/packages/core/src/blitz/fetchBlitz.ts +++ /dev/null @@ -1,89 +0,0 @@ -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 = - | { - 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; - -export function fetchBlitz( - region: Region, - path: string, - params: FetchBlitzParams = {} -) { - return new Promise((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(); - }); -} diff --git a/packages/core/src/blitz/fetchGlossary.ts b/packages/core/src/blitz/fetchGlossary.ts deleted file mode 100644 index a6a92a333..000000000 --- a/packages/core/src/blitz/fetchGlossary.ts +++ /dev/null @@ -1,11 +0,0 @@ -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; -} diff --git a/packages/core/src/blitz/getAccountAchievements.ts b/packages/core/src/blitz/getAccountAchievements.ts deleted file mode 100644 index 284838ed5..000000000 --- a/packages/core/src/blitz/getAccountAchievements.ts +++ /dev/null @@ -1,35 +0,0 @@ -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( - 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; - } -} diff --git a/packages/core/src/blitz/getAccountInfo.ts b/packages/core/src/blitz/getAccountInfo.ts deleted file mode 100644 index 31836ee75..000000000 --- a/packages/core/src/blitz/getAccountInfo.ts +++ /dev/null @@ -1,96 +0,0 @@ -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(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; - } -} diff --git a/packages/core/src/blitz/getClanAccountInfo.ts b/packages/core/src/blitz/getClanAccountInfo.ts deleted file mode 100644 index 6bf90034b..000000000 --- a/packages/core/src/blitz/getClanAccountInfo.ts +++ /dev/null @@ -1,46 +0,0 @@ -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( - 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; - } -} diff --git a/packages/core/src/blitz/getClanInfo.ts b/packages/core/src/blitz/getClanInfo.ts deleted file mode 100644 index b9b4fe7f6..000000000 --- a/packages/core/src/blitz/getClanInfo.ts +++ /dev/null @@ -1,51 +0,0 @@ -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(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; - } -} diff --git a/packages/core/src/blitz/getTankStats.ts b/packages/core/src/blitz/getTankStats.ts deleted file mode 100644 index 111aacf1d..000000000 --- a/packages/core/src/blitz/getTankStats.ts +++ /dev/null @@ -1,16 +0,0 @@ -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(region, "tanks/stats", { - account_id: id, - ...params, - }); - - return tankStats[id]; -} diff --git a/packages/core/src/blitz/index.ts b/packages/core/src/blitz/index.ts index 6b8193288..5dc253fec 100644 --- a/packages/core/src/blitz/index.ts +++ b/packages/core/src/blitz/index.ts @@ -3,21 +3,13 @@ export * from "./clans"; export * from "./classes"; export * from "./constants"; export * from "./equipmentPrice"; -export * from "./fetchBlitz"; -export * from "./fetchGlossary"; export * from "./generateStats"; -export * from "./getAccountAchievements"; -export * from "./getAccountInfo"; -export * from "./getClanAccountInfo"; -export * from "./getClanInfo"; export * from "./getLeagueFromScore"; export * from "./getRatingInfo"; export * from "./getRatingLeague"; export * from "./getRatingNeighbors"; -export * from "./getTankStats"; export * from "./idToRegion"; export * from "./isOnGoingRatingSeason"; -export * from "./isValidBlitzId"; export * from "./nations"; export * from "./normalizeExtra"; export * from "./normalizeIds"; @@ -27,7 +19,5 @@ export * from "./readDVPL"; export * from "./regions"; export * from "./regionSubdomainToRegion"; export * from "./regionToRegionSubdomain"; -export * from "./searchClansAcrossRegions"; export * from "./searchCurrentRatingPlayers"; -export * from "./searchPlayersAcrossRegions"; export * from "./toUniqueId"; diff --git a/packages/core/src/blitz/isValidBlitzId.ts b/packages/core/src/blitz/isValidBlitzId.ts deleted file mode 100644 index a1750c147..000000000 --- a/packages/core/src/blitz/isValidBlitzId.ts +++ /dev/null @@ -1,32 +0,0 @@ -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; - } - | { status: "error" } - > - ); - - return json.status === "ok" && json.data[id] !== null; -} diff --git a/packages/core/src/blitz/searchClansAcrossRegions.ts b/packages/core/src/blitz/searchClansAcrossRegions.ts deleted file mode 100644 index 7bc6acd11..000000000 --- a/packages/core/src/blitz/searchClansAcrossRegions.ts +++ /dev/null @@ -1,51 +0,0 @@ -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("com", "clans/list", { - search, - limit: normalizedLimit, - }).then((value) => - value - .map((account) => ({ - ...account, - region: "com" as Region, - })) - .splice(0, normalizedLimit) - ), - fetchBlitz("eu", "clans/list", { - search, - limit: normalizedLimit, - }).then((value) => - value - .map((account) => ({ - ...account, - region: "eu" as Region, - })) - .splice(0, normalizedLimit) - ), - fetchBlitz("asia", "clans/list", { - search, - limit: normalizedLimit, - }).then((value) => - value - .map((account) => ({ - ...account, - region: "asia" as Region, - })) - .splice(0, normalizedLimit) - ), - ]) - ) - .filter(Boolean) - .flat(); -} diff --git a/packages/core/src/blitz/searchPlayersAcrossRegions/constants.ts b/packages/core/src/blitz/searchPlayersAcrossRegions/constants.ts deleted file mode 100644 index 4a16941a4..000000000 --- a/packages/core/src/blitz/searchPlayersAcrossRegions/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const usernamePattern = /^[a-zA-Z0-9_]{3,24}$/; -export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/; diff --git a/packages/core/src/blitz/searchPlayersAcrossRegions/index.ts b/packages/core/src/blitz/searchPlayersAcrossRegions/index.ts deleted file mode 100644 index 4dd909e64..000000000 --- a/packages/core/src/blitz/searchPlayersAcrossRegions/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -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("com", "account/list", { - search, - limit: normalizedLimit, - }).then( - (value) => - value && - value.map((account) => ({ - ...account, - region: "com" as Region, - })) - ), - fetchBlitz("eu", "account/list", { - search, - limit: normalizedLimit, - }).then( - (value) => - value && - value.map((account) => ({ - ...account, - region: "eu" as Region, - })) - ), - fetchBlitz("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"; diff --git a/packages/core/src/blitzkit/asset.ts b/packages/core/src/blitzkit/asset.ts deleted file mode 100644 index adfd8d5db..000000000 --- a/packages/core/src/blitzkit/asset.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { assertSecret } from "./assertSecret"; - -export function asset(path: string) { - return `${assertSecret(import.meta.env.PUBLIC_ASSET_BASE)}/${path}`; -} diff --git a/packages/core/src/blitzkit/averageDefinitions/constants.ts b/packages/core/src/blitzkit/averageDefinitions/constants.ts deleted file mode 100644 index 79c732fd6..000000000 --- a/packages/core/src/blitzkit/averageDefinitions/constants.ts +++ /dev/null @@ -1,22 +0,0 @@ -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, -}; diff --git a/packages/core/src/blitzkit/averageDefinitions/index.ts b/packages/core/src/blitzkit/averageDefinitions/index.ts deleted file mode 100644 index 61ddb2c57..000000000 --- a/packages/core/src/blitzkit/averageDefinitions/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { fetchPB } from '../../protobuf'; -import { AverageDefinitions } from '../../protos'; -import { asset } from '../asset'; - -export interface AverageDefinitionsManifest { - version: 1; - /** - * epoch in days - */ - latest: number; -} - -export async function fetchAverageDefinitions() { - const manifestResponse = await fetch(asset('averages/manifest.json')); - const manifestJson = - (await manifestResponse.json()) as AverageDefinitionsManifest; - return await fetchPB( - asset(`averages/${manifestJson.latest}.pb`), - AverageDefinitions, - ); -} - -export * from './constants'; diff --git a/packages/core/src/blitzkit/camouflageDefinitions.ts b/packages/core/src/blitzkit/camouflageDefinitions.ts deleted file mode 100644 index 20315860c..000000000 --- a/packages/core/src/blitzkit/camouflageDefinitions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { fetchPB } from '../protobuf'; -import { CamouflageDefinitions } from '../protos'; -import { asset } from './asset'; - -export function fetchCamouflageDefinitions() { - return fetchPB(asset('definitions/camouflages.pb'), CamouflageDefinitions); -} diff --git a/packages/core/src/blitzkit/consumableDefinitions.ts b/packages/core/src/blitzkit/consumableDefinitions.ts deleted file mode 100644 index 077e2e3b3..000000000 --- a/packages/core/src/blitzkit/consumableDefinitions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { fetchPB } from '../protobuf'; -import { ConsumableDefinitions } from '../protos'; -import { asset } from './asset'; - -export function fetchConsumableDefinitions() { - return fetchPB(asset('definitions/consumables.pb'), ConsumableDefinitions); -} diff --git a/packages/core/src/blitzkit/discoveredIdDefinitions.ts b/packages/core/src/blitzkit/discoveredIdDefinitions.ts deleted file mode 100644 index c6830554c..000000000 --- a/packages/core/src/blitzkit/discoveredIdDefinitions.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { asset } from './asset'; - -export interface DiscoveredIdsDefinitions { - time: number; - chunks: number; - count: number; -} - -export async function fetchDiscoveredIdsDefinitions() { - const response = await fetch(asset('ids/manifest.json')); - return response.json() as Promise; -} diff --git a/packages/core/src/blitzkit/equipmentDefinitions.ts b/packages/core/src/blitzkit/equipmentDefinitions.ts deleted file mode 100644 index a42f5bced..000000000 --- a/packages/core/src/blitzkit/equipmentDefinitions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { fetchPB } from '../protobuf'; -import { EquipmentDefinitions } from '../protos'; -import { asset } from './asset'; - -export function fetchEquipmentDefinitions() { - return fetchPB(asset('definitions/equipment.pb'), EquipmentDefinitions); -} diff --git a/packages/core/src/blitzkit/gallery.ts b/packages/core/src/blitzkit/gallery.ts deleted file mode 100644 index 18ddc672b..000000000 --- a/packages/core/src/blitzkit/gallery.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { fetchPB } from '../protobuf'; -import { Gallery } from '../protos'; -import { asset } from './asset'; - -export function fetchGallery() { - return fetchPB(asset('definitions/gallery.pb'), Gallery); -} diff --git a/packages/core/src/blitzkit/gameDefinitions.ts b/packages/core/src/blitzkit/gameDefinitions.ts deleted file mode 100644 index d7573adb0..000000000 --- a/packages/core/src/blitzkit/gameDefinitions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { fetchPB } from '../protobuf'; -import { GameDefinitions } from '../protos'; -import { asset } from './asset'; - -export function fetchGameDefinitions() { - return fetchPB(asset('definitions/game.pb'), GameDefinitions); -} diff --git a/packages/core/src/blitzkit/getArchivedLatestSeasonNumber.ts b/packages/core/src/blitzkit/getArchivedLatestSeasonNumber.ts deleted file mode 100644 index 460a81f01..000000000 --- a/packages/core/src/blitzkit/getArchivedLatestSeasonNumber.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { getRatingInfo } from "../blitz/getRatingInfo"; -import { assertSecret } from "./assertSecret"; - -interface GitHubTrees { - tree: { path: string; url: string }[]; -} - -let cachedLatestArchivedSeasonNumber: number | null = null; - -export async function getArchivedLatestSeasonNumber() { - if (cachedLatestArchivedSeasonNumber !== null) { - return cachedLatestArchivedSeasonNumber; - } - - const ratingInfo = await getRatingInfo("com"); - - if (ratingInfo.detail) { - const regionsURL = ( - (await fetch( - `https://api.github.com/repos/${assertSecret( - import.meta.env.PUBLIC_ASSET_REPO - )}/git/trees/main` - ).then((response) => response.json())) as GitHubTrees - ).tree.find(({ path }) => path === "regions")!.url; - const comURL = ( - (await fetch(regionsURL).then((response) => - response.json() - )) as GitHubTrees - ).tree.find(({ path }) => path === "com")!.url; - const ratingURL = ( - (await fetch(comURL).then((response) => response.json())) as { - tree: [{ url: string }]; - } - ).tree[0].url; - const latestSeason = Math.max( - ...( - (await fetch(ratingURL).then((response) => response.json())) as { - tree: { path: string }[]; - } - ).tree.map(({ path }) => Number(path)) - ); - - cachedLatestArchivedSeasonNumber = latestSeason; - } else { - cachedLatestArchivedSeasonNumber = ratingInfo.current_season - 1; - } - - return cachedLatestArchivedSeasonNumber; -} diff --git a/packages/core/src/blitzkit/getArchivedRatingLeaderboard.ts b/packages/core/src/blitzkit/getArchivedRatingLeaderboard.ts deleted file mode 100644 index 7943199bb..000000000 --- a/packages/core/src/blitzkit/getArchivedRatingLeaderboard.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Region } from "../blitz/regions"; -import { RatingLeaderboard } from "../protos"; -import { asset } from "./asset"; - -const cache: Record> = { - asia: {}, - eu: {}, - com: {}, -}; - -export async function getArchivedRatingLeaderboard( - region: Region, - season: number -) { - if (!cache[region][season]) { - const info = await fetch(asset(`regions/${region}/rating/${season}.pb`)) - .then((response) => response.arrayBuffer()) - .then((buffer) => RatingLeaderboard.decode(new Uint8Array(buffer))); - cache[region][season] = info; - } - - return cache[region][season]; -} diff --git a/packages/core/src/blitzkit/index.ts b/packages/core/src/blitzkit/index.ts index 2c289c4ba..43e952b32 100644 --- a/packages/core/src/blitzkit/index.ts +++ b/packages/core/src/blitzkit/index.ts @@ -1,50 +1,22 @@ -export * from "./assertSecret"; -export * from "./asset"; export * from "./availableProvisions"; -export * from "./averageDefinitions"; -// export * from './camouflageDefinitions'; -// export * from './checkConsumableProvisionInclusivity'; export * from "./coefficient"; -export * from "./consumableDefinitions"; -// export * from './context'; export * from "./createDefaultSkills"; export * from "./degressiveStat"; -export * from "./discoveredIdDefinitions"; export * from "./emblemURL"; -export * from "./equipmentDefinitions"; export * from "./eventManager"; -export * from "./gameDefinitions"; -export * from "./getArchivedLatestSeasonNumber"; -// export * from './getArchivedRatingInfo'; -export * from "./getArchivedRatingLeaderboard"; -// export * from './getArchivedRatingMidnightLeaderboard'; export * from "./getTimeDaysAgo"; export * from "./isExplosive"; -export * from "./mapDefinitions"; -export * from "./modelDefinitions"; export * from "./normalizeBoundingBox"; -// export * from './parseBkni'; -// export * from './patientFetch'; export * from "./patientFetchJSON"; export * from "./progressiveStat"; -export * from "./provisionDefinitions"; export * from "./resolveDpm"; export * from "./resolvePenetrationCoefficient"; -export * from "./reviews"; -export * from "./skillDefinitions"; export * from "./sum"; -export * from "./tankDefinitions"; -export * from "./tankIcon"; -export * from "./tankNames"; export * from "./unionBoundingBox"; export * from "./uniqueGuns"; export * from "./unwrapBlitzkitResponse"; export * from "./votes"; -// export * from './withCORSProxy'; export * from "./createDefaultProvisions"; -export * from "./gallery"; export * from "./metaSortTank"; -export * from "./popularTanks"; export * from "./romanize"; -export * from "./sluggify.ts"; export * from "./youtubers"; diff --git a/packages/core/src/blitzkit/mapDefinitions.ts b/packages/core/src/blitzkit/mapDefinitions.ts deleted file mode 100644 index 50bf8525a..000000000 --- a/packages/core/src/blitzkit/mapDefinitions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { fetchPB } from '../protobuf'; -import { MapDefinitions } from '../protos'; -import { asset } from './asset'; - -export function fetchMapDefinitions() { - return fetchPB(asset('definitions/maps.pb'), MapDefinitions); -} diff --git a/packages/core/src/blitzkit/modelDefinitions.ts b/packages/core/src/blitzkit/modelDefinitions.ts deleted file mode 100644 index 59ca764e5..000000000 --- a/packages/core/src/blitzkit/modelDefinitions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { fetchPB } from '../protobuf'; -import { ModelDefinitions } from '../protos'; -import { asset } from './asset'; - -export function fetchModelDefinitions() { - return fetchPB(asset('definitions/models.pb'), ModelDefinitions); -} diff --git a/packages/core/src/blitzkit/popularTanks.ts b/packages/core/src/blitzkit/popularTanks.ts deleted file mode 100644 index 92d6e372b..000000000 --- a/packages/core/src/blitzkit/popularTanks.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { fetchPB } from '../protobuf'; -import { PopularTanks } from '../protos'; -import { asset } from './asset'; - -export function fetchPopularTanks() { - return fetchPB(asset('definitions/popular-tanks.pb'), PopularTanks); -} diff --git a/packages/core/src/blitzkit/provisionDefinitions.ts b/packages/core/src/blitzkit/provisionDefinitions.ts deleted file mode 100644 index 6c85d2d5b..000000000 --- a/packages/core/src/blitzkit/provisionDefinitions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { fetchPB } from '../protobuf'; -import { ProvisionDefinitions } from '../protos'; -import { asset } from './asset'; - -export function fetchProvisionDefinitions() { - return fetchPB(asset('definitions/provisions.pb'), ProvisionDefinitions); -} diff --git a/packages/core/src/blitzkit/reviews.ts b/packages/core/src/blitzkit/reviews.ts deleted file mode 100644 index 40922fca4..000000000 --- a/packages/core/src/blitzkit/reviews.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { fetchPB } from '../protobuf'; -import { Reviews } from '../protos'; -import { asset } from './asset'; - -export function fetchReviews() { - return fetchPB(asset('definitions/reviews.pb'), Reviews); -} diff --git a/packages/core/src/blitzkit/skillDefinitions.ts b/packages/core/src/blitzkit/skillDefinitions.ts deleted file mode 100644 index a2bebd8f0..000000000 --- a/packages/core/src/blitzkit/skillDefinitions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { fetchPB } from '../protobuf'; -import { SkillDefinitions } from '../protos'; -import { asset } from './asset'; - -export function fetchSkillDefinitions() { - return fetchPB(asset('definitions/skills.pb'), SkillDefinitions); -} diff --git a/packages/core/src/blitzkit/tankDefinitions/constants.ts b/packages/core/src/blitzkit/tankDefinitions/constants.ts deleted file mode 100644 index 5224cdae7..000000000 --- a/packages/core/src/blitzkit/tankDefinitions/constants.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { times } from "lodash-es"; -import { CrewType, ShellType, TankClass, TankType } from "../../protos"; - -export const SHELL_NAMES: Record = { - [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.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.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.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 = { - 1: "I", - 2: "II", - 3: "III", - 4: "IV", - 5: "V", - 6: "VI", - 7: "VII", - 8: "VIII", - 9: "IX", - 10: "X", -}; - -export const flags: Record = { - ussr: "<:ussr:1218421042033197197>", - germany: "πŸ‡©πŸ‡ͺ", - usa: "πŸ‡ΊπŸ‡Έ", - china: "πŸ‡¨πŸ‡³", - uk: "πŸ‡¬πŸ‡§", - france: "πŸ‡«πŸ‡·", - japan: "πŸ‡―πŸ‡΅", - european: "πŸ‡ͺπŸ‡Ί", - other: "<:other:1218421572243558482>", -}; -export const TREE_TYPE_ICONS: Record> = { - [TankType.TANK_TYPE_RESEARCHABLE]: TANK_ICONS, - [TankType.TANK_TYPE_PREMIUM]: TANK_ICONS_PREMIUM, - [TankType.TANK_TYPE_COLLECTOR]: TANK_ICONS_COLLECTOR, -}; diff --git a/packages/core/src/blitzkit/tankDefinitions/index.ts b/packages/core/src/blitzkit/tankDefinitions/index.ts deleted file mode 100644 index 18bdfcd2a..000000000 --- a/packages/core/src/blitzkit/tankDefinitions/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { fetchPB } from '../../protobuf'; -import { TankDefinitions } from '../../protos'; -import { asset } from '../asset'; - -export function fetchTankDefinitions() { - return fetchPB(asset('definitions/tanks.pb'), TankDefinitions); -} - -export * from './constants'; diff --git a/packages/core/src/blitzkit/tankIcon.ts b/packages/core/src/blitzkit/tankIcon.ts deleted file mode 100644 index 546ef5f8f..000000000 --- a/packages/core/src/blitzkit/tankIcon.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { asset } from "./asset"; - -export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 }; - -export function tankIcon(id: number, size: "big" | "small" = "big") { - return asset(`icons/tanks/${size}/${id}.webp`); -} diff --git a/packages/core/src/blitzkit/tankNames.ts b/packages/core/src/blitzkit/tankNames.ts deleted file mode 100644 index d778ed33e..000000000 --- a/packages/core/src/blitzkit/tankNames.ts +++ /dev/null @@ -1,52 +0,0 @@ -import locales from "@blitzkit/i18n/locales.json"; -import { deburr } from "lodash-es"; -import { I18nString } from "../protos"; -import { fetchCamouflageDefinitions } from "./camouflageDefinitions"; -import { fetchTankDefinitions } from "./tankDefinitions"; - -export async function fetchTankNames() { - const [tankDefinitions, camouflageDefinitions] = await Promise.all([ - fetchTankDefinitions(), - fetchCamouflageDefinitions(), - ]); - const tankDefinitionsArray = Object.values(tankDefinitions.tanks); - - return await Promise.all( - tankDefinitionsArray.map(async (tank) => { - const searchableNameDeburr: I18nString = { locales: {} }; - - Object.entries(tank.name!).forEach(([key, value]) => { - searchableNameDeburr.locales[key] = deburr(value); - }); - - return { - id: tank.id, - name: tank.name, - searchableName: tank.name, - searchableNameDeburr, - camouflages: tank.camouflages - ?.map((id) => - locales.supported.map( - ({ locale }) => - camouflageDefinitions.camouflages[id]?.name!.locales[locale], - ), - ) - .flat() - .filter(Boolean) - .map(deburr) - .join(" "), - treeType: tank.type, - }; - }), - ); -} - -export const SEARCH_KEYS = [ - ...locales.supported - .map(({ locale }) => [ - `searchableName.locales.${locale}`, - `searchableNameDeburr.locales.${locale}`, - ]) - .flat(), - "camouflages", -]; diff --git a/packages/core/src/patreon/patreonLoginUrl.ts b/packages/core/src/patreon/patreonLoginUrl.ts index 104b8537f..8634e6195 100644 --- a/packages/core/src/patreon/patreonLoginUrl.ts +++ b/packages/core/src/patreon/patreonLoginUrl.ts @@ -1,11 +1,11 @@ -import { assertSecret } from '../blitzkit/assertSecret'; +import { secret } from "../secrets"; export function patreonLoginUrl() { const params = new URLSearchParams({ - 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', + response_type: "code", + client_id: secret("PUBLIC_PATREON_CLIENT_ID"), + redirect_uri: secret("PUBLIC_PATREON_REDIRECT_URI"), + scope: "identity", }); return `https://www.patreon.com/oauth2/authorize?${params}`; diff --git a/packages/core/src/secrets/secret.ts b/packages/core/src/secrets/secret.ts index b4fb3b637..c73060778 100644 --- a/packages/core/src/secrets/secret.ts +++ b/packages/core/src/secrets/secret.ts @@ -6,7 +6,7 @@ export enum SecretType { Number, } -export type PassableSecretName = WellKnownSecret | (string & {}); +export type PassableSecretName = WellKnownSecret; export function secret( name: PassableSecretName, @@ -18,10 +18,7 @@ export function secret( type: SecretType.Number, ): number; -export function secret( - name: WellKnownSecret | (string & {}), - type = SecretType.String, -) { +export function secret(name: PassableSecretName, type = SecretType.String) { if (!hasSecret(name)) { throw new Error(`Secret "${name}" not defined`); } @@ -30,7 +27,7 @@ export function secret( switch (type) { case SecretType.String: - return value; + return String(value); case SecretType.Number: return Number(value); diff --git a/packages/core/src/secrets/wellKnown.ts b/packages/core/src/secrets/wellKnown.ts index 2f6e77f70..a725debfb 100644 --- a/packages/core/src/secrets/wellKnown.ts +++ b/packages/core/src/secrets/wellKnown.ts @@ -1,10 +1,17 @@ export type WellKnownSecret = + // development set-up + | "TEMP" + // website | "BUILD_CHUNK" | "TEXTURE_CHUNKS" - | "TEMP" + // wotb | "WOTB_SERVER" | "WOTB_VERSION" | "PUBLIC_WOTB_CLIENT" | "WOTB_USMAP" + // google | "GOOGLE_APPLICATION_CREDENTIALS" - | "PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID"; + | "PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID" + // patreon + | "PUBLIC_PATREON_CLIENT_ID" + | "PUBLIC_PATREON_REDIRECT_URI"; diff --git a/packages/core/src/statistics/getTierWeights.ts b/packages/core/src/statistics/getTierWeights.ts deleted file mode 100644 index 6144b078f..000000000 --- a/packages/core/src/statistics/getTierWeights.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { fetchTankDefinitions } from '../blitzkit'; -import type { BlitzStats } from './compositeStats/constants'; - -export async function getTierWeights( - stats: Record, - order: number[] = Object.keys(stats).map(parseInt), -) { - const tankDefinitions = await fetchTankDefinitions(); - const tierWeights: TierWeightsRecord = {}; - - order.forEach((id) => { - const entry = tankDefinitions.tanks[id]; - - if (!entry) return; - if (!tierWeights[entry.tier]) tierWeights[entry.tier] = 0; - - tierWeights[entry.tier]! += stats[id].battles; - }); - - return tierWeights; -} -export type TierWeightsRecord = Partial>; diff --git a/packages/core/src/statistics/index.ts b/packages/core/src/statistics/index.ts index 81288f813..adad33644 100644 --- a/packages/core/src/statistics/index.ts +++ b/packages/core/src/statistics/index.ts @@ -1,9 +1,8 @@ -export * from './calculateWN8'; -export * from './compositeStats'; -export * from './deltaBkrlBlitzStats'; -export * from './deltaTankStats'; -export * from './getBkniPercentile'; -export * from './getTierWeights'; -export * from './getWN8Percentile'; -export * from './percentiles'; -export * from './sumAllStats'; +export * from "./calculateWN8"; +export * from "./compositeStats"; +export * from "./deltaBkrlBlitzStats"; +export * from "./deltaTankStats"; +export * from "./getBkniPercentile"; +export * from "./getWN8Percentile"; +export * from "./percentiles"; +export * from "./sumAllStats"; diff --git a/packages/scripts/src/applyPatches.ts b/packages/scripts/src/applyPatches.ts deleted file mode 100644 index 7064ce932..000000000 --- a/packages/scripts/src/applyPatches.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { dvp } from "@blitzkit/closed"; -import { assertSecret } from "@blitzkit/core"; -import { mkdir, writeFile } from "fs/promises"; -import { parse as parsePath } from "path"; -import ProgressBar from "progress"; -import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; -import { vfs } from "./buildAssets/constants"; -import { writeDVPL } from "./core/blitz/writeDVPL"; - -const versionTextFile = await vfs.text(`Data/version.txt`); -const currentVersion = versionTextFile - .split(" ")[0] - .split(".") - .slice(0, 3) - .join("."); - -console.log(`Installing patches for ${currentVersion}...`); - -let patchIndex = 1; -while (true) { - const response = await fetch( - `${assertSecret( - import.meta.env.WOTB_DLC_CDN - )}/dlc/s${currentVersion}_${patchIndex}.yaml` - ); - - if (response.status === 200) { - console.log(`Applying patch ${patchIndex}...`); - - const data = parseYaml(await response.text()); - const dvpm = await fetch( - `${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11}` - ).then((response) => response.arrayBuffer()); - const dvpd = await fetch( - `${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11.replace( - ".dvpm", - ".dvpd" - )}` - ).then((response) => response.arrayBuffer()); - const files = await dvp(dvpm, dvpd); - const bar = new ProgressBar( - `Patching ${files.length} files :bar`, - files.length - ); - - for (const { path, data } of files) { - console.log(`Patching "${path}"...`); - - const { dir } = parsePath(path); - - try { - await mkdir(`Data/${dir}`, { recursive: true }); - } catch (error) { - console.warn(`Failed to make directory "${dir}"`); - } - - const isDvpl = path.endsWith(".dvpl"); - const buffer = Buffer.from(data); - - await writeFile( - `Data/${path}${isDvpl ? "" : ".dvpl"}`, - new Uint8Array(isDvpl ? buffer : writeDVPL(buffer)) - ); - - bar.tick(); - } - - if ("dynamicContentLocalizationsDir" in data) { - console.log("Found dynamic content localizations; patching..."); - - const localizationsResponse = await fetch( - `${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${ - data.dynamicContentLocalizationsDir - }/en.yaml` - ); - const newStrings = parseYaml(await localizationsResponse.text()); - const oldStrings = await vfs.yaml>( - `Data/Strings/en.yaml` - ); - const patchedStrings = { ...oldStrings, ...newStrings }; - const patchedContent = stringifyYaml(patchedStrings); - - await writeFile( - `Data/Strings/en.yaml`, - new Uint8Array(writeDVPL(Buffer.from(patchedContent))) - ); - } - - patchIndex++; - } else break; -} diff --git a/packages/scripts/src/buildAssets/boosterIcons.ts b/packages/scripts/src/buildAssets/boosterIcons.ts deleted file mode 100644 index e2cda995c..000000000 --- a/packages/scripts/src/buildAssets/boosterIcons.ts +++ /dev/null @@ -1,46 +0,0 @@ -import sharp from "sharp"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; - -export async function boosterIcons() { - console.log("Building booster icons..."); - - const uploader = new AssetUploader("booster icons"); - const boosterFiles = await vfs - .dir("Data/Gfx/Shared/boosters") - .then((files) => - files.filter( - (file) => !file.includes("@2x.txt") && !file.startsWith("texture0"), - ), - ); - const image = sharp( - await vfs.file(`Data/Gfx/Shared/boosters/texture0.packed.webp`), - ); - - for (const file of boosterFiles) { - const name = file.match(/booster_(.+).txt/)?.[1]; - - if (name === undefined) continue; - - const sizes = (await vfs.text(`Data/Gfx/Shared/boosters/${file}`)) - .split("\n")[4] - .split(" ") - .map(Number); - const content = await image - .clone() - .extract({ - left: sizes[0], - top: sizes[1], - width: sizes[2], - height: sizes[3], - }) - .toBuffer(); - - await uploader.add({ - path: `icons/boosters/${name}.webp`, - content, - }); - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/camouflageIcons.ts b/packages/scripts/src/buildAssets/camouflageIcons.ts deleted file mode 100644 index 897165169..000000000 --- a/packages/scripts/src/buildAssets/camouflageIcons.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; - -export async function camouflageIcons() { - console.log("Building camouflage icons..."); - - using uploader = new AssetUploader("camo icons"); - const content = await vfs.file( - `Data/Gfx/UI/Hangar/IconCamouflage.packed.webp`, - ); - - await uploader.add({ content, path: "icons/camo.webp" }); - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/constants.ts b/packages/scripts/src/buildAssets/constants.ts deleted file mode 100644 index 7516210da..000000000 --- a/packages/scripts/src/buildAssets/constants.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { assertSecret } from "@blitzkit/core"; -import { AbstractVFS } from "../core/vfs/abstract"; -import { LocalVFS } from "../core/vfs/local"; -import { SteamVFS } from "../core/vfs/steam"; -import { ZipVFS } from "../core/vfs/zip"; - -const steamPattern = /steam:(\d+)\/(\d+)/; -const zipPattern = /zip:(.+)/; -const localePattern = /local:"?(.+)"?/; - -const source = Bun.argv - .find((arg) => arg.startsWith("--source=")) - ?.split("=")[1]; - -let resolved: AbstractVFS; - -if (!source) { - throw new Error("No source specified"); -} else if (steamPattern.test(source)) { - const [, app, depot] = source.match(steamPattern)!.map(Number); - - resolved = await new SteamVFS( - assertSecret(import.meta.env.STEAM_USERNAME), - assertSecret(import.meta.env.STEAM_PASSWORD), - app, - depot, - ).init(); -} else if (zipPattern.test(source)) { - const [, url] = source.match(zipPattern)!; - resolved = await new ZipVFS(url).init(); -} else if (localePattern.test(source)) { - const [, path] = source.match(localePattern)!; - resolved = await new LocalVFS(path).init(); -} else { - throw new Error("No valid source specified"); -} - -export const vfs = resolved; diff --git a/packages/scripts/src/buildAssets/consumableProvisionIcons.ts b/packages/scripts/src/buildAssets/consumableProvisionIcons.ts deleted file mode 100644 index a9031bb24..000000000 --- a/packages/scripts/src/buildAssets/consumableProvisionIcons.ts +++ /dev/null @@ -1,225 +0,0 @@ -import sharp from "sharp"; -import { AssetUploader } from "../core/github/assetUploader"; -import { parsePackedSpriteRect } from "../core/blitz/parsePackedSpriteRect"; -import { vfs } from "./constants"; -import { ConsumablesCommon, ProvisionsCommon } from "./definitions"; - -interface Mappings { - Header: { version: number }; - StyleSheets: { - selectors: string[]; - properties: { "bg-sprite": string }; - }[]; -} - -const listItemsPattern = //g; - -async function extractPackedIcon( - texture: sharp.Sharp, - sizes: number[], - context: string, -) { - const [left, top, width, height] = sizes; - const bounds = [left, top, width, height]; - - if (!bounds.every(Number.isInteger)) { - throw new Error( - `Invalid RIFF sprite bounds for ${context}: ${bounds.join(" ")}`, - ); - } - - const metadata = await texture.metadata(); - - if (metadata.width === undefined || metadata.height === undefined) { - throw new Error(`Failed to read image dimensions for ${context}`); - } - - if ( - left < 0 || - top < 0 || - width <= 0 || - height <= 0 || - left + width > metadata.width || - top + height > metadata.height - ) { - throw new Error( - `Out-of-bounds RIFF sprite for ${context}: ${bounds.join(" ")} in ${metadata.width}x${metadata.height}`, - ); - } - - return await texture.clone().extract({ left, top, width, height }).toBuffer(); -} - -export async function consumableProvisionIcons() { - console.log("Building consumable and provision icons..."); - - using uploader = new AssetUploader("consumable and provision icons"); - const styles = [ - "UI/Styles/Lobby/Inventory/InventoryNormalStyles.yaml", - "UI/Styles/Lobby/Inventory/Event/InventoryBigStyles.yaml", - "UI/Styles/Lobby/Inventory/Event/InventoryNormalStyles.yaml", - "UI/Screens3/Lobby/Inventory/Equipment/EquipmentItemImage.style.yaml", - "UI/Screens/Battle/Styles/BattleEquipmentStyles.yaml", - ]; - const styleSheets = await Promise.all( - styles.map(async (path) => - Object.values((await vfs.yaml(`Data/${path}`)).StyleSheets), - ), - ).then((array) => array.flat()); - - for (const match of ( - await vfs.text(`Data/XML/item_defs/vehicles/common/consumables/list.xml`) - ).matchAll(listItemsPattern)) { - const consumablesCommon = await vfs.xml<{ - root: ConsumablesCommon; - }>(`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`); - - for (const consumable of Object.values(consumablesCommon.root)) { - const styleSheet = styleSheets.find((styleSheet) => - styleSheet.selectors.some((selector) => - selector.includes(`${consumable.icon} `), - ), - ); - - if (!styleSheet) { - console.warn( - `No style sheet found for consumable ${consumable.icon}; skipping...`, - ); - continue; - } - - const configPath = styleSheet.properties["bg-sprite"] - .replace("~res:/", "") - .replace(".psd", "") - .replace(".txt", ""); - - if (await vfs.resolve(`Data/${configPath}.packed.webp`)) { - const packedBuffer = await vfs.file(`Data/${configPath}.packed.webp`); - const consumablesTexture = sharp(packedBuffer); - const sizes = parsePackedSpriteRect(packedBuffer); - let content: Buffer; - - if (sizes) { - content = await extractPackedIcon( - consumablesTexture, - sizes, - `consumable ${consumable.icon}`, - ); - } else { - content = await consumablesTexture.trim({ threshold: 80 }).toBuffer(); - } - - await uploader.add({ - path: `icons/consumables/${consumable.id}.webp`, - content, - }); - } else { - const consumablesTexture = sharp( - await vfs.file( - `${`Data/${configPath}` - .split("/") - .slice(0, -1) - .join("/")}/texture0.packed.webp`, - ), - ); - const sizes = (await vfs.text(`Data/${configPath}.txt`)) - .split("\n")[4] - .split(" ") - .map(Number); - const content = await consumablesTexture - .clone() - .extract({ - left: sizes[0], - top: sizes[1], - width: sizes[2], - height: sizes[3], - }) - .toBuffer(); - - await uploader.add({ - path: `icons/consumables/${consumable.id}.webp`, - content, - }); - } - } - } - - for (const match of ( - await vfs.text(`Data/XML/item_defs/vehicles/common/provisions/list.xml`) - ).matchAll(listItemsPattern)) { - const provisionsCommon = await vfs.xml<{ - root: ProvisionsCommon; - }>(`Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`); - - for (const provision of Object.values(provisionsCommon.root)) { - const styleSheet = styleSheets.find((styleSheet) => - styleSheet.selectors.some((selector) => - selector.includes(`${provision.icon} `), - ), - ); - - if (!styleSheet) { - console.warn( - `No style sheet found for provision ${provision.icon}; skipping...`, - ); - continue; - } - - const configPath = styleSheet.properties["bg-sprite"] - .replace("~res:/", "") - .replace(".psd", "") - .replace(".txt", ""); - - if (await vfs.resolve(`Data/${configPath}.packed.webp`)) { - const packedBuffer = await vfs.file(`Data/${configPath}.packed.webp`); - const provisionsTexture = sharp(packedBuffer); - const sizes = parsePackedSpriteRect(packedBuffer); - let content: Buffer; - - if (sizes) { - content = await extractPackedIcon( - provisionsTexture, - sizes, - `provision ${provision.icon}`, - ); - } else { - content = await provisionsTexture.trim({ threshold: 80 }).toBuffer(); - } - - await uploader.add({ - path: `icons/provisions/${provision.id}.webp`, - content, - }); - } else { - const provisionsTexture = sharp( - await vfs.file( - `${`Data/${configPath}` - .split("/") - .slice(0, -1) - .join("/")}/texture0.packed.webp`, - ), - ); - const sizes = (await vfs.text(`Data/${configPath}.txt`)) - .split("\n")[4] - .split(" ") - .map(Number); - const content = await provisionsTexture - .clone() - .extract({ - left: sizes[0], - top: sizes[1], - width: sizes[2], - height: sizes[3], - }) - .toBuffer(); - - await uploader.add({ - path: `icons/provisions/${provision.id}.webp`, - content, - }); - } - } - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/currencies.ts b/packages/scripts/src/buildAssets/currencies.ts deleted file mode 100644 index 78d10ede1..000000000 --- a/packages/scripts/src/buildAssets/currencies.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { assertSecret, BlitzGlossary } from "@blitzkit/core"; -import sharp from "sharp"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; - -const ICONS = [ - ["currency_silver_m.packed.webp", "silver"], - ["currency_premium_xl.packed.webp", "premium"], - ["currency_gold_m.packed.webp", "gold"], - ["currency_free-xp_xl.packed.webp", "free-xp"], - ["currency_elite-xp_xl.packed.webp", "elite-xp"], - ["currency_crew-xp_xl.packed.webp", "crew-xp"], - ["currency_battle-xp_xl.packed.webp", "xp"], -]; - -export async function currencies() { - console.log("Building currency icons..."); - - using uploader = new AssetUploader("currency icons"); - - for (const [file, name] of ICONS) { - const content = await sharp( - await vfs.file(`Data/Gfx/Lobby/currency/${file}`), - ) - .trim() - .toBuffer(); - - await uploader.add({ - content, - path: `icons/currencies/${name}.webp`, - }); - } - - const glossary = await fetch(assertSecret(import.meta.env.WOTB_GLOSSARY)) - .then((response) => response.json() as Promise) - .then((glossary) => - Object.values(glossary) - .entries() - .filter(([key]) => /^prx_season_\d+$/.test(`${key}`)), - ); - - for (const [key, item] of glossary) { - if (item.image_url === null) throw new Error(`No image_url for ${key}`); - - const imageRaw = await fetch(item.image_url).then((response) => - response.arrayBuffer(), - ); - const content = await sharp(imageRaw) - .trim({ - threshold: 100, - background: { r: 0, g: 0, b: 0, alpha: 0 }, - }) - .toBuffer(); - - await uploader.add({ - content, - path: `icons/currencies/${key}.webp`, - }); - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/definitions.ts b/packages/scripts/src/buildAssets/definitions.ts deleted file mode 100644 index 467a99b85..000000000 --- a/packages/scripts/src/buildAssets/definitions.ts +++ /dev/null @@ -1,1848 +0,0 @@ -import { - Armor, - AssaultRanges, - type BlitzTankClass, - CamouflageDefinitions, - Consumable, - ConsumableDefinitions, - ConsumableTankCategoryFilterCategory, - Crew, - CrewType, - Equalizer, - EquipmentDefinitions, - EquipmentSlot, - GameDefinitions, - GunDefinition, - I18nString, - MapDefinitions, - ModelDefinitions, - ModuleType, - Provision, - ProvisionDefinitions, - ResearchCost, - ShellType, - SkillDefinitions, - TankClass, - TankDefinitions, - TankPrice, - TankPriceType, - TankType, - toUniqueId, - Unlock, - Vector3, -} from "@blitzkit/core"; -import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n"; -import locales from "@blitzkit/i18n/locales.json"; -import { readFile } from "fs/promises"; -import { deburr } from "lodash-es"; -import { parse as parsePath } from "path"; -import type { Vector3Tuple } from "three"; -import { parse as parseYaml } from "yaml"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; -import type { Avatar } from "./skillIcons"; -import type { TankParameters } from "./tankIcons"; - -const nonAlphanumericRegex = /[^a-z0-9]/g; -const multipleDashesRegex = /--+/g; -const trailingDashRegex = /-$/g; -const leadingDashRegex = /^-/g; - -const nationSlugDiscriminators = { - china: "cn", - european: "eu", - france: "fr", - germany: "de", - japan: "jp", - other: "ot", - uk: "gb", - usa: "us", - ussr: "ru", -}; - -function 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; - } -} - -type BlitzTankFilterDefinitionCategory = "clip"; -const blitzTankFilterDefinitionCategoryToBlitzkit: Record< - BlitzTankFilterDefinitionCategory, - ConsumableTankCategoryFilterCategory -> = { - clip: ConsumableTankCategoryFilterCategory.CONSUMABLE_TANK_CATEGORY_FILTER_CATEGORY_CLIP, -}; -function vector3TupleToBlitzkit(tuple: Vector3Tuple) { - return { x: tuple[0], y: tuple[1], z: tuple[2] } satisfies Vector3; -} -const blitzTankClassToBlitzkit: Record = { - lightTank: TankClass.TANK_CLASS_LIGHT, - "AT-SPG": TankClass.TANK_CLASS_TANK_DESTROYER, - heavyTank: TankClass.TANK_CLASS_HEAVY, - mediumTank: TankClass.TANK_CLASS_MEDIUM, -}; -export interface BlitzStrings { - [key: string]: string; -} -type ShellKind = - | "ARMOR_PIERCING" - | "ARMOR_PIERCING_CR" - | "HIGH_EXPLOSIVE" - | "HOLLOW_CHARGE"; -type ShellDefinitionsList = Record< - string, - { - id: number; - userString: string; - icon: string; - kind: ShellKind; - caliber: number; - damage: { armor: number; devices: number }; - normalizationAngle?: number; - ricochetAngle?: number; - explosionRadius?: number; - } -> & { - icons: Record; -}; -interface EngineDefinitionsList { - nextAvailableId: number; - ids: Record; - shared: { - [key: string]: { - userString: string; - level: number; - fireStartingChance: number; - weight: number; - power: number; - }; - }; -} -export interface ChassisDefinitionsList { - nextAvailableId: number; - ids: Record; -} -type VehicleDefinitionArmor = Record< - string, - number | { vehicleDamageFactor: 0; "#text": number } | number[] ->; -interface UnlocksInner { - cost: number | string; - "#text": number; -} -type BlitzModuleType = { - [key in "vehicle" | "engine" | "chassis" | "turret" | "gun"]: - | UnlocksInner - | UnlocksInner[]; -}; -type UnlocksListing = BlitzModuleType | undefined; -type BlitzCrewType = "commander" | "radioman" | "gunner" | "driver" | "loader"; -const blitzCrewTypeToBlitzkit: Record = { - 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, -}; -const blitzkitCrewTypeToBlitz: Record = { - [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", -}; -interface VehicleDefinitions { - invisibility: { - moving: number; - still: number; - firePenalty: number; - }; - crew: Record; - speedLimits: { - forward: number; - backward: number; - }; - consumableSlots: number; - provisionSlots: number; - optDevicePreset: string | string[]; - hull: { - armor: VehicleDefinitionArmor; - turretPositions: { turret: string }; - turretInitialRotation?: { yaw: 0; pitch: 6.5; roll: 0 }; - maxHealth: number; - weight: number; - }; - chassis: { - [key: string]: { - unlocks: UnlocksListing; - weight: number; - terrainResistance: string; - rotationSpeed: number; - userString: string; - shotDispersionFactors: { - vehicleMovement: number; - vehicleRotation: number; - }; - level: number; - hullPosition: string; - armor: { - leftTrack: - | number - | { - chanceToHitByProjectile: number; - chanceToHitByExplosion: number; - "#text": number; - }; - rightTrack: - | number - | { - chanceToHitByProjectile: number; - chanceToHitByExplosion: number; - "#text": number; - }; - }; - }; - }; - engines: { - [key: string]: { - unlocks: UnlocksListing; - }; - }; - turrets0: { - [key: string]: { - unlocks: UnlocksListing; - rotationSpeed: number; - weight: number; - circularVisionRadius: number; - maxHealth: number; - armor: VehicleDefinitionArmor; - userString: string; - level: number; - yawLimits: string | string[]; - gunPosition: string | string[]; - models: { undamaged: string }; - hitTester: { - collisionModel: string; - }; - guns: { - [key: string]: { - unlocks: UnlocksListing; - armor: VehicleDefinitionArmor & { - gun?: - | number - | { - chanceToHitByProjectile: number; - chanceToHitByExplosion: number; - "#text": number; - }; - }; - invisibilityFactorAtShot: number | number[]; - reloadTime: number; - aimingTime?: number; - shotDispersionRadius: number; - shotDispersionFactors?: { - turretRotation: number; - afterShot: number; - whileGunDamaged: number; - }; - maxAmmo: number; - extraPitchLimits?: { - front?: string; - back?: string; - transition?: number | number[]; - }; - pitchLimits?: string | string[]; - pumpGunMode?: boolean; - pumpGunReloadTimes?: string; - clip?: { count: number; rate: number }; - burst?: { count: number; rate: number }; - models: { undamaged: string }; - extras?: { - trayShell?: { - kinds: string; - sectors: Record< - string, - { id: number; distance: number; factor: number }[] - >; - }; - }; - }; - }; - }; - }; - extras?: { - armorsStatesController?: { - state: [ - { - id: 0; - armors: string; - type: "default"; - enabled: true; - factorsModifiers: ""; - default: ""; - }, - { - id: 1; - armors: string; - type: "default"; - enabled: false; - factorsModifiers: ""; - }, - ]; - }; - }; -} -export interface VehicleDefinitionList { - [key: string]: { - id: number; - userString: string; - shortUserString?: string; - description: string; - price: number | { gold: ""; "#text": number }; - sellPrice?: { gold: ""; "#text": number }; - enrichmentPermanentCost: number; - notInShop?: boolean; - tags: string; - level: number; - combatRole?: Record; - configurationModes: string; - }; -} -interface TurretDefinitionsList { - nextAvailableId: number; - ids: Record; -} -interface GunDefinitionsList { - nextAvailableId: number; - ids: Record; - shared: { - [key: string]: { - rotationSpeed: number; - weight: number; - shotDispersionFactors: { - turretRotation: number; - afterShot: number; - whileGunDamaged: number; - }; - maxAmmo: number; - aimingTime: number; - shotDispersionRadius: number; - userString: string; - tags: string; - level: number; - pitchLimits: string; - burst?: { count: number; rate: number }; - clip?: { count: number; rate: number }; - shots: { - [key: string]: { - speed: number; - piercingPower: string; - maxDistance: number; - }; - }; - }; - }; -} - -export interface OptionalDevices { - [key: string]: { - id: number; - userString: string; - description: string; - icon: string; - script: unknown; - display_params: unknown; - }; -} - -interface OptionalDeviceSlots { - presets: { - [key: string]: { - level0: OptionalDeviceSlotRow; - level1: OptionalDeviceSlotRow; - level2: OptionalDeviceSlotRow; - }; - }; -} - -interface OptionalDeviceSlotRow { - [key: string]: { device0: string; device1: string }; -} -export interface ConsumablesCommon { - [key: string]: { - id: number; - userString: string; - description: string; - icon: string; - category: string; - tags: string; - vehicleFilter?: { - include: { vehicle: ConsumablesVehicleFilter; nations?: string }; - exclude?: { vehicle: ConsumablesVehicleFilter; nations?: string }; - }; - script: { - "#text": string; - automatic?: boolean; - cooldown: number; - duration?: number; - shotEffect?: string; - bonusValues?: { [key: string]: number }; - } & Record; - }; -} -export interface ProvisionsCommon { - [key: string]: { - id: number; - userString: string; - description: string; - icon: string; - category: string; - tags: string; - vehicleFilter?: { - include: { vehicle: ConsumablesVehicleFilter; nations?: string }; - exclude?: { vehicle: ConsumablesVehicleFilter; nations?: string }; - }; - script: { - "#text": string; - automatic?: boolean; - shotEffect?: string; - bonusValues?: { [key: string]: number }; - } & Record; - }; -} - -type CamouflagesInclude = - | { - nations?: string; - } - | { - vehicle?: { name?: string; minLevel?: number; maxLevel?: number }; - }; - -interface CamouflagesXML { - camouflages: Record< - string, - { - id: number; - userString: string; - description: string; - category: string; - group: string; - kind: string; - notInShop: boolean; - unlockCostCategory?: string; - vehicleFilter: { - include?: CamouflagesInclude | CamouflagesInclude[]; - }; - script: string[] | string; - icon?: string; - unlockPremium?: string; - unlockQuest?: string; - unlockClanLevel?: number; - unlockShopBundle?: string; - unlockTankRank?: number; - } - >; -} - -type CamouflagesYaml = Record< - string, - { - userString?: string; - shortUserString?: string; - } ->; - -type ConsumablesVehicleFilter = - | { minLevel: number; maxLevel: number } - | { name: string } - | { extendedTags: string }; - -export interface Maps { - maps: { - [key: string]: { - id: number; - tags?: string; - localName: string; - avaliableInTrainingRoom: boolean; // lol wg typo - spriteFrame: number; - supremacyPointsThreshold?: number; - availableModes: number[]; - shadowMapsAvailable?: boolean; - assaultRespawnPoints: { - allies: MapAlly[]; - enemies: MapAlly[]; - }; - levels?: number[]; - }; - }; -} - -export interface MapAlly { - respawnNumber: number; - points: Array; -} - -interface AvailableNationsYaml { - available_nations: string[]; -} - -export interface SquadBattleTypeStyles { - Prototypes: { - components: { - UIDataLocalBindingsComponent: { - data: [string, string, string][]; - }; - }; - }[]; -} - -type CombatRolesYaml = Record< - string, - { - id: number; - default_abilities: string[]; - } ->; - -const blitzShellKindToBlitzkit: Record = { - 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, -}; -export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU))$/; -const blitzModuleTypeToBlitzkit: Record = { - 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, -}; - -function 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); - } -} - -export async function definitions() { - console.log("Building definitions..."); - - const uploader = new AssetUploader("definitions"); - const gameDefinitions: GameDefinitions = { - version: (await vfs.text("Data/version.txt")).split(" ")[0], - nations: ( - await vfs.yaml("Data/available_nations.yaml") - ).available_nations, - gameModes: {}, - roles: {}, - }; - const tankDefinitions: TankDefinitions = { tanks: {} }; - const camouflageDefinitions: CamouflageDefinitions = { camouflages: {} }; - const modelDefinitions: ModelDefinitions = { models: {} }; - const mapDefinitions: MapDefinitions = { maps: {} }; - const equipmentDefinitions: EquipmentDefinitions = { - presets: {}, - equipments: {}, - }; - const consumableDefinitions: ConsumableDefinitions = { consumables: {} }; - const provisionDefinitions: ProvisionDefinitions = { provisions: {} }; - const skillDefinitions: SkillDefinitions = { - classes: { - [TankClass.TANK_CLASS_LIGHT]: { skills: [] }, - [TankClass.TANK_CLASS_MEDIUM]: { skills: [] }, - [TankClass.TANK_CLASS_HEAVY]: { skills: [] }, - [TankClass.TANK_CLASS_TANK_DESTROYER]: { skills: [] }, - }, - }; - const nations = await vfs - .dir("Data/XML/item_defs/vehicles") - .then((files) => files.filter((nation) => nation !== "common")); - const tankStringIdMap: Record = {}; - const optionalDevices = await vfs.xml<{ root: OptionalDevices }>( - "Data/XML/item_defs/vehicles/common/optional_devices.xml", - ); - const stringsI18n: Record> = {}; - - 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 vfs.yaml( - `Data/Strings/${blitzLocale}.yaml`, - ); - - stringsI18n[locale] = { - ...cache, - ...preInstalled, - }; - - console.log(`Loaded strings for ${locale}`); - }), - ); - - function getString(name: string) { - const collection: Record = { - [locales.default]: stringsI18n[locales.default][name], - }; - - for (const { locale } of locales.supported) { - const localizedString = stringsI18n[locale][name]; - - if ( - localizedString === undefined || - localizedString === collection[locales.default] - ) { - continue; - } - - collection[locale] = localizedString; - } - - return { locales: collection } satisfies I18nString; - } - - const optionalDeviceSlots = await vfs.xml<{ - root: OptionalDeviceSlots; - }>(`Data/XML/item_defs/vehicles/common/optional_device_slots.xml`); - const consumables: ConsumablesCommon = {}; - const provisions: ProvisionsCommon = {}; - - for (const match of ( - await vfs.text(`Data/XML/item_defs/vehicles/common/consumables/list.xml`) - ).matchAll(//g)) { - if (match[1] === "prototypes") continue; - - Object.assign( - consumables, - ( - await vfs.xml<{ root: ConsumablesCommon }>( - `Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`, - ) - ).root, - ); - } - - for (const match of ( - await vfs.text(`Data/XML/item_defs/vehicles/common/provisions/list.xml`) - ).matchAll(//g)) { - if (match[1] === "prototypes") continue; - - Object.assign( - provisions, - ( - await vfs.xml<{ root: ConsumablesCommon }>( - `Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`, - ) - ).root, - ); - } - - const avatar = await vfs.xml<{ root: Avatar }>( - `Data/XML/item_defs/tankmen/avatar.xml`, - ); - const maps = await vfs.yaml(`Data/maps.yaml`); - const tankXps = new Map(); - const camouflagesXml = await vfs.xml<{ root: CamouflagesXML }>( - `Data/XML/item_defs/vehicles/common/camouflages.xml`, - ); - const camouflagesYaml = await vfs.yaml( - `Data/camouflages.yaml`, - ); - const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages); - const squadBattleTypeStyles = await vfs.yaml( - `Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`, - ); - const gameTypeSelectorStyles = await vfs.yaml( - `Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`, - ); - const gameModeNativeNames: Record = {}; - const combatRoles = await vfs.yaml( - `Data/XML/item_defs/vehicles/common/combat_roles.yaml`, - ); - const consumableNativeNames: Record = {}; - const provisionNativeNames: Record = {}; - let equalizerDefinitions: string[][]; - - if ( - await vfs.resolve("Data/XML/item_defs/vehicles/common/tier_equializer.csv") - ) { - equalizerDefinitions = await vfs.csv( - `Data/XML/item_defs/vehicles/common/tier_equializer.csv`, - { delimiter: ";" }, - ); - } else { - const stubEqualizerDefinitions = await readFile("stub/equalizer.csv").then( - (buffer) => buffer.toString(), - ); - - equalizerDefinitions = await vfs._csv(stubEqualizerDefinitions, { - delimiter: ";", - }); - } - - for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( - /"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g, - )) { - const id = Number(match[1]); - const name = getString(match[2]); - - gameModeNativeNames[match[3]] = id; - gameDefinitions.gameModes[id] = { - name, - }; - } - - for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( - /eGameMode\.([a-zA-Z]+) -> "~res:\/Gfx\/UI\/Hangar\/GameTypes\/battle-type_([^"]+)"/g, - )) { - Object.entries(gameModeNativeNames).forEach(([key, value]) => { - if (key.toLowerCase() === match[2].toLowerCase()) { - gameModeNativeNames[match[1]] = value; - } - }); - } - - camouflagesXmlEntries.forEach(([camoKey, camo]) => { - const yamlEntry = camouflagesYaml[camoKey]; - const fullName = yamlEntry.userString - ? getString(yamlEntry.userString) - : undefined; - const shortName = yamlEntry.shortUserString - ? getString(yamlEntry.shortUserString) - : undefined; - const resolvedTankName = shortName ?? fullName; - const resolvedTankNameFull = - resolvedTankName === fullName ? undefined : fullName; - - camouflageDefinitions.camouflages[camo.id] = { - id: camo.id, - name: getString(camo.userString), - tank_name: resolvedTankName, - tank_name_full: resolvedTankNameFull, - }; - }); - - const slugRequesters = new Map(); - const idToNation: Record = {}; - - for (const nation of nations) { - const tankList = await vfs.xml<{ root: VehicleDefinitionList }>( - `Data/XML/item_defs/vehicles/${nation}/list.xml`, - ); - - for (const tankKey in tankList.root) { - if (botPattern.test(tankKey)) continue; - - const tank = tankList.root[tankKey]; - const tankId = toUniqueId(nation, tank.id); - - const name = ( - (tank.shortUserString ? getString(tank.shortUserString) : undefined) ?? - getString(tank.userString) - ).locales.en; - - let slug = deburr(name).toLowerCase(); - slug = slug.replaceAll(nonAlphanumericRegex, "-"); - slug = slug.replaceAll(multipleDashesRegex, "-"); - slug = slug.replaceAll(trailingDashRegex, ""); - slug = slug.replaceAll(leadingDashRegex, ""); - - 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(); - - 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 = - nationSlugDiscriminators[ - nation as keyof typeof 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`); - } - }); - - for (const nation of nations) { - const tankList = await vfs.xml<{ root: VehicleDefinitionList }>( - `Data/XML/item_defs/vehicles/${nation}/list.xml`, - ); - const turretList = await vfs.xml<{ - root: TurretDefinitionsList; - }>(`Data/XML/item_defs/vehicles/${nation}/components/turrets.xml`); - const gunList = await vfs.xml<{ - root: GunDefinitionsList; - }>(`Data/XML/item_defs/vehicles/${nation}/components/guns.xml`); - const shellList = await vfs.xml<{ - root: ShellDefinitionsList; - }>(`Data/XML/item_defs/vehicles/${nation}/components/shells.xml`); - const enginesList = await vfs.xml<{ - root: EngineDefinitionsList; - }>(`Data/XML/item_defs/vehicles/${nation}/components/engines.xml`); - const chassisList = await vfs.xml<{ - root: ChassisDefinitionsList; - }>(`Data/XML/item_defs/vehicles/${nation}/components/chassis.xml`); - - function resolveUnlocks(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 (botPattern.test(tankKey)) continue; - - const gunXps = new Map(); - const turretXps = new Map(); - const engineXps = new Map(); - const trackXps = new Map(); - const tank = tankList.root[tankKey]; - let tankPrice: TankPrice; - const tankDefinition = await vfs.xml<{ root: VehicleDefinitions }>( - `Data/XML/item_defs/vehicles/${nation}/${tankKey}.xml`, - ); - const tankParameters = await vfs.yaml( - `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 tankTags = tank.tags.split(" "); - const hullArmor: Armor = { spaced: [], thickness: {} }; - const equipment = tankDefinition.root.optDevicePreset; - tankStringIdMap[`${nation}:${tankKey}`] = tankId; - 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, - }; - } - - 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]; - - assignArmor(armorRaw, armorId, hullArmor); - }); - const crew: Crew[] = []; - const fixedCamouflage = tankTags.includes("eventCamouflage_user"); - const totalUnlocks: UnlocksListing[] = []; - - Object.entries(tankDefinition.root.crew).forEach(([crewKey, value]) => { - let entry: Crew; - const index = crew.findIndex( - ({ type }) => blitzkitCrewTypeToBlitz[type] === crewKey, - ); - if (index === -1) { - if (crewKey === "#text") return; - entry = { - type: 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 blitzCrewTypeToBlitzkit[member.trim() as BlitzCrewType]; - }); - } - } else { - if (entry.count === undefined) { - entry.count = value.length; - } else { - entry.count += value.length; - } - } - }); - - const camouflages = camouflagesXmlEntries - .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 = equalizerDefinitions.find( - (line) => line[0] === `${nation}:${tankKey}`, - ); - - let equalizer: Equalizer | undefined; - - if (equalizerEntry) { - const [health, penetration, module_health, damage, armor] = - equalizerEntry?.slice(1).map(Number); - equalizer = { health, penetration, module_health, damage, armor }; - } - - tankDefinitions.tanks[tankId] = { - ancestors: [], - successors: [], - id: tankId, - dev_name: tankKey, - roles: {}, - camouflages: camouflages, - fixed_camouflage: fixedCamouflage, - crew, - weight: tankDefinition.root.hull.weight, - health: tankDefinition.root.hull.maxHealth, - speed_forwards: tankDefinition.root.speedLimits.forward, - speed_backwards: tankDefinition.root.speedLimits.backward, - equipment_preset: - typeof equipment === "string" ? equipment : equipment.at(-1)!, - max_consumables: tankDefinition.root.consumableSlots, - max_provisions: tankDefinition.root.provisionSlots, - name: - (tank.shortUserString - ? getString(tank.shortUserString) - : undefined) ?? 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: blitzTankClassToBlitzkit[tankTags[0] as BlitzTankClass], - testing: tankTags.includes("testTank"), - deprecated: tankTags.includes("deprecated"), - price: tankPrice, - camouflage_still: tankDefinition.root.invisibility.still, - camouflage_moving: tankDefinition.root.invisibility.moving, - camouflage_on_fire: tankDefinition.root.invisibility.firePenalty, - turrets: [], - engines: [], - tracks: [], - equalizer, - }; - - if (tank.combatRole) { - Object.entries(tank.combatRole).forEach(([gameMode, role]) => { - const id = Object.entries(gameModeNativeNames).find( - ([key]) => key.toLowerCase() === gameMode.toLowerCase(), - )?.[1]; - - if (id === undefined) { - throw new Error( - `Unknown game mode in tank ${tankKey}: ${gameMode}`, - ); - } - - tankDefinitions.tanks[tankId].roles[id] = combatRoles[role].id; - }); - } - - modelDefinitions.models[tankId] = { - armor: hullArmor, - turret_origin: 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: vector3TupleToBlitzkit(tankParameters.collision.hull.bbox.min), - max: vector3TupleToBlitzkit(tankParameters.collision.hull.bbox.max), - }, - turrets: {}, - tracks: {}, - }; - - 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); - const trackArmorRaw = track.armor.leftTrack; - const hullOrigin = track.hullPosition - .split(" ") - .map(Number) as Vector3Tuple; - - totalUnlocks.push(track.unlocks); - tankDefinitions.tanks[tankId].tracks.push({ - id: trackId, - weight: track.weight, - name: 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(track.unlocks), - }); - - modelDefinitions.models[tankId].tracks[trackId] = { - thickness: - typeof trackArmorRaw === "number" - ? trackArmorRaw - : trackArmorRaw["#text"], - origin: vector3TupleToBlitzkit(hullOrigin), - }; - }); - - 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: getString(engineListEntry.userString), - fire_chance: engineListEntry.fireStartingChance, - tier: engineListEntry.level, - weight: engineListEntry.weight, - power: engineListEntry.power, - unlocks: resolveUnlocks(engine.unlocks), - }); - }); - - Object.keys(tankDefinition.root.turrets0).forEach( - (turretKey, turretIndex) => { - 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: [] }; - - totalUnlocks.push(turret.unlocks); - 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]; - - assignArmor(armorRaw, armorId, turretArmor); - }); - - turret.userString; - - tankDefinitions.tanks[tankId].turrets.push({ - id: turretId, - traverse_speed: turret.rotationSpeed, - name: getString(turret.userString), - tier: turret.level, - guns: [], - health: turret.maxHealth, - view_range: turret.circularVisionRadius, - weight: turret.weight, - unlocks: resolveUnlocks(turret.unlocks), - }); - - modelDefinitions.models[tankId].turrets[turretId] = { - armor: turretArmor, - gun_origin: vector3TupleToBlitzkit(gunOrigin), - model_id: turretModel, - yaw: - -turretYaw[0] + turretYaw[1] === 360 - ? undefined - : { min: turretYaw[0], max: turretYaw[1] }, - guns: {}, - bounding_box: { - min: vector3TupleToBlitzkit( - tankParameters.collision[ - parsePath(turret.hitTester.collisionModel).name.toLowerCase() - ].bbox.min, - ), - max: 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 gunName = getString(gunListEntry.userString); - const gunType = - "clip" in gun - ? gun.pumpGunMode - ? "autoReloader" - : "autoLoader" - : "regular"; - // const gunReload = gun.reloadTime; - // const shellReloads = - const gunClipCount = gunType === "regular" ? 1 : gun.clip!.count; - 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 shotDispersionFactors = - gun.shotDispersionFactors ?? gunListEntry.shotDispersionFactors; - const maskName = `gun_${gunModel.toString().padStart(2, "0")}`; - const maskEnabled = - tankParameters.maskSlice?.[maskName]?.enabled ?? false; - let mask: number | undefined; - 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 blitzShellKindToBlitzkit) { - return 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, - })), - }; - } - - if (maskEnabled) { - const maskRaw = tankParameters.maskSlice![maskName]!; - mask = maskRaw.planePosition[1]; - } else { - mask = undefined; - } - - totalUnlocks.push(gun.unlocks); - 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]; - - assignArmor(armorRaw, armorId, gunArmor); - }); - - 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(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); - - 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, - }, - }; - - Object.keys(gunListEntry.shots).forEach((shellKey) => { - const gunShellEntry = gunListEntry.shots[shellKey]; - const shell = shellList.root[shellKey]; - const shellId = toUniqueId(nation, shell.id); - const shellName = 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: 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, - }); - }); - }); - }, - ); - - totalUnlocks.forEach((unlocks) => { - if (unlocks === undefined) return; - - Object.entries(unlocks).forEach(([key, value]) => { - (Array.isArray(value) ? value : [value]).forEach((vehicle) => { - 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, 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"]]), - parseResearchCost(vehicle.cost), - ); - break; - } - - case "turret": { - turretXps.set( - toUniqueId(nation, turretList.root.ids[vehicle["#text"]]), - parseResearchCost(vehicle.cost), - ); - break; - } - - case "engine": { - engineXps.set( - toUniqueId(nation, enginesList.root.ids[vehicle["#text"]]), - parseResearchCost(vehicle.cost), - ); - break; - } - - case "chassis": { - trackXps.set( - toUniqueId(nation, chassisList.root.ids[vehicle["#text"]]), - 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); - } - }); - }); - - Object.entries(optionalDevices.root).forEach( - ([optionalDeviceKey, optionalDeviceEntry]) => { - if (optionalDeviceKey === "nextAvailableId") return; - - equipmentDefinitions.equipments[optionalDeviceEntry.id] = { - name: getString(optionalDeviceEntry.userString), - description: getString(optionalDeviceEntry.description), - }; - }, - ); - - Object.entries(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: optionalDevices.root[options.device0].id, - right: optionalDevices.root[options.device1].id, - } satisfies EquipmentSlot; - }); - }) - .flat(), - }; - }, - ); - - Object.entries(consumables).forEach(([key, consumable]) => { - consumableNativeNames[key] = consumable.id; - - const entry: Consumable = { - id: consumable.id, - game_mode_exclusive: "gameModeFilter" in consumable, - cooldown: consumable.script.cooldown, - duration: consumable.script.duration, - name: 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 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 tankStringIdMap[key]; - }), - }, - }, - }); - } else if ("extendedTags" in excludeRaw) { - entry.exclude!.push({ - filter_type: { - $case: "categories", - value: { - categories: excludeRaw.extendedTags - .split(" ") - .map( - (item) => - 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(" "), - }, - }, - }); - } - } - }); - - Object.entries(provisions).forEach(([key, provision]) => { - provisionNativeNames[key] = provision.id; - - const entry: Provision = { - id: provision.id, - exclude: [], - include: [], - game_mode_exclusive: "gameModeFilter" in provision, - name: 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 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) => tankStringIdMap[key]), - }, - }, - }); - } else if ("extendedTags" in excludeRaw) { - entry.exclude!.push({ - filter_type: { - $case: "categories", - value: { - categories: excludeRaw.extendedTags - .split(" ") - .map( - (item) => - 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; - } - }); - - Object.entries(combatRoles).forEach(([, value]) => { - gameDefinitions.roles[value.id] = { provisions: [], consumables: [] }; - - value.default_abilities.forEach((ability) => { - if (ability in consumableNativeNames) { - gameDefinitions.roles[value.id].consumables.push( - consumableNativeNames[ability], - ); - } else if (ability in provisionNativeNames) { - gameDefinitions.roles[value.id].provisions.push( - provisionNativeNames[ability], - ); - } else throw new Error(`Unknown ability ${ability}`); - }); - }); - - Object.entries(avatar.root.skillsByClasses).forEach(([tankClass, skills]) => { - skillDefinitions.classes[ - blitzTankClassToBlitzkit[tankClass as BlitzTankClass] - ] = { - skills: skills.split(" "), - }; - }); - - Object.entries(maps.maps).forEach(([key, map]) => { - mapDefinitions.maps[map.id] = { - id: map.id, - name: getString(`#maps:${key}:${map.localName}`), - }; - }); - - await uploader.add({ - content: GameDefinitions.encode(gameDefinitions).finish(), - path: "definitions/game.pb", - }); - await uploader.add({ - content: TankDefinitions.encode(tankDefinitions).finish(), - path: "definitions/tanks.pb", - }); - await uploader.add({ - content: ModelDefinitions.encode(modelDefinitions).finish(), - path: "definitions/models.pb", - }); - await uploader.add({ - content: EquipmentDefinitions.encode(equipmentDefinitions).finish(), - path: "definitions/equipment.pb", - }); - await uploader.add({ - content: ConsumableDefinitions.encode(consumableDefinitions).finish(), - path: "definitions/consumables.pb", - }); - await uploader.add({ - content: ProvisionDefinitions.encode(provisionDefinitions).finish(), - path: "definitions/provisions.pb", - }); - await uploader.add({ - content: SkillDefinitions.encode(skillDefinitions).finish(), - path: "definitions/skills.pb", - }); - await uploader.add({ - content: MapDefinitions.encode(mapDefinitions).finish(), - path: "definitions/maps.pb", - }); - await uploader.add({ - content: CamouflageDefinitions.encode(camouflageDefinitions).finish(), - path: "definitions/camouflages.pb", - }); - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/equipmentIcons.ts b/packages/scripts/src/buildAssets/equipmentIcons.ts deleted file mode 100644 index aebe2c847..000000000 --- a/packages/scripts/src/buildAssets/equipmentIcons.ts +++ /dev/null @@ -1,80 +0,0 @@ -import sharp from "sharp"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; -import { OptionalDevices } from "./definitions"; - -interface Mappings { - Header: { version: number }; - - StyleSheets: { - selectors: string[]; - properties: { "bg-sprite": string }; - }[]; -} - -export async function equipmentIcons() { - console.log("Building equipment icons..."); - - using uploader = new AssetUploader("equipment icons"); - const optionalDevices = await vfs.xml<{ root: OptionalDevices }>( - `Data/XML/item_defs/vehicles/common/optional_devices.xml`, - ); - const mappings = await vfs.yaml( - `Data/UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml`, - ); - const image = sharp( - await vfs.file( - `Data/Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp`, - ), - ); - - for (const optionalDeviceKey in optionalDevices.root) { - const optionalDevice = optionalDevices.root[optionalDeviceKey]; - - if (optionalDeviceKey === "nextAvailableId") continue; - - const mapping = mappings.StyleSheets.find((mapping) => - mapping.selectors.includes( - `.optional_device_item.${optionalDevice.icon} #Img`, - ), - ); - - if (!mapping) { - console.warn(`No mapping found for ${optionalDevice.icon}; skipping...`); - continue; - } - - const configPathRaw = mapping.properties["bg-sprite"]; - const configPath = configPathRaw.replace("~res:/", ""); - - if (configPath.startsWith("Gfx/Lobby")) { - const configPathWebp = configPath.replace(".txt", ""); - const image = sharp(await vfs.file(`Data/${configPathWebp}.packed.webp`)); - const content = await image.trim().toBuffer(); - - await uploader.add({ - path: `icons/equipment/${optionalDevice.id}.webp`, - content, - }); - } else { - const config = await vfs.text(`Data/${configPath}`); - const sizes = config.split("\n")[4].split(" ").map(Number); - const content = await image - .clone() - .extract({ - left: sizes[0], - top: sizes[1], - width: sizes[2], - height: sizes[3], - }) - .toBuffer(); - - await uploader.add({ - path: `icons/equipment/${optionalDevice.id}.webp`, - content, - }); - } - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/flags.ts b/packages/scripts/src/buildAssets/flags.ts deleted file mode 100644 index 1168af119..000000000 --- a/packages/scripts/src/buildAssets/flags.ts +++ /dev/null @@ -1,73 +0,0 @@ -import sharp from "sharp"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; - -export async function flags() { - console.log("Building flags..."); - - using uploader = new AssetUploader("flags"); - const circleFiles = await vfs - .dir(`Data/Gfx/Lobby/flags`) - .then((files) => - files.filter( - (flag) => - flag.startsWith("flag_profile-stat_") && - !flag.endsWith("@2x.packed.webp"), - ), - ); - const scratchedFiles = await vfs - .dir(`Data/Gfx/Lobby/flags`) - .then((files) => - files.filter( - (flag) => - flag.startsWith("flag_tutor-tank_") && - !flag.endsWith("@2x.packed.webp"), - ), - ); - const fadedFiles = await vfs - .dir(`Data/Gfx/Lobby/flags`) - .then((files) => - files.filter( - (flag) => - flag.startsWith("flag_filter_") && flag.endsWith("@2x.packed.webp"), - ), - ); - - for (const flag of circleFiles) { - const image = sharp(await vfs.file(`Data/Gfx/Lobby/flags/${flag}`)); - const content = await image.trim({ threshold: 100 }).toBuffer(); - const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1]; - - uploader.add({ - content, - path: `flags/circle/${name}.webp`, - }); - } - - for (const flag of scratchedFiles) { - const content = await vfs.file(`Data/Gfx/Lobby/flags/${flag}`); - const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1]; - - uploader.add({ - path: `flags/scratched/${name}.webp`, - content, - }); - } - - for (const flag of fadedFiles) { - const content = await sharp(await vfs.file(`Data/Gfx/Lobby/flags/${flag}`)) - .trim({ - threshold: 100, - background: { r: 0, g: 0, b: 0, alpha: 0 }, - }) - .toBuffer(); - const name = flag.match(/flag_filter_(.+)@2x\.packed\.webp/)![1]; - - uploader.add({ - content, - path: `flags/fade_small/${name}.webp`, - }); - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/gameModeBanners.ts b/packages/scripts/src/buildAssets/gameModeBanners.ts deleted file mode 100644 index df8a6a73d..000000000 --- a/packages/scripts/src/buildAssets/gameModeBanners.ts +++ /dev/null @@ -1,48 +0,0 @@ -import sharp from "sharp"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; -import { SquadBattleTypeStyles } from "./definitions"; - -export async function gameModeBanners() { - console.log("Building game mode banners..."); - - using uploader = new AssetUploader("game mode banners"); - const gameTypeSelectorStyles = await vfs.yaml( - `Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`, - ); - const squadBattleTypeStyles = await vfs.yaml( - `Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`, - ); - const bannerMatches: { name: string; path: string }[] = []; - - for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( - /eGameMode\.([a-zA-Z]+) -> "~res:([^"]+)"/g, - )) { - bannerMatches.push({ name: match[1], path: match[2] }); - } - - for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( - /"(\d+)" -> "battleType\/([a-zA-Z]+)"/g, - )) { - const id = Number(match[1]); - const name = match[2]; - let path = bannerMatches.find( - (bannerMatch) => bannerMatch.name.toLowerCase() === name.toLowerCase(), - )?.path; - - if (path === undefined) { - path = `/Gfx/UI/Hangar/GameTypes/battle-type_${name.toLowerCase()}`; - } - - const content = await sharp(await vfs.file(`Data${path}.packed.webp`)) - .trim({ background: { r: 0, g: 0, b: 0, alpha: 0 } }) - .toBuffer(); - - await uploader.add({ - path: `icons/game_mode_banners/${id}.webp`, - content, - }); - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/glossary.ts b/packages/scripts/src/buildAssets/glossary.ts deleted file mode 100644 index ed3b83cfd..000000000 --- a/packages/scripts/src/buildAssets/glossary.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Avatar, fetchGlossary, Gallery } from "@blitzkit/core"; -import locales from "@blitzkit/i18n/locales.json"; -import { extname } from "path"; -import ProgressBar from "progress"; -import { AssetUploader } from "../core/github/assetUploader"; - -export async function glossary() { - console.log("Building glossary..."); - - using uploader = new AssetUploader("glossary"); - const avatars: Record = {}; - - 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: [] }; - const totalCount = Object.keys(avatars).length; - - const bar = new ProgressBar( - `Fetching avatars of ${totalCount} things :bar`, - totalCount, - ); - - 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`); - bar.tick(); - - return; - } - - gallery.avatars.push(avatar); - - const content = await fetch(url) - .then((response) => response.arrayBuffer()) - .then((buffer) => new Uint8Array(buffer)); - - await uploader.add({ - path: `gallery/avatars/${key}${avatar.extension}`, - content, - }); - - bar.tick(); - }), - ); - - await uploader.add({ - path: "definitions/gallery.pb", - content: Gallery.encode(gallery).finish(), - }); - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/index.ts b/packages/scripts/src/buildAssets/index.ts deleted file mode 100644 index 495765e0c..000000000 --- a/packages/scripts/src/buildAssets/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { config } from "dotenv"; -import { argv } from "process"; -import { boosterIcons } from "./boosterIcons"; -import { camouflageIcons } from "./camouflageIcons"; -import { vfs } from "./constants"; -import { consumableProvisionIcons } from "./consumableProvisionIcons"; -import { currencies } from "./currencies"; -import { definitions } from "./definitions"; -import { equipmentIcons } from "./equipmentIcons"; -import { flags } from "./flags"; -import { gameModeBanners } from "./gameModeBanners"; -import { glossary } from "./glossary"; -import { migration } from "./migration"; -import { moduleIcons } from "./moduleIcons"; -import { popularTanks } from "./popularTanks"; -import { shellIcons } from "./shellIcons"; -import { skillIcons } from "./skillIcons"; -import { tankArmors } from "./tankArmors"; -import { tankIcons } from "./tankIcons"; -import { tankModels } from "./tankModels"; - -config(); - -const allTargets = argv.includes("--all-targets"); -const targets = argv - .find((argument) => argument.startsWith("--target")) - ?.split("=")[1] - .split(","); - -if (!targets && !allTargets) throw new Error("No target(s) specified"); - -const methods = [ - definitions, - tankIcons, - tankModels, - tankArmors, - gameModeBanners, - currencies, - skillIcons, - flags, - shellIcons, - moduleIcons, - equipmentIcons, - consumableProvisionIcons, - camouflageIcons, - boosterIcons, - glossary, - migration, - popularTanks, -]; - -for (const method of methods) { - if (allTargets || targets?.includes(method.name)) { - try { - await method(); - } catch (error) { - console.warn(`Failed method ${method.name}; skipping...`); - console.error(error); - } - } -} - -vfs.dispose(); -process.exit(0); diff --git a/packages/scripts/src/buildAssets/migration.ts b/packages/scripts/src/buildAssets/migration.ts deleted file mode 100644 index ca806f961..000000000 --- a/packages/scripts/src/buildAssets/migration.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { toUniqueId } from "@blitzkit/core"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; -import { VehicleDefinitionList } from "./definitions"; - -export async function migration() { - console.log("Building migration map..."); - - using uploader = new AssetUploader("migration map"); - const map: Record = {}; - const nations = await vfs - .dir(`Data/XML/item_defs/vehicles`) - .then((files) => files.filter((nation) => nation !== "common")); - - for (const nation of nations) { - const tankList = await vfs.xml<{ root: VehicleDefinitionList }>( - `Data/XML/item_defs/vehicles/${nation}/list.xml`, - ); - - for (const tankKey in tankList.root) { - const tank = tankList.root[tankKey]; - const tankId = toUniqueId(nation, tank.id); - const davaKey = `${nation}:${tankKey}`; - - map[davaKey] = tankId; - } - } - - await uploader.add({ - path: "definitions/migration.json", - content: new TextEncoder().encode(JSON.stringify(map)), - }); - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/moduleIcons.ts b/packages/scripts/src/buildAssets/moduleIcons.ts deleted file mode 100644 index 01b9790b1..000000000 --- a/packages/scripts/src/buildAssets/moduleIcons.ts +++ /dev/null @@ -1,34 +0,0 @@ -import sharp from "sharp"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; - -export async function moduleIcons() { - console.log("Building module icons..."); - - using uploader = new AssetUploader("module icons"); - const files = await vfs - .dir(`Data/Gfx/UI/ModulesTechTree`) - .then((files) => - files.filter( - (file) => - !file.endsWith("@2x.packed.webp") && file.startsWith("vehicle"), - ), - ); - - for (const file of files) { - const nameRaw = file.match(/vehicle(.+)\.packed\.webp/)![1]; - const name = nameRaw[0].toLowerCase() + nameRaw.slice(1); - const content = await sharp( - await vfs.file(`Data/Gfx/UI/ModulesTechTree/${file}`), - ) - .trim() - .toBuffer(); - - await uploader.add({ - content, - path: `icons/modules/${name}.webp`, - }); - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/popularTanks.ts b/packages/scripts/src/buildAssets/popularTanks.ts deleted file mode 100644 index 9d281d4d2..000000000 --- a/packages/scripts/src/buildAssets/popularTanks.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - assertSecret, - fetchTankDefinitions, - PopularTanks, -} from "@blitzkit/core"; -import { google } from "googleapis"; -import { AssetUploader } from "../core/github/assetUploader"; - -export async function popularTanks() { - using uploader = new AssetUploader("popular tanks"); - const tankDefinitions = await fetchTankDefinitions(); - - const auth = await google.auth.getClient({ - keyFile: import.meta.env.GOOGLE_APPLICATION_CREDENTIALS, - scopes: ["https://www.googleapis.com/auth/analytics.readonly"], - }); - const analytics = google.analyticsdata({ version: "v1beta", auth }); - const report = await analytics.properties.runReport({ - property: `properties/${assertSecret( - import.meta.env.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID, - )}`, - requestBody: { - dateRanges: [{ startDate: "7daysAgo", endDate: "today" }], - dimensions: [{ name: "pagePath" }], - metrics: [{ name: "screenPageViews" }], - orderBys: [{ metric: { metricName: "screenPageViews" }, desc: true }], - dimensionFilter: { - filter: { - fieldName: "pagePath", - stringFilter: { - matchType: "BEGINS_WITH", - value: "/tanks/", - }, - }, - }, - }, - }); - - if (!report.data.rows) { - throw new Error("No rows in report"); - } - - const tanks = report.data.rows - .filter( - (row) => - row.dimensionValues && - row.dimensionValues[0].value && - row.dimensionValues[0].value !== "/tanks/" && - row.metricValues && - row.metricValues[0].value, - ) - .map((row) => ({ - id: Number( - row.dimensionValues![0].value!.match( - /\/tools\/tankopedia\/(\d+)\/?/, - )?.[1], - ), - views: Number(row.metricValues![0].value!), - })) - .filter((row) => { - if (!(row.id in tankDefinitions.tanks)) { - console.warn("Skipping unknown popular tank", row.id); - return false; - } - - return row.id in tankDefinitions.tanks; - }) - .slice(0, 8) - .map(({ id, views }) => ({ id, views })); - const popularTanks: PopularTanks = { tanks }; - - await uploader.add({ - path: "definitions/popular-tanks.pb", - content: PopularTanks.encode(popularTanks).finish(), - }); - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/shellIcons.ts b/packages/scripts/src/buildAssets/shellIcons.ts deleted file mode 100644 index fabe047e1..000000000 --- a/packages/scripts/src/buildAssets/shellIcons.ts +++ /dev/null @@ -1,42 +0,0 @@ -import sharp from "sharp"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; - -export async function shellIcons() { - console.log("Building shell icons..."); - - using uploader = new AssetUploader("shell icons"); - const image = sharp( - await vfs.file( - `Data/Gfx/Shared/tank-supply/ammunition/big/texture0.packed.webp`, - ), - ); - const files = await vfs - .dir(`Data/Gfx/Shared/tank-supply/ammunition/big`) - .then((files) => files.filter((file) => file.endsWith("_l.txt"))); - - for (const file of files) { - const name = file.match(/(.+)_l\.txt/)![1]; - const sizes = ( - await vfs.text(`Data/Gfx/Shared/tank-supply/ammunition/big/${file}`) - ) - .split("\n")[4] - .split(" ") - .map(Number); - - await uploader.add({ - content: await image - .clone() - .extract({ - left: sizes[0], - top: sizes[1], - width: sizes[2], - height: sizes[3], - }) - .toBuffer(), - path: `icons/shells/${name}.webp`, - }); - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/skillIcons.ts b/packages/scripts/src/buildAssets/skillIcons.ts deleted file mode 100644 index 4b3264ae1..000000000 --- a/packages/scripts/src/buildAssets/skillIcons.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { TankClass } from "@blitzkit/core"; -import sharp from "sharp"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; - -interface SkillIcon { - name: string; - state: number; -} - -export interface Avatar { - roles: unknown; - skillsByClasses: Record; - skills: { - [name: string]: { - userString: string; - effectDescription: string; - tipDescription: string; - icon: SkillIcon | SkillIcon[]; - type: "continuous" | "trigger"; - }; - }; -} - -export async function skillIcons() { - console.log("Building skill icons..."); - - const avatar = await vfs.xml<{ root: Avatar }>( - `Data/XML/item_defs/tankmen/avatar.xml`, - ); - const uploader = new AssetUploader("skill icons"); - - for (const key in avatar.root.skills) { - const skill = avatar.root.skills[key]; - const icon = Array.isArray(skill.icon) ? skill.icon[0] : skill.icon; - const name = icon.name.split("/").at(-1)!.replace(/_\d$/, ""); - const path = `Data${icon.name.replace("~res:", "")}.packed.webp`; - const image = sharp(await vfs.file(path)).trim(); - const content = await image.toBuffer(); - - await uploader.add({ content, path: `icons/skills/${name}.webp` }); - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/tankArmors.ts b/packages/scripts/src/buildAssets/tankArmors.ts deleted file mode 100644 index 1deb46514..000000000 --- a/packages/scripts/src/buildAssets/tankArmors.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { toUniqueId } from "@blitzkit/core"; -import { NodeIO } from "@gltf-transform/core"; -import { extractArmor } from "../core/blitz/extractArmor"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; -import { VehicleDefinitionList } from "./definitions"; - -export async function tankArmors() { - console.log("Building tank armors..."); - - using uploader = new AssetUploader("tank armors"); - const nodeIO = new NodeIO(); - const nations = await vfs - .dir(`Data/XML/item_defs/vehicles`) - .then((files) => files.filter((nation) => nation !== "common")); - - for (const nation of nations) { - const tanks = await vfs.xml<{ root: VehicleDefinitionList }>( - `Data/XML/item_defs/vehicles/${nation}/list.xml`, - ); - - for (const tankKey in tanks.root) { - const tank = tanks.root[tankKey]; - - if (tankKey.includes("tutorial_bot")) continue; - - const id = toUniqueId(nation, tank.id); - const model = await extractArmor(vfs, `${nation}-${tankKey}`); - const content = await nodeIO.writeBinary(model); - - await uploader.add({ path: `3d/tanks/armor/${id}.glb`, content }); - } - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/tankIcons.ts b/packages/scripts/src/buildAssets/tankIcons.ts deleted file mode 100644 index fb74388d9..000000000 --- a/packages/scripts/src/buildAssets/tankIcons.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { NATION_IDS } from "@blitzkit/core"; -import sharp from "sharp"; -import type { Vector3Tuple } from "three"; -import { parsePackedSpriteRect } from "../core/blitz/parsePackedSpriteRect"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; -import type { VehicleDefinitionList } from "./definitions"; - -export interface TankParameters { - resourcesPath: { - smallIconPath: string; - bigIconPath: string; - blitzModelPath: string; - }; - collision: { - [key: string]: { - points: Vector3Tuple; - bbox: { - min: Vector3Tuple; - max: Vector3Tuple; - }; - averageThickness?: Record; - }; - }; - maskSlice?: { - [key: string]: - | { - enabled: boolean; - planePosition: Vector3Tuple; - planeNormal: Vector3Tuple; - planeAxis: Vector3Tuple; - frustumConeBaseRadius: number; - frustumConeAngleDegrees: number; - planeSliceMode: number; - frustumConeSliceMode: number; - } - | undefined; - }; -} - -async function extractPackedTankIcon(packedBuffer: Buffer, context: string) { - const texture = sharp(packedBuffer); - const spriteRect = parsePackedSpriteRect(packedBuffer); - - if (!spriteRect) { - throw new Error(`Missing RIFF sprite bounds for ${context}`); - } - - const [left, top, width, height] = spriteRect; - const bounds = [left, top, width, height]; - - if (!bounds.every(Number.isInteger)) { - throw new Error( - `Invalid RIFF sprite bounds for ${context}: ${bounds.join(" ")}`, - ); - } - - const metadata = await texture.metadata(); - - if (metadata.width === undefined || metadata.height === undefined) { - throw new Error(`Failed to read image dimensions for ${context}`); - } - - if ( - left < 0 || - top < 0 || - width <= 0 || - height <= 0 || - left + width > metadata.width || - top + height > metadata.height - ) { - throw new Error( - `Out-of-bounds RIFF sprite for ${context}: ${bounds.join(" ")} in ${metadata.width}x${metadata.height}`, - ); - } - - return await texture.extract({ left, top, width, height }).toBuffer(); -} - -export async function tankIcons() { - console.log("Building tank icons..."); - - using uploader = new AssetUploader("tank icons"); - const nations = await vfs - .dir(`Data/XML/item_defs/vehicles`) - .then((files) => files.filter((nation) => nation !== "common")); - - for (const nation of nations) { - const tanks = await vfs.xml<{ root: VehicleDefinitionList }>( - `Data/XML/item_defs/vehicles/${nation}/list.xml`, - ); - - for (const tankKey in tanks.root) { - const tank = tanks.root[tankKey]; - - if (tankKey.includes("tutorial_bot")) continue; - - const nationVehicleId = tank.id; - const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; - - const parameters = await vfs.yaml( - `Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`, - ); - const smallPath = `Data/${parameters.resourcesPath.smallIconPath - .replace(/~res:\//, "") - .replace(/\..+/, "")}.packed.webp`; - const bigPath = `Data/${parameters.resourcesPath.bigIconPath - .replace(/~res:\//, "") - .replace(/\..+/, "")}.packed.webp`; - const big = await extractPackedTankIcon( - await vfs.file(bigPath), - `tank ${tankKey} big`, - ); - const small = await extractPackedTankIcon( - await vfs.file(smallPath), - `tank ${tankKey} small`, - ); - - if (big) { - await uploader.add({ - content: big, - path: `icons/tanks/big/${id}.webp`, - }); - } - if (small) { - await uploader.add({ - content: small, - path: `icons/tanks/small/${id}.webp`, - }); - } - } - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/buildAssets/tankModels.ts b/packages/scripts/src/buildAssets/tankModels.ts deleted file mode 100644 index e0f609ef7..000000000 --- a/packages/scripts/src/buildAssets/tankModels.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { toUniqueId } from "@blitzkit/core"; -import { NodeIO } from "@gltf-transform/core"; -import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; -import { extractModel } from "../core/blitz/extractModel"; -import { AssetUploader } from "../core/github/assetUploader"; -import { vfs } from "./constants"; -import { VehicleDefinitionList } from "./definitions"; -import { TankParameters } from "./tankIcons"; - -export async function tankModels() { - console.log("Building tank models..."); - - using uploader = new AssetUploader("tank models"); - const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS); - const nations = await vfs - .dir(`Data/XML/item_defs/vehicles`) - .then((files) => files.filter((nation) => nation !== "common")); - - for (const nationIndex in nations) { - const nation = nations[nationIndex]; - const tanks = await vfs.xml<{ root: VehicleDefinitionList }>( - `Data/XML/item_defs/vehicles/${nation}/list.xml`, - ); - const entries = Object.entries(tanks.root); - - for (const [tankKey, tank] of entries) { - if (tankKey.includes("tutorial_bot")) continue; - - const id = toUniqueId(nation, tank.id); - const parameters = await vfs.yaml( - `Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`, - ); - const model = await extractModel( - vfs, - parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ""), - ); - const content = Buffer.from(await nodeIO.writeBinary(model)); - - await uploader.add({ path: `3d/tanks/models/${id}.glb`, content }); - } - } - - await uploader.flush(); -} diff --git a/packages/scripts/src/core/github/assetUploader.ts b/packages/scripts/src/core/github/assetUploader.ts deleted file mode 100644 index ff8254a7f..000000000 --- a/packages/scripts/src/core/github/assetUploader.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { assertSecret, asset } from "@blitzkit/core"; -import { Octokit } from "@octokit/rest"; -import ProgressBar from "progress"; -import { compareUint8Arrays } from "./compareUint8Arrays"; - -export interface FileChange { - path: string; - content: Uint8Array; -} - -interface GithubChangeBlob { - sha: string; - path: string; - mode: "100644"; - type: "blob"; -} - -enum DiffStatus { - New, - Changed, - Unchanged, -} - -export class AssetUploader { - time_per_blob = 2 ** 4 * 1000; - time_between_blobs = (60 * 60 * 1000) / 5000 / 0.9; - - max_tree_size = 7_000_000; - max_file_count = 128; - - heuristicFormats = ["glb", "webp", "jpg", "png"]; - - owner: string; - repo: string; - branch: string; - - changes: FileChange[] = []; - changesSize = 0; - - private octokit = new Octokit({ - auth: assertSecret(import.meta.env.GH_TOKEN), - }); - - constructor(public message: string) { - [this.owner, this.repo] = assertSecret( - import.meta.env.PUBLIC_ASSET_REPO, - ).split("/"); - this.branch = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH); - } - - async add(change: FileChange) { - const response = await fetch( - asset(change.path), - { headers: { "Accept-Encoding": "identity" } }, - ); - let diff: { change: number; status: DiffStatus }; - - if (response.status === 404) { - diff = { change: change.content.length, status: DiffStatus.New }; - } else if ( - response.headers.has("Content-Length") && - Number(response.headers.get("Content-Length")) !== change.content.length - ) { - diff = { - change: - change.content.length - - Number(response.headers.get("Content-Length")), - status: DiffStatus.Changed, - }; - } else if (response.status === 200) { - if ( - this.heuristicFormats.some((format) => - change.path.endsWith(`.${format}`), - ) - ) { - /** - * Heuristic: if it's a large blob like a .glb, a diff = 0 might end - * up being a false positive with isDifferent = true. So, it's okay - * to assume it's unchanged. - */ - diff = { change: 0, status: DiffStatus.Unchanged }; - } else { - const buffer = new Uint8Array(await response.arrayBuffer()); - - if (compareUint8Arrays(buffer, change.content)) { - diff = { change: 0, status: DiffStatus.Unchanged }; - } else { - diff = { - change: change.content.length - buffer.length, - status: DiffStatus.Changed, - }; - } - } - } else { - throw new Error( - `Unexpected status code ${response.status} for ${change.path}`, - ); - } - - if (diff.status === DiffStatus.Unchanged) { - // console.log(`πŸ”΅ ${change.path}`); - } else { - console.log( - `${diff.status === DiffStatus.New ? "🟒" : "🟑"} (${ - diff.change > 0 ? "+" : "" - }${diff.change.toLocaleString()}B) ${change.path}`, - ); - - // flush BEFORE going over limit - if ( - this.changesSize + change.content.length > this.max_tree_size || - this.changes.length + 1 > this.max_file_count - ) { - console.log(`Flushing ${this.changes.length} changes...`); - await this.flush(); - } - - this.changes.push(change); - this.changesSize += change.content.length; - } - } - - async flush() { - if (this.changes.length == 0) return; - - const latestCommitSha = ( - await this.octokit.git.getRef({ - owner: this.owner, - repo: this.repo, - ref: `heads/${this.branch}`, - }) - ).data.object.sha; - const treeSha = ( - await this.octokit.git.getCommit({ - owner: this.owner, - repo: this.repo, - commit_sha: latestCommitSha, - }) - ).data.tree.sha; - const blobs: GithubChangeBlob[] = []; - const bar = new ProgressBar("Blobs :bar", this.changes.length); - - for (const change of this.changes) { - while (true) { - try { - const start = Date.now(); - var { sha } = await this.octokit.git - .createBlob({ - owner: this.owner, - repo: this.repo, - encoding: "base64", - content: Buffer.from(change.content).toString("base64"), - }) - .then((response) => response.data); - const end = Date.now(); - - console.log(` blobbed ${change.path} ${end - start}ms`); - - await new Promise((resolve) => - setTimeout( - resolve, - Math.max(0, this.time_between_blobs + (start - end)), - ), - ); - - blobs.push({ sha, path: change.path, mode: "100644", type: "blob" }); - bar.tick(); - - break; - } catch { - console.warn( - ` failed ${change.path}; retrying in ${this.time_per_blob}ms...`, - ); - - await new Promise((resolve) => - setTimeout(resolve, this.time_per_blob), - ); - } - } - } - - const { data: treeData } = await this.octokit.git.createTree({ - owner: this.owner, - repo: this.repo, - base_tree: treeSha, - tree: blobs, - }); - const { data: commitData } = await this.octokit.git.createCommit({ - owner: this.owner, - repo: this.repo, - message: this.message, - tree: treeData.sha, - parents: [latestCommitSha], - }); - - await this.octokit.git.updateRef({ - owner: this.owner, - repo: this.repo, - ref: `heads/${this.branch}`, - sha: commitData.sha, - force: true, - }); - - this.changes = []; - this.changesSize = 0; - } - - [Symbol.dispose]() { - if (this.changes.length > 0) { - throw new Error( - `${this.changes.length} uncommitted changes. Did you forget to flush?`, - ); - } - } -} diff --git a/packages/scripts/src/core/github/compareUint8Arrays.ts b/packages/scripts/src/core/github/compareUint8Arrays.ts deleted file mode 100644 index fd927cb9c..000000000 --- a/packages/scripts/src/core/github/compareUint8Arrays.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function compareUint8Arrays(a: Uint8Array, b: Uint8Array) { - if (a.length !== b.length) return false; - - for (let index = 0; index < a.length; index++) { - if (a[index] !== b[index]) return false; - } - - return true; -} diff --git a/packages/scripts/src/core/github/createBlob.ts b/packages/scripts/src/core/github/createBlob.ts deleted file mode 100644 index cd27ced51..000000000 --- a/packages/scripts/src/core/github/createBlob.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { throttle } from "lodash-es"; -import { FileChange } from "./assetUploader"; -import { octokit } from "./octokit"; - -export interface GithubChangeBlob { - sha: string; - path: string; - mode: "100644"; - type: "blob"; -} - -const TIME_PER_BLOB = 2 ** 4 * 1000; - -// github rate limit: 5000 requests per hour -export const createBlob = throttle( - async (owner: string, repo: string, change: FileChange) => { - while (true) { - try { - const { sha } = ( - await octokit.git.createBlob({ - owner, - repo, - content: Buffer.from(change.content).toString("base64"), - encoding: "base64", - }) - ).data; - - return { - sha, - path: change.path, - mode: "100644", - type: "blob", - } satisfies GithubChangeBlob; - } catch (error) { - console.warn( - `Failed blob ${change.path}; retrying in ${TIME_PER_BLOB}ms...`, - error - ); - - await new Promise((resolve) => setTimeout(resolve, TIME_PER_BLOB)); - } - } - }, - (5000 / (60 * 60 * 1000)) * 0.9, - { trailing: true } -); diff --git a/packages/scripts/src/core/github/fetchPreDiscoveredIds.ts b/packages/scripts/src/core/github/fetchPreDiscoveredIds.ts deleted file mode 100644 index c14d779ae..000000000 --- a/packages/scripts/src/core/github/fetchPreDiscoveredIds.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - asset, - DidsReadStream, - DiscoveredIdsDefinitions, -} from '@blitzkit/core'; -import { times } from 'lodash-es'; -import { decompress } from 'lz4js'; -import ProgressBar from 'progress'; - -export async function fetchPreDiscoveredIds() { - const idChunks: number[][] = []; - const manifest = (await fetch(asset('ids/manifest.json')).then((response) => - response.json(), - )) as DiscoveredIdsDefinitions; - const bar = new ProgressBar( - `Fetching ${manifest.count.toLocaleString()} ids in ${ - manifest.chunks - } chunks :bar`, - manifest.chunks, - ); - - bar.render(); - await Promise.all( - times(manifest.chunks, async (chunkIndex) => { - const preDiscovered = await fetch( - asset(`ids/${chunkIndex}.dids.lz4`), - ).then(async (response) => { - const buffer = await response.arrayBuffer(); - const decompressed = decompress(new Uint8Array(buffer)).buffer; - return new DidsReadStream(decompressed as ArrayBuffer).dids(); - }); - - idChunks.push(preDiscovered); - bar.tick(); - }), - ); - - return idChunks.sort((a, b) => a[0] - b[0]).flat(); -} diff --git a/packages/scripts/src/core/github/octokit.ts b/packages/scripts/src/core/github/octokit.ts deleted file mode 100644 index b0b21eada..000000000 --- a/packages/scripts/src/core/github/octokit.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { assertSecret } from "@blitzkit/core"; -import { throttling } from "@octokit/plugin-throttling"; -import { Octokit } from "@octokit/rest"; - -Octokit.plugin(throttling); - -export const octokit = new Octokit({ - auth: assertSecret(import.meta.env.GH_TOKEN), -}); diff --git a/packages/scripts/src/discoverServers.ts b/packages/scripts/src/discoverServers.ts index a54920538..725069c03 100644 --- a/packages/scripts/src/discoverServers.ts +++ b/packages/scripts/src/discoverServers.ts @@ -1,4 +1,4 @@ -import { assertSecret } from "@blitzkit/core"; +import { secret } from "@blitzkit/core"; import { GameInterface } from "@blitzkit/game"; interface Bindings { @@ -50,10 +50,10 @@ interface Server { } const game = new GameInterface( - assertSecret(import.meta.env.PUBLIC_WOTB_CLIENT), - assertSecret(import.meta.env.WOTB_USMAP), - assertSecret(import.meta.env.TEMP), - Number(assertSecret(import.meta.env.TEXTURE_CHUNKS)), + secret("PUBLIC_WOTB_CLIENT"), + secret("WOTB_USMAP"), + secret("TEMP"), + Number(secret("TEXTURE_CHUNKS")), ); game.discoveryUrls().forEach(async (url) => {