{
const tankAveragesEntry = awaitedTankAverages[id];
const current = statsInPeriod.diff[id];
diff --git a/src/commands/ownedTanks.tsx b/src/commands/ownedTanks.tsx
index 29f081c67..8a51ffde2 100644
--- a/src/commands/ownedTanks.tsx
+++ b/src/commands/ownedTanks.tsx
@@ -31,11 +31,16 @@ export const ownedTanksCommand = new Promise
((resolve) => {
.addStringOption(addUsernameChoices),
async handler(interaction) {
- const { translate } = translator(interaction.locale);
+ const { t, translate } = translator(interaction.locale);
const tier = Number(interaction.options.getString('tier'));
const { id, region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id);
- const tankStats = await getTankStats(region, id, interaction.locale);
+ const tankStats = await getTankStats(region, id);
+
+ if (tankStats === null) {
+ return t`bot.common.errors.no_tank_stats`;
+ }
+
const filteredTanks = (
await Promise.all(
tankStats.map(async (tankData) => ({
diff --git a/src/commands/research.tsx b/src/commands/research.tsx
index ab50379c6..d2bfd9e21 100644
--- a/src/commands/research.tsx
+++ b/src/commands/research.tsx
@@ -126,7 +126,11 @@ export const researchCommand = new Promise((resolve) => {
]);
}
- const tankStats = await getTankStats(region, id, interaction.locale);
+ const tankStats = await getTankStats(region, id);
+
+ if (tankStats === null) {
+ return t`bot.common.errors.no_tank_stats`;
+ }
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
return translate('bot.commands.research.errors.already_researched', [
diff --git a/src/commands/searchClans.ts b/src/commands/searchClans.ts
index 3fe09224a..2307ad249 100644
--- a/src/commands/searchClans.ts
+++ b/src/commands/searchClans.ts
@@ -1,7 +1,6 @@
import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { Region } from '../constants/regions';
-import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchBlitz';
import addClanChoices from '../core/discord/addClanChoices';
import addRegionChoices from '../core/discord/addRegionChoices';
@@ -49,17 +48,18 @@ export const searchClansCommand = new Promise((resolve) => {
async handler(interaction) {
const { translate } = translator(interaction.locale);
- const server = interaction.options.getString('region') as Region;
+ const region = interaction.options.getString('region') as Region;
const clan = interaction.options.getString('clan')!;
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
- const clanList = await fetchBlitz(
- `https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
- );
+ const clanList = await fetchBlitz(region, 'clans/list', {
+ search: clan,
+ limit,
+ });
return embedInfo(
translate('bot.commands.search_clans.body.title', [
markdownEscape(clan),
- translate(`common.regions.normal.${server}`),
+ translate(`common.regions.normal.${region}`),
]),
clanList.length === 0
? translate('bot.commands.search_clans.body.no_results')
diff --git a/src/commands/searchPlayers.ts b/src/commands/searchPlayers.ts
index ed816c1f2..5be8c4f5b 100644
--- a/src/commands/searchPlayers.ts
+++ b/src/commands/searchPlayers.ts
@@ -1,7 +1,6 @@
import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { Region } from '../constants/regions';
-import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchBlitz';
import { AccountList } from '../core/blitz/searchPlayersAcrossRegions';
import { usernamePattern } from '../core/blitz/searchPlayersAcrossRegions/constants';
@@ -50,22 +49,21 @@ export const searchPlayersCommand = new Promise((resolve) => {
async handler(interaction) {
const { translate } = translator(interaction.locale);
- const server = interaction.options.getString('region') as Region;
+ const region = interaction.options.getString('region') as Region;
const name = interaction.options.getString('username')!;
const limit = interaction.options.getInteger('limit') ?? 25;
const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch)
- ? await fetchBlitz(
- `https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
- trimmedSearch,
- )}&limit=${limit}`,
- )
+ ? await fetchBlitz(region, 'account/list', {
+ search: trimmedSearch,
+ limit,
+ })
: [];
return embedInfo(
translate('bot.commands.search_players.body.title', [
markdownEscape(trimmedSearch),
- translate(`common.regions.normal.${server}`),
+ translate(`common.regions.normal.${region}`),
]),
`\`\`\`${
players.length === 0
diff --git a/src/core/blitz/fetchBlitz.ts b/src/core/blitz/fetchBlitz.ts
index 48bd1f211..cc2df8816 100644
--- a/src/core/blitz/fetchBlitz.ts
+++ b/src/core/blitz/fetchBlitz.ts
@@ -1,3 +1,5 @@
+import { Region } from '../../constants/regions';
+import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { patientFetch } from '../blitzkrieg/patientFetch';
type BlitzResponse =
@@ -24,31 +26,54 @@ async function manageQueue() {
manageQueue();
}, 1000);
- const { url, resolve } = queue.shift()!;
- const data = (await patientFetch(url)
+ const request = queue.shift()!;
+ const data = (await patientFetch(request.url)
.then((response) => response.json())
.catch((error) => ({ status: 'error', error }))) as BlitzResponse<
- typeof resolve
+ typeof request.resolve
>;
if (data.status === 'ok') {
- resolve(data.data);
+ request.resolve(data.data);
} else {
- clearTimeout(timeout);
- inProgress--;
- manageQueue();
+ if (data.error.message === 'REQUEST_LIMIT_EXCEEDED') {
+ console.log(
+ `Rate limit exceeded, putting "${request.url}" back in queue...`,
+ );
- throw new Error(`Wargaming response error status: "${data.status}"`, {
- cause: `Message: "${data.error.message}"\nURL: "${url}"`,
- });
+ clearTimeout(timeout);
+ setTimeout(() => {
+ queue.push(request);
+ inProgress--;
+ manageQueue();
+ }, 1000 / MAX_CALLS_PER_SECOND);
+ } else {
+ clearTimeout(timeout);
+ inProgress--;
+ manageQueue();
+
+ throw new Error(`Wargaming response error status: "${data.status}"`, {
+ cause: `Message: "${data.error.message}"\nURL: "${request.url}"`,
+ });
+ }
}
}
}
-export default function fetchBlitz(url: string) {
+export default function fetchBlitz(
+ region: Region,
+ path: string,
+ params: Record = {},
+) {
return new Promise((resolve) => {
queue.push({
- url,
+ url: `https://api.wotblitz.${region}/wotb/${path}/?${Object.entries({
+ application_id: WARGAMING_APPLICATION_ID,
+ ...params,
+ })
+ .filter(([_, value]) => value !== undefined)
+ .map(([key, value]) => `${key}=${encodeURIComponent(value!)}`)
+ .join('&')}`,
resolve(data) {
resolve(data as Data);
},
diff --git a/src/core/blitz/getAccountAchievements.ts b/src/core/blitz/getAccountAchievements.ts
index 98b7c9cfd..c87e34118 100644
--- a/src/core/blitz/getAccountAchievements.ts
+++ b/src/core/blitz/getAccountAchievements.ts
@@ -1,5 +1,4 @@
import { Region } from '../../constants/regions';
-import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds';
@@ -23,9 +22,9 @@ export async function getAccountAchievements<
: IndividualAccountAchievements[],
>(region: Region, ids: Ids) {
const object = await fetchBlitz(
- `https://api.wotblitz.${region}/wotb/account/achievements/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds(
- ids,
- )}`,
+ region,
+ 'account/achievements',
+ { account_id: normalizeIds(ids) },
);
if (typeof ids === 'number') {
diff --git a/src/core/blitz/getAccountInfo.ts b/src/core/blitz/getAccountInfo.ts
index e9cab4285..51f602f0c 100644
--- a/src/core/blitz/getAccountInfo.ts
+++ b/src/core/blitz/getAccountInfo.ts
@@ -1,5 +1,4 @@
import { Region } from '../../constants/regions';
-import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds';
@@ -97,11 +96,10 @@ export async function getAccountInfo<
? IndividualAccountInfo
: IndividualAccountInfo[],
>(region: Region, ids: Ids, extra: string[] = []) {
- const object = await fetchBlitz(
- `https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds(
- ids,
- )}${extra.length === 0 ? '' : `&extra=${extra.join(',')}`}`,
- );
+ const object = await fetchBlitz(region, 'account/info', {
+ account_id: normalizeIds(ids),
+ extra: extra.length === 0 ? undefined : extra.join(','),
+ });
if (typeof ids === 'number') {
return object[ids as number] as ReturnType;
diff --git a/src/core/blitz/getClanAccountInfo.ts b/src/core/blitz/getClanAccountInfo.ts
index de5a73cf4..0a986eac7 100644
--- a/src/core/blitz/getClanAccountInfo.ts
+++ b/src/core/blitz/getClanAccountInfo.ts
@@ -1,7 +1,5 @@
import { Region } from '../../constants/regions';
-import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchBlitz';
-import { normalizeExtra } from './normalizeExtra';
import { normalizeIds } from './normalizeIds';
type IndividualClanAccountInfo = {
@@ -30,11 +28,14 @@ export async function getClanAccountInfo<
ReturnType = Ids extends number
? IndividualClanAccountInfo
: IndividualClanAccountInfo[],
->(region: Region, ids: Ids, extra?: string[]) {
+>(region: Region, ids: Ids, extra: string[] = []) {
const object = await fetchBlitz(
- `https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds(
- ids,
- )}${normalizeExtra(extra)}`,
+ region,
+ 'clans/accountinfo',
+ {
+ account_id: normalizeIds(ids),
+ extra: extra.length === 0 ? undefined : extra.join(','),
+ },
);
if (typeof ids === 'number') {
diff --git a/src/core/blitz/getClanInfo.ts b/src/core/blitz/getClanInfo.ts
index e61a828c2..9ac36d92d 100644
--- a/src/core/blitz/getClanInfo.ts
+++ b/src/core/blitz/getClanInfo.ts
@@ -1,5 +1,4 @@
import { Region } from '../../constants/regions';
-import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds';
@@ -40,11 +39,9 @@ export async function getClanInfo<
Ids extends number | number[],
ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[],
>(region: Region, ids: Ids) {
- const object = await fetchBlitz(
- `https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${normalizeIds(
- ids,
- )}`,
- );
+ const object = await fetchBlitz(region, 'clans/info', {
+ clan_id: normalizeIds(ids),
+ });
if (typeof ids === 'number') {
return object[ids as number] as ReturnType;
diff --git a/src/core/blitz/getTankStats.ts b/src/core/blitz/getTankStats.ts
index 918afc7ee..060efc064 100644
--- a/src/core/blitz/getTankStats.ts
+++ b/src/core/blitz/getTankStats.ts
@@ -1,24 +1,11 @@
-import { Locale } from 'discord.js';
import { Region } from '../../constants/regions';
-import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
-import { UserError } from '../../hooks/userError';
import { TanksStats } from '../../types/tanksStats';
-import { translator } from '../localization/translator';
import fetchBlitz from './fetchBlitz';
-export default async function getTankStats(
- region: Region,
- id: number,
- locale: Locale,
-) {
- const { t } = translator(locale);
- const tankStats = await fetchBlitz(
- `https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
- );
-
- if (tankStats[id] === null) {
- throw new UserError(t`bot.common.errors.no_tank_stats`);
- }
+export default async function getTankStats(region: Region, id: number) {
+ const tankStats = await fetchBlitz(region, 'tanks/stats', {
+ account_id: id,
+ });
return tankStats[id];
}
diff --git a/src/core/blitz/idToRegion.ts b/src/core/blitz/idToRegion.ts
index 2c95af2a1..81b6e5b4f 100644
--- a/src/core/blitz/idToRegion.ts
+++ b/src/core/blitz/idToRegion.ts
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions';
-export function idToRange(id: number): Region {
+export function idToRegion(id: number): Region {
if (id >= 42e8) {
throw new Error('bot id');
} else if (id >= 31e8) {
diff --git a/src/core/blitz/searchClansAcrossRegions.ts b/src/core/blitz/searchClansAcrossRegions.ts
index de5e7a224..6d14f7e8d 100644
--- a/src/core/blitz/searchClansAcrossRegions.ts
+++ b/src/core/blitz/searchClansAcrossRegions.ts
@@ -1,5 +1,4 @@
import { Region } from '../../constants/regions';
-import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { ClanList } from '../../types/clanList';
import fetchBlitz from './fetchBlitz';
@@ -15,9 +14,10 @@ export default async function searchClansAcrossRegions(
return (
await Promise.all([
- fetchBlitz(
- `https://api.wotblitz.com/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
- ).then((value) =>
+ fetchBlitz('com', 'clans/list', {
+ search,
+ limit: normalizedLimit,
+ }).then((value) =>
value
.map((account) => ({
...account,
@@ -25,9 +25,10 @@ export default async function searchClansAcrossRegions(
}))
.splice(0, normalizedLimit),
),
- fetchBlitz(
- `https://api.wotblitz.eu/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
- ).then((value) =>
+ fetchBlitz('eu', 'clans/list', {
+ search,
+ limit: normalizedLimit,
+ }).then((value) =>
value
.map((account) => ({
...account,
@@ -35,9 +36,10 @@ export default async function searchClansAcrossRegions(
}))
.splice(0, normalizedLimit),
),
- fetchBlitz(
- `https://api.wotblitz.asia/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
- ).then((value) =>
+ fetchBlitz('asia', 'clans/list', {
+ search,
+ limit: normalizedLimit,
+ }).then((value) =>
value
.map((account) => ({
...account,
diff --git a/src/core/blitz/searchPlayersAcrossRegions/index.ts b/src/core/blitz/searchPlayersAcrossRegions/index.ts
index 92c7d7ae9..8ca9130a7 100644
--- a/src/core/blitz/searchPlayersAcrossRegions/index.ts
+++ b/src/core/blitz/searchPlayersAcrossRegions/index.ts
@@ -1,5 +1,4 @@
import { Region } from '../../../constants/regions';
-import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../fetchBlitz';
import { usernamePattern } from './constants';
@@ -18,14 +17,14 @@ export default async function searchPlayersAcrossRegions(
) {
const trimmedSearch = search.trim();
const normalizedLimit = Math.floor(limit / 3);
- const encodedSearch = encodeURIComponent(trimmedSearch);
if (usernamePattern.test(trimmedSearch)) {
return (
await Promise.all([
- fetchBlitz(
- `https://api.wotblitz.com/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
- ).then(
+ fetchBlitz('com', 'account/list', {
+ search,
+ limit: normalizedLimit,
+ }).then(
(value) =>
value &&
value.map((account) => ({
@@ -33,9 +32,10 @@ export default async function searchPlayersAcrossRegions(
region: 'com' as Region,
})),
),
- fetchBlitz(
- `https://api.wotblitz.eu/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
- ).then(
+ fetchBlitz('eu', 'account/list', {
+ search,
+ limit: normalizedLimit,
+ }).then(
(value) =>
value &&
value.map((account) => ({
@@ -43,9 +43,10 @@ export default async function searchPlayersAcrossRegions(
region: 'eu' as Region,
})),
),
- fetchBlitz(
- `https://api.wotblitz.asia/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
- ).then(
+ fetchBlitz('asia', 'account/list', {
+ search,
+ limit: normalizedLimit,
+ }).then(
(value) =>
value &&
value.map((account) => ({
diff --git a/src/core/blitzkrieg/commitAssets.ts b/src/core/blitzkrieg/commitAssets.ts
index 2c53e8814..9b5b93be8 100644
--- a/src/core/blitzkrieg/commitAssets.ts
+++ b/src/core/blitzkrieg/commitAssets.ts
@@ -4,6 +4,7 @@ export async function commitAssets(
message: string,
changes: FileChange[],
production: boolean,
+ verbose = true,
) {
console.log(`Committing ${message}...`);
@@ -15,6 +16,6 @@ export async function commitAssets(
production ? 'main' : 'dev',
`${message} - ${new Date().toDateString()}`,
changes,
- true,
+ verbose,
);
}
diff --git a/src/core/blitzkrieg/discordBlitzLink.ts b/src/core/blitzkrieg/discordBlitzLink.ts
index 987c95d14..4fdaf5e75 100644
--- a/src/core/blitzkrieg/discordBlitzLink.ts
+++ b/src/core/blitzkrieg/discordBlitzLink.ts
@@ -1,5 +1,5 @@
import { usersDatabase } from '../../databases/users';
-import { idToRange } from '../blitz/idToRegion';
+import { idToRegion } from '../blitz/idToRegion';
export async function flagUserActivity(blitz: number) {
await usersDatabase.users.upsert({
@@ -26,5 +26,5 @@ export async function getBlitzFromDiscord(discord: bigint) {
if (unique === null) return null;
await flagUserActivity(unique.blitz);
- return { id: unique.blitz, region: idToRange(unique.blitz) };
+ return { id: unique.blitz, region: idToRegion(unique.blitz) };
}
diff --git a/src/core/blitzkrieg/getTankHistories.ts b/src/core/blitzkrieg/getTankHistories.ts
index bba997d01..1a281beec 100644
--- a/src/core/blitzkrieg/getTankHistories.ts
+++ b/src/core/blitzkrieg/getTankHistories.ts
@@ -1,5 +1,6 @@
import { Locale } from 'discord.js';
import { Region } from '../../constants/regions';
+import { UserError } from '../../hooks/userError';
import {
GetHistoriesOptions,
Histories,
@@ -8,6 +9,7 @@ import {
} from '../../types/histories';
import getTankStats from '../blitz/getTankStats';
import { emptyAllStats } from '../blitzstars/getStatsInPeriod/constants';
+import { translator } from '../localization/translator';
export interface TankHistory extends History {
tank_id: number;
@@ -38,6 +40,7 @@ export default async function getTankHistories(
locale: Locale,
options?: Partial,
) {
+ const { t } = translator(locale);
const mergedOptions = { ...getTankHistoriesDefaultOptions, ...options };
const tankHistoriesResponse = await fetch(
`https://www.blitzstars.com/api/tankhistories/for/${id}/`,
@@ -78,7 +81,13 @@ export default async function getTankHistories(
const latestNodes: TankHistories = [];
if (mergedOptions.includeLatestHistories) {
- (await getTankStats(server, id, locale)).forEach((tankHistory) => {
+ const tankStats = await getTankStats(server, id);
+
+ if (tankStats === null) {
+ throw new UserError(t`bot.common.errors.no_tank_stats`);
+ }
+
+ tankStats.forEach((tankHistory) => {
if (
tankHistory.all.battles > (lastBattles[tankHistory.tank_id] ?? 0) &&
(mergedOptions.tankId === undefined ||
diff --git a/src/core/blitzkrieg/setSession.ts b/src/core/blitzkrieg/setSession.ts
index fea967a3d..17a971fb3 100644
--- a/src/core/blitzkrieg/setSession.ts
+++ b/src/core/blitzkrieg/setSession.ts
@@ -1,5 +1,4 @@
import { Region } from '../../constants/regions';
-import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { useSession } from '../../stores/session';
import { NormalizedTankStats, TanksStats } from '../../types/tanksStats';
import fetchBlitz from '../blitz/fetchBlitz';
@@ -7,11 +6,9 @@ import fetchBlitz from '../blitz/fetchBlitz';
export async function setSession(region: Region, id: number, nickname: string) {
// TODO: generalize this function and also check for more of these kind
const rawTankStats = (
- await fetchBlitz(
- `https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
- )
+ await fetchBlitz(region, 'tanks/stats', { account_id: id })
)[id];
- const tankStats = rawTankStats.reduce(
+ const tankStats = rawTankStats!.reduce(
(accumulator, tank) => ({
...accumulator,
[tank.tank_id]: tank,
diff --git a/src/core/blitzkrieg/tankCharacteristics.ts b/src/core/blitzkrieg/tankCharacteristics.ts
index a925f8aad..63681ddab 100644
--- a/src/core/blitzkrieg/tankCharacteristics.ts
+++ b/src/core/blitzkrieg/tankCharacteristics.ts
@@ -291,7 +291,11 @@ export function tankCharacteristics(
);
const dpmMaximum =
gun.type === 'autoReloader'
- ? gun.reload.at(-1)! < gun.reload.at(-2)!
+ ? (
+ gun.count > 2
+ ? gun.reload.at(-1)! < gun.reload.at(-2)!
+ : gun.reload.at(-1)! + gun.intraClip < gun.reload.at(-2)!
+ )
? ((damageCoefficient * shell.damage.armor) /
(gun.reload.at(-1)! * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
diff --git a/src/core/streams/rts.ts b/src/core/streams/rts.ts
new file mode 100644
index 000000000..2d9b0d0e9
--- /dev/null
+++ b/src/core/streams/rts.ts
@@ -0,0 +1,183 @@
+import { times } from 'lodash';
+import { IndividualTankStats } from '../../types/tanksStats';
+import { AllStats } from '../blitz/getAccountInfo';
+import { ReadStream, WriteStream } from './buffer';
+
+export interface BlitzkriegTankStats extends AllStats {
+ id: number;
+ battle_life_time: number;
+ last_battle_time: number;
+ mark_of_mastery: number;
+}
+
+export class RtsmReadStream extends ReadStream {
+ rtsm() {
+ this.magic();
+ this.header();
+ return this.body();
+ }
+
+ magic() {
+ const magic = this.utf8(4);
+
+ if (magic !== 'RTSM') {
+ throw new Error(`Invalid RTSM magic number: "${magic}"`);
+ }
+ }
+
+ header() {
+ return {
+ version: this.uint16(),
+ };
+ }
+
+ body() {
+ return {
+ time: this.uint32(),
+ base: this.uint8(),
+ };
+ }
+}
+
+export class RtsmWriteStream extends WriteStream {
+ rtsm(base: number) {
+ this.magic();
+ this.header();
+ this.body(base);
+
+ return this;
+ }
+
+ magic() {
+ this.utf8('RTSM');
+ }
+
+ header() {
+ this.uint16(1);
+ }
+
+ body(base: number) {
+ this.uint32(Math.round(Date.now() / 1000));
+ this.uint8(base);
+ }
+}
+
+export class RtscReadStream extends ReadStream {
+ rtsc() {
+ this.magic();
+ const header = this.header();
+ return this.body(header.playerCount);
+ }
+
+ magic() {
+ const magic = this.utf8(4);
+
+ if (magic !== 'RTSC') {
+ throw new Error(`Invalid RTSC magic number: "${magic}"`);
+ }
+ }
+
+ header() {
+ return {
+ version: this.uint16(),
+ playerCount: this.uint32(),
+ };
+ }
+
+ body(playerCount: number) {
+ return times(playerCount, () => this.player());
+ }
+
+ player() {
+ const id = this.uint32();
+ const tankCount = this.uint16();
+ const tanks = times(tankCount, () => this.tank());
+
+ return {
+ id,
+ tankCount,
+ tanks,
+ };
+ }
+
+ tank() {
+ return {
+ id: this.uint32(),
+ battle_life_time: this.uint32(),
+ last_battle_time: this.uint32(),
+ mark_of_mastery: this.uint8(),
+ battles: this.uint32(),
+ capture_points: this.uint32(),
+ damage_dealt: this.uint32(),
+ damage_received: this.uint32(),
+ dropped_capture_points: this.uint32(),
+ frags: this.uint32(),
+ frags8p: this.uint32(),
+ hits: this.uint32(),
+ losses: this.uint32(),
+ max_frags: this.uint8(),
+ max_xp: this.uint16(),
+ shots: this.uint32(),
+ spotted: this.uint32(),
+ survived_battles: this.uint32(),
+ win_and_survived: this.uint32(),
+ wins: this.uint32(),
+ xp: this.uint32(),
+ } satisfies BlitzkriegTankStats;
+ }
+}
+
+export interface RtscWritePlayer {
+ id: number;
+ tanks: IndividualTankStats[];
+}
+
+export class RtscWriteStream extends WriteStream {
+ rtsc(players: RtscWritePlayer[]) {
+ this.magic();
+ this.header(players.length);
+ this.body(players);
+
+ return this;
+ }
+
+ magic() {
+ this.utf8('RTSC');
+ }
+
+ header(playerCount: number) {
+ this.uint16(1);
+ this.uint32(playerCount);
+ }
+
+ body(players: RtscWritePlayer[]) {
+ players.forEach((player) => {
+ this.uint32(player.id);
+ this.uint16(player.tanks.length);
+
+ player.tanks.forEach((tank) => {
+ this.uint32(tank.tank_id);
+ this.uint32(tank.battle_life_time);
+ this.uint32(tank.last_battle_time);
+ this.uint8(tank.mark_of_mastery);
+ this.uint32(tank.all.battles);
+ this.uint32(tank.all.capture_points);
+ this.uint32(tank.all.damage_dealt);
+ this.uint32(tank.all.damage_received);
+ this.uint32(tank.all.dropped_capture_points);
+ this.uint32(tank.all.frags);
+ this.uint32(tank.all.frags8p);
+ this.uint32(tank.all.hits);
+ this.uint32(tank.all.losses);
+ this.uint8(tank.all.max_frags);
+ this.uint16(tank.all.max_xp);
+ this.uint32(tank.all.shots);
+ this.uint32(tank.all.spotted);
+ this.uint32(tank.all.survived_battles);
+ this.uint32(tank.all.win_and_survived);
+ this.uint32(tank.all.wins);
+ this.uint32(tank.all.xp);
+ });
+ });
+ }
+}
diff --git a/src/lang/en-US.json b/src/lang/en-US.json
index 3c1f6ab93..ae0712ae0 100644
--- a/src/lang/en-US.json
+++ b/src/lang/en-US.json
@@ -62,6 +62,18 @@
"bot": {
"common": {
+ "errors": {
+ "clan_not_found": "# Could not find clan\nCouldn't find \"%s\". Try selecting a clan from the search result.",
+ "player_not_found": "# Could not find user\nI couldn't find user \"%s\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
+ "player_not_linked": "# I don't know who you are\nUse the `/link` command to get started.",
+ "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"%s\".",
+ "tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"%s\". Try typing the name of the tank instead of a number.",
+ "no_tank_stats": "# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.",
+ "get_help": "Help Discord server",
+ "uncaught_error": "# Blitzkrieg ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.",
+ "preview": "# This command is in preview\n\nYou can only access this command in [the official Discord server](https://discord.gg/nDt7AjGJQH). The command will be generally available soon."
+ },
+
"wn8": {
"$": "WN8",
"very_bad": "Very bad",
@@ -183,18 +195,6 @@
"refresh": "Refresh"
},
- "errors": {
- "clan_not_found": "# Could not find clan\nCouldn't find \"%s\". Try selecting a clan from the search result.",
- "player_not_found": "# Could not find user\nI couldn't find user \"%s\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
- "player_not_linked": "# I don't know who you are\nUse the `/link` command to get started.",
- "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"%s\".",
- "tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"%s\". Try typing the name of the tank instead of a number.",
- "no_tank_stats": "# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.",
- "get_help": "Help Discord server",
- "uncaught_error": "# Blitzkrieg ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.",
- "preview": "# This command is in preview\n\nYou can only access this command in [the official Discord server](https://discord.gg/nDt7AjGJQH). The command will be generally available soon."
- },
-
"hero_stats": {
"battles": "Battles",
"winrate": "Winrate",
diff --git a/src/types/tanksStats.ts b/src/types/tanksStats.ts
index 70aa4986d..73c73fda9 100644
--- a/src/types/tanksStats.ts
+++ b/src/types/tanksStats.ts
@@ -15,7 +15,7 @@ export interface IndividualTankStats {
}
export interface TanksStats {
- [accountId: number]: IndividualTankStats[];
+ [accountId: number]: null | IndividualTankStats[];
}
export interface NormalizedTankStats {