diff --git a/docs/changelogs/1.14.3.md b/docs/changelogs/1.14.3.md new file mode 100644 index 000000000..26a30b3e1 --- /dev/null +++ b/docs/changelogs/1.14.3.md @@ -0,0 +1,24 @@ +# Blitzkrieg 1.14.3 + +Pull your brush out and start polishing. + +## Changes + +![HisRoyalFatness' career breakdown](https://i.imgur.com/GhgMTnS.png) + +- Rows in `/breakdown` are now chunked into `8` per image + - This is true unless the number of pages exceeds `10` in which case the rows are even distributed across all `10` pages + - `10` is the maximum number of images that Discord allows +- Short region names are used in username autocomplete instead of their full names + - Example: "North America" is "NA" now + +## Fixes + +- Removed reference to username inference in the username option description +- Removed an unnecessary type `asd` that did absolutely nothing used nowhere + +## Technical changes + +- Image orders are now preserved even if processed in parallel and all completed out of order +- `fetchBlitz` is now throttled to `10` requests per second + - Request overflows are managed by queueing diff --git a/scripts/build.ts b/scripts/build.ts index d94cdc7d7..9a6efefcc 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -27,7 +27,7 @@ readdir('src/workers').then((workers) => { bundle: true, sourcemap: true, - minifyIdentifiers: false, // cause goofy errors + minifyIdentifiers: false, // causes goofy errors minifySyntax: true, minifyWhitespace: true, diff --git a/src/app/embeds/session/page.tsx b/src/app/embeds/session/page.tsx index 728f84710..0b75c1617 100644 --- a/src/app/embeds/session/page.tsx +++ b/src/app/embeds/session/page.tsx @@ -5,7 +5,7 @@ import { useEffect, useState } from 'react'; import * as Breakdown from '../../../components/Breakdown'; import { TreeTypeEnum } from '../../../components/Tanks'; import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID'; -import fetchBlitz from '../../../core/blitz/fetchWargaming'; +import fetchBlitz from '../../../core/blitz/fetchBlitz'; import { tankAverages } from '../../../core/blitzstars/tankAverages'; import { TankopediaEntry, diff --git a/src/commands/breakdown.tsx b/src/commands/breakdown.tsx index 477b158f4..bf17273f8 100644 --- a/src/commands/breakdown.tsx +++ b/src/commands/breakdown.tsx @@ -1,4 +1,5 @@ import { SlashCommandBuilder } from 'discord.js'; +import { chunk } from 'lodash'; import * as Breakdown from '../components/Breakdown'; import NoData, { NoDataType } from '../components/NoData'; import TitleBar from '../components/TitleBar'; @@ -35,6 +36,9 @@ import { StatFilters, filterStats } from '../core/statistics/filterStats'; import getWN8Percentile from '../core/statistics/getWN8Percentile'; import { CommandRegistryRaw } from '../events/interactionCreate'; +const ROWS_PER_PAGE = 8; +const MAX_PAGES = 10; + async function render( { region, id }: ResolvedPlayer, { start, end, name }: ResolvedPeriod, @@ -132,38 +136,80 @@ async function render( 0, ) / careerBattles; - return ( - - + const children: JSX.Element[] = []; - {filteredOrder.length === 0 && ( - - )} - {filteredOrder.length > 0 && ( - + if (filteredOrder.length > 0) { + children.push( + , + ); + + await Promise.all( + filteredOrder.map(async (id, index) => { + const tankopediaEntry = awaitedTankopedia[id]; + const current = orderedCurrentStats[index]; + const career = orderedCareerStats[index]; + const currentWN8 = orderedCurrentWN8[index]; + const careerWN8 = orderedCareerWN8[index]; + + children.push( + />, + ); + }), + ); + } - {await Promise.all( - filteredOrder.map(async (id, index) => { - const tankopediaEntry = awaitedTankopedia[id]; - const current = orderedCurrentStats[index]; - const career = orderedCareerStats[index]; - const currentWN8 = orderedCurrentWN8[index]; - const careerWN8 = orderedCareerWN8[index]; - - return ( - - ); - }), - )} - - )} - + const pages = chunk( + children, + Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE), ); + + if (filteredOrder.length > 0) { + return pages + .map((page, index) => ( + + {index === 0 && ( + + )} + + {page} + + )) + .reverse(); + } else { + return [ + + + + {filteredOrder.length === 0 && ( + + )} + , + ]; + } } export const breakdownCommand = new Promise( @@ -286,11 +316,11 @@ export const breakdownCommand = new Promise( ...filters, }); - return Promise.all([ - render(player, period, filters), + return [ + ...(await render(player, period, filters)), buttonPrimary(path, 'Refresh'), - getBlitzStarsLinkButton(player.region, player.id), - ]); + await getBlitzStarsLinkButton(player.region, player.id), + ]; }, autocomplete: (interaction) => { diff --git a/src/commands/searchClans.ts b/src/commands/searchClans.ts index 8eb4bc83d..9b4860799 100644 --- a/src/commands/searchClans.ts +++ b/src/commands/searchClans.ts @@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js'; import markdownEscape from 'markdown-escape'; import { REGION_NAMES, Region } from '../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; -import fetchBlitz from '../core/blitz/fetchWargaming'; +import fetchBlitz from '../core/blitz/fetchBlitz'; import addClanChoices from '../core/discord/addClanChoices'; import addRegionChoices from '../core/discord/addRegionChoices'; import embedInfo from '../core/discord/embedInfo'; diff --git a/src/commands/searchPlayers.ts b/src/commands/searchPlayers.ts index 9a376d9c7..3d6037bcc 100644 --- a/src/commands/searchPlayers.ts +++ b/src/commands/searchPlayers.ts @@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js'; import markdownEscape from 'markdown-escape'; import { REGION_NAMES, Region } from '../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; -import fetchBlitz from '../core/blitz/fetchWargaming'; +import fetchBlitz from '../core/blitz/fetchBlitz'; import { AccountList, usernamePattern, diff --git a/src/constants/regions.ts b/src/constants/regions.ts index 218394efe..ee4b04781 100644 --- a/src/constants/regions.ts +++ b/src/constants/regions.ts @@ -11,7 +11,7 @@ export const REGION_NAMES: Record = { export const REGION_NAMES_SHORT: Record = { com: 'NA', eu: 'EU', - asia: 'AS', + asia: 'ASIA', }; export const REGION_SUBDOMAIN_NAMES: Record = { diff --git a/src/core/blitz/fetchBlitz.ts b/src/core/blitz/fetchBlitz.ts new file mode 100644 index 000000000..64f1ae26d --- /dev/null +++ b/src/core/blitz/fetchBlitz.ts @@ -0,0 +1,52 @@ +export 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; + +async function manageQueue() { + if (queue.length > 0 && inProgress < MAX_CALLS_PER_SECOND) { + inProgress++; + + setTimeout(() => { + inProgress--; + manageQueue(); + }, 1000); + + const { url, resolve } = queue.shift()!; + const data = (await fetch(url) + .then((response) => response.json()) + .catch((error) => ({ status: 'error', error }))) as BlitzResponse< + typeof resolve + >; + + if (data.status === 'ok') { + resolve(data.data); + } else { + throw new Error(`Wargaming response error status:"${data.status}"`, { + cause: `Message: "${data.error.message}"\nURL: "${url}"`, + }); + } + } +} + +export default function fetchBlitz(url: string) { + return new Promise((resolve) => { + queue.push({ + url, + resolve(data) { + resolve(data as Data); + }, + }); + manageQueue(); + }); +} diff --git a/src/core/blitz/fetchWargaming.ts b/src/core/blitz/fetchWargaming.ts deleted file mode 100644 index a17c98d5c..000000000 --- a/src/core/blitz/fetchWargaming.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type BlitzResponse = - | { - status: 'error'; - error: { field: string; message: string; code: 402; value: null }; - } - | { - status: 'ok'; - data: Data; - }; - -export default async function fetchBlitz(url: string) { - const data = (await fetch(url).then((response) => - response.json(), - )) as BlitzResponse; - - if (data.status === 'ok') { - return data.data; - } else { - throw new Error(`Wargaming response error status:"${data.status}"`, { - cause: `Message: "${data.error.message}"\nURL: "${url}"`, - }); - } -} diff --git a/src/core/blitz/getAccountAchievements.ts b/src/core/blitz/getAccountAchievements.ts index edb21ca14..98b7c9cfd 100644 --- a/src/core/blitz/getAccountAchievements.ts +++ b/src/core/blitz/getAccountAchievements.ts @@ -1,6 +1,6 @@ import { Region } from '../../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; -import fetchBlitz from './fetchWargaming'; +import fetchBlitz from './fetchBlitz'; import { normalizeIds } from './normalizeIds'; interface IndividualAccountAchievements { diff --git a/src/core/blitz/getAccountInfo.ts b/src/core/blitz/getAccountInfo.ts index 5852ea073..efb795c41 100644 --- a/src/core/blitz/getAccountInfo.ts +++ b/src/core/blitz/getAccountInfo.ts @@ -1,6 +1,6 @@ import { Region } from '../../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; -import fetchBlitz from './fetchWargaming'; +import fetchBlitz from './fetchBlitz'; import { normalizeIds } from './normalizeIds'; export interface AllStats { diff --git a/src/core/blitz/getClanAccountInfo.ts b/src/core/blitz/getClanAccountInfo.ts index 6c36fc15f..ffb334ca2 100644 --- a/src/core/blitz/getClanAccountInfo.ts +++ b/src/core/blitz/getClanAccountInfo.ts @@ -1,6 +1,6 @@ import { Region } from '../../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; -import fetchBlitz from './fetchWargaming'; +import fetchBlitz from './fetchBlitz'; import { normalizeExtra } from './normalizeExtra'; import { normalizeIds } from './normalizeIds'; diff --git a/src/core/blitz/getClanInfo.ts b/src/core/blitz/getClanInfo.ts index 702ebea84..85cfa154f 100644 --- a/src/core/blitz/getClanInfo.ts +++ b/src/core/blitz/getClanInfo.ts @@ -1,6 +1,6 @@ import { Region } from '../../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; -import fetchBlitz from './fetchWargaming'; +import fetchBlitz from './fetchBlitz'; import { normalizeIds } from './normalizeIds'; export interface IndividualClanInfo { diff --git a/src/core/blitz/getTankStats.ts b/src/core/blitz/getTankStats.ts index acb9fcc39..4965c43d7 100644 --- a/src/core/blitz/getTankStats.ts +++ b/src/core/blitz/getTankStats.ts @@ -2,7 +2,7 @@ import { Region } from '../../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { TanksStats } from '../../types/tanksStats'; import { UserError } from '../blitzkrieg/userError'; -import fetchBlitz from './fetchWargaming'; +import fetchBlitz from './fetchBlitz'; export default async function getTankStats(region: Region, id: number) { const tankStats = await fetchBlitz( diff --git a/src/core/blitz/searchClansAcrossRegions.ts b/src/core/blitz/searchClansAcrossRegions.ts index 1e62aa981..98b4cfabb 100644 --- a/src/core/blitz/searchClansAcrossRegions.ts +++ b/src/core/blitz/searchClansAcrossRegions.ts @@ -1,7 +1,7 @@ import { Region } from '../../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { Clan, ClanList } from '../../types/clanList'; -import fetchBlitz from './fetchWargaming'; +import fetchBlitz from './fetchBlitz'; export type ClanListWithRegion = (Clan & { region: 'com' | 'eu' | 'asia'; diff --git a/src/core/blitz/searchPlayersAcrossRegions.ts b/src/core/blitz/searchPlayersAcrossRegions.ts index b24315ee1..e8a12a17a 100644 --- a/src/core/blitz/searchPlayersAcrossRegions.ts +++ b/src/core/blitz/searchPlayersAcrossRegions.ts @@ -1,6 +1,6 @@ import { Region } from '../../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; -import fetchBlitz from './fetchWargaming'; +import fetchBlitz from './fetchBlitz'; interface Account { nickname: string; diff --git a/src/core/blitzstars/tankopedia.ts b/src/core/blitzstars/tankopedia.ts index 7be335929..a21182b0e 100644 --- a/src/core/blitzstars/tankopedia.ts +++ b/src/core/blitzstars/tankopedia.ts @@ -1,7 +1,6 @@ import { deburr } from 'lodash'; -import { TreeTypeString } from '../../components/Tanks'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; -import fetchBlitz from '../blitz/fetchWargaming'; +import fetchBlitz from '../blitz/fetchBlitz'; import { context } from '../blitzkrieg/context'; export interface TankopediaEntry { @@ -16,7 +15,6 @@ export interface TankopediaEntry { type: string; description: string; } -type asd = TreeTypeString; export interface Tankopedia { [id: number]: TankopediaEntry | undefined; } diff --git a/src/core/discord/addUsernameChoices.ts b/src/core/discord/addUsernameChoices.ts index cc089f368..77b19936d 100644 --- a/src/core/discord/addUsernameChoices.ts +++ b/src/core/discord/addUsernameChoices.ts @@ -3,8 +3,6 @@ import { SlashCommandStringOption } from 'discord.js'; export default function addUsernameChoices(option: SlashCommandStringOption) { return option .setName('username') - .setDescription( - 'The username you use in Blitz (inferred from Discord nickname by default)', - ) + .setDescription('The username you use in Blitz') .setAutocomplete(true); } diff --git a/src/core/discord/autocompleteUsername.ts b/src/core/discord/autocompleteUsername.ts index 8d5428883..5160a2024 100644 --- a/src/core/discord/autocompleteUsername.ts +++ b/src/core/discord/autocompleteUsername.ts @@ -3,7 +3,7 @@ import { AutocompleteInteraction, CacheType, } from 'discord.js'; -import { REGION_NAMES } from '../../constants/regions'; +import { REGION_NAMES_SHORT } from '../../constants/regions'; import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions'; export default async function autocompleteUsername( @@ -19,7 +19,9 @@ export default async function autocompleteUsername( ? players.map( (player) => ({ - name: `${player.nickname} (${REGION_NAMES[player.region]})`, + name: `${player.nickname} (${ + REGION_NAMES_SHORT[player.region] + })`, value: `${player.region}/${player.account_id}`, }) satisfies ApplicationCommandOptionChoiceData, ) diff --git a/src/core/discord/normalizeInteractionReturnable.ts b/src/core/discord/normalizeInteractionReturnable.ts index cdb0e2208..5642f5564 100644 --- a/src/core/discord/normalizeInteractionReturnable.ts +++ b/src/core/discord/normalizeInteractionReturnable.ts @@ -11,6 +11,7 @@ import jsxToPngThreaded from '../blitzkrieg/jsxToPngThreaded'; export default async function normalizeInteractionReturnable( returnable: InteractionReturnable, ) { + const images: [number, Buffer][] = []; const reply: InteractionEditReplyOptions = {}; const awaitedReturnable = await returnable; @@ -19,7 +20,7 @@ export default async function normalizeInteractionReturnable( : [awaitedReturnable]; await Promise.all( - normalizedReturnable.map(async (item) => { + normalizedReturnable.map(async (item, index) => { if (typeof item === 'string') { reply.content = item; } else if (item instanceof EmbedBuilder) { @@ -38,11 +39,16 @@ export default async function normalizeInteractionReturnable( return; } else { const image = await jsxToPngThreaded(item); - if (!reply.files) reply.files = []; - reply.files.push(image); + images.push([index, image]); } }), ); + if (images.length > 0) { + images.sort(([a], [b]) => b - a); + if (!reply.files) reply.files = []; + images.forEach(([, image]) => reply.files?.push(image)); + } + return reply; } diff --git a/src/stores/session.ts b/src/stores/session.ts index 9c125f933..fd3786189 100644 --- a/src/stores/session.ts +++ b/src/stores/session.ts @@ -4,7 +4,7 @@ import { devtools, persist } from 'zustand/middleware'; import { CustomColumnDisplay } from '../app/tools/session/components/CustomColumn'; import { Region } from '../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; -import fetchBlitz from '../core/blitz/fetchWargaming'; +import fetchBlitz from '../core/blitz/fetchBlitz'; import { NormalizedTankStats, TanksStats } from '../types/tanksStats'; type Session = (