mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: migration 4
This commit is contained in:
1 parent
4055988478
commit
135dd85a82
26 files changed
+1
-19
No files matched your search
@@ -0,0 +1,78 @@
|
||||
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 { readStringDVPL } from '../src/core/blitz/readStringDVPL';
|
||||
import { readYAMLDVPL } from '../src/core/blitz/readYAMLDVPL';
|
||||
import { writeDVPL } from '../src/core/blitz/writeDVPL';
|
||||
import { assertSecret } from '../src/core/blitzkit/secret';
|
||||
import { dvp } from '../submodules/blitzkit-closed/src/dvp';
|
||||
import { DATA } from './buildAssets/constants';
|
||||
|
||||
const versionTextFile = await readStringDVPL(`${DATA}/version.txt.dvpl`);
|
||||
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(process.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(process.env.WOTB_DLC_CDN)}/dlc/${data.dx11}`,
|
||||
).then((response) => response.arrayBuffer());
|
||||
const dvpd = await fetch(
|
||||
`${assertSecret(process.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,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
files.map(async ({ path, data }) => {
|
||||
const { dir } = parsePath(path);
|
||||
|
||||
await mkdir(`${DATA}/${dir}`, { recursive: true });
|
||||
await writeFile(
|
||||
`${DATA}/${path}.dvpl`,
|
||||
new Uint8Array(writeDVPL(Buffer.from(data))),
|
||||
);
|
||||
|
||||
bar.tick();
|
||||
}),
|
||||
);
|
||||
|
||||
if ('dynamicContentLocalizationsDir' in data) {
|
||||
console.log('Found dynamic content localizations; patching...');
|
||||
|
||||
const localizationsResponse = await fetch(
|
||||
`${assertSecret(process.env.WOTB_DLC_CDN)}/dlc/${data.dynamicContentLocalizationsDir}/en.yaml`,
|
||||
);
|
||||
const newStrings = parseYaml(await localizationsResponse.text());
|
||||
const oldStrings = await readYAMLDVPL<Record<string, string>>(
|
||||
`${DATA}/Strings/en.yaml.dvpl`,
|
||||
);
|
||||
const patchedStrings = { ...oldStrings, ...newStrings };
|
||||
const patchedContent = stringifyYaml(patchedStrings);
|
||||
|
||||
await writeFile(
|
||||
`${DATA}/Strings/en.yaml.dvpl`,
|
||||
new Uint8Array(writeDVPL(Buffer.from(patchedContent))),
|
||||
);
|
||||
}
|
||||
|
||||
patchIndex++;
|
||||
} else break;
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { chunk, times } from 'lodash';
|
||||
import { Region, REGIONS } from '../src/constants/regions';
|
||||
import { getAccountInfo } from '../src/core/blitz/getAccountInfo';
|
||||
import getTankStats from '../src/core/blitz/getTankStats';
|
||||
import { idToRegion } from '../src/core/blitz/idToRegion';
|
||||
import {
|
||||
AverageDefinitions,
|
||||
AverageDefinitionsEntry,
|
||||
AverageDefinitionsEntrySubPartial,
|
||||
AverageDefinitionsManifest,
|
||||
Samples,
|
||||
} from '../src/core/blitzkit/averageDefinitions';
|
||||
import {
|
||||
averageDefinitionsAllStatsKeys,
|
||||
emptySamples,
|
||||
} from '../src/core/blitzkit/averageDefinitions/constants';
|
||||
import { commitAssets } from '../src/core/blitzkit/commitAssets';
|
||||
import { fetchPreDiscoveredIds } from '../src/core/blitzkit/fetchPreDiscoveredIds';
|
||||
import { encodeToBase64 } from '../src/core/protobuf/encodeToBase64';
|
||||
import { IndividualTankStats } from '../src/types/tanksStats';
|
||||
|
||||
interface DataPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
}
|
||||
|
||||
const MINUTE = 60 * 1000;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
// const MAX_ACTIVITY_TIME = 120 * DAY;
|
||||
// const MIN_BATTLES = 5000;
|
||||
const RUN_TIME = 5 * HOUR + 55 * MINUTE;
|
||||
const THREADS = 10;
|
||||
const PLAYER_IDS_PER_CALL = 100;
|
||||
|
||||
const startTime = Date.now();
|
||||
const preDiscoveredIds = await fetchPreDiscoveredIds();
|
||||
const playerIds: Record<Region, number[][]> = {
|
||||
asia: chunk(
|
||||
preDiscoveredIds.filter((id) => idToRegion(id) === 'asia'),
|
||||
PLAYER_IDS_PER_CALL,
|
||||
),
|
||||
com: chunk(
|
||||
preDiscoveredIds.filter((id) => idToRegion(id) === 'com'),
|
||||
PLAYER_IDS_PER_CALL,
|
||||
),
|
||||
eu: chunk(
|
||||
preDiscoveredIds.filter((id) => idToRegion(id) === 'eu'),
|
||||
PLAYER_IDS_PER_CALL,
|
||||
),
|
||||
};
|
||||
let regionIndex = 0;
|
||||
const samples: Samples = { ...emptySamples };
|
||||
const availableRegions = [...REGIONS];
|
||||
const tankIds: number[] = [];
|
||||
const tanksSorted: Record<number, IndividualTankStats[]> = {};
|
||||
let postWorkRequested = false;
|
||||
|
||||
times(THREADS, async () => {
|
||||
while (availableRegions.length > 0 && startTime + RUN_TIME > Date.now()) {
|
||||
const region = availableRegions[regionIndex];
|
||||
const regionIds = playerIds[region];
|
||||
const [ids] = regionIds.splice(
|
||||
Math.floor(Math.random() * regionIds.length),
|
||||
1,
|
||||
);
|
||||
const accountInfo = await getAccountInfo(region, ids, undefined, {
|
||||
fields: 'last_battle_time,statistics.all.battles',
|
||||
});
|
||||
|
||||
const filteredIds = ids.filter((_, index) => {
|
||||
const info = accountInfo[index];
|
||||
|
||||
if (info === null) return false;
|
||||
|
||||
const timeSinceLastActivity = Date.now() - info.last_battle_time * 1000;
|
||||
|
||||
samples.total++;
|
||||
if (timeSinceLastActivity <= 120 * DAY) samples.d_120++;
|
||||
if (timeSinceLastActivity <= 90 * DAY) samples.d_90++;
|
||||
if (timeSinceLastActivity <= 60 * DAY) samples.d_60++;
|
||||
if (timeSinceLastActivity <= 30 * DAY) samples.d_30++;
|
||||
if (timeSinceLastActivity <= 7 * DAY) samples.d_7++;
|
||||
if (timeSinceLastActivity <= 1 * DAY) samples.d_1++;
|
||||
|
||||
return true;
|
||||
|
||||
// return (
|
||||
// timeSinceLastActivity <= MAX_ACTIVITY_TIME &&
|
||||
// info.statistics.all.battles > MIN_BATTLES
|
||||
// );
|
||||
});
|
||||
|
||||
const players = await Promise.all(
|
||||
filteredIds.map((id) =>
|
||||
getTankStats(region, id, {
|
||||
fields: 'last_battle_time,battle_life_time,all,tank_id',
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
players.forEach((tanks) => {
|
||||
tanks?.forEach((tank) => {
|
||||
if (tank.all.battles === 0) return;
|
||||
|
||||
if (!tankIds.includes(tank.tank_id)) {
|
||||
console.log(
|
||||
`Found new tank: ${tank.tank_id} (${tankIds.length + 1})`,
|
||||
);
|
||||
tankIds.push(tank.tank_id);
|
||||
tanksSorted[tank.tank_id] = [];
|
||||
}
|
||||
|
||||
tanksSorted[tank.tank_id].push(tank);
|
||||
});
|
||||
});
|
||||
|
||||
if (regionIds.length === 0 && availableRegions.includes(region)) {
|
||||
availableRegions.splice(availableRegions.indexOf(region), 1);
|
||||
}
|
||||
regionIndex = (regionIndex + 1) % availableRegions.length;
|
||||
}
|
||||
|
||||
if (!postWorkRequested) {
|
||||
postWorkRequested = true;
|
||||
postWork();
|
||||
}
|
||||
});
|
||||
|
||||
async function postWork() {
|
||||
console.log(
|
||||
`Generating statistics based on ${samples.d_120.toLocaleString()} players (${samples.total.toLocaleString()} checked in total) and ${tankIds.length} tanks...`,
|
||||
);
|
||||
|
||||
const averages: Record<number, AverageDefinitionsEntry> = {};
|
||||
|
||||
tankIds.forEach((id) => {
|
||||
const tanksSortedEntry = tanksSorted[id];
|
||||
// const tanks = tanksSortedEntry.filter(
|
||||
// (tank) => Date.now() - tank.last_battle_time * 1000 <= MAX_ACTIVITY_TIME,
|
||||
// );
|
||||
const tanks = tanksSortedEntry;
|
||||
const entry: AverageDefinitionsEntrySubPartial = {
|
||||
mu: {},
|
||||
sigma: {},
|
||||
r: {},
|
||||
samples: { ...emptySamples },
|
||||
};
|
||||
|
||||
tanks.forEach((tank) => {
|
||||
const timeSinceLastActivity = Date.now() - tank.last_battle_time * 1000;
|
||||
|
||||
entry.samples.total++;
|
||||
if (timeSinceLastActivity <= 120 * DAY) entry.samples.d_120++;
|
||||
if (timeSinceLastActivity <= 90 * DAY) entry.samples.d_90++;
|
||||
if (timeSinceLastActivity <= 60 * DAY) entry.samples.d_60++;
|
||||
if (timeSinceLastActivity <= 30 * DAY) entry.samples.d_30++;
|
||||
if (timeSinceLastActivity <= 7 * DAY) entry.samples.d_7++;
|
||||
if (timeSinceLastActivity <= 1 * DAY) entry.samples.d_1++;
|
||||
});
|
||||
|
||||
const dataWY = tanks.map((tank) => ({
|
||||
w: tank.all.battles,
|
||||
y: tank.all.wins / tank.all.battles,
|
||||
}));
|
||||
|
||||
averageDefinitionsAllStatsKeys.forEach((key) => {
|
||||
const data: DataPoint[] = dataWY.map(({ w, y }, index) => ({
|
||||
w,
|
||||
x:
|
||||
(key === 'battle_life_time'
|
||||
? tanks[index].battle_life_time
|
||||
: tanks[index].all[key]) / w,
|
||||
y,
|
||||
}));
|
||||
|
||||
if (data.length === 0) return;
|
||||
|
||||
function sum(slicer: (data: DataPoint) => number) {
|
||||
return data.reduce(
|
||||
(accumulator, data) => accumulator + slicer(data),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
const sum_w = sum(({ w }) => w);
|
||||
const sum_wx = sum(({ w, x }) => w * x);
|
||||
const sum_wy = sum(({ w, y }) => w * y);
|
||||
const x_bar = sum_wx / sum_w;
|
||||
const y_bar = sum_wy / sum_w;
|
||||
// undo normalization for mu to get a simple average
|
||||
const mu = sum(({ w, x }) => w * x) / samples.d_120;
|
||||
|
||||
const sigma_numerator = sum(({ w, x }) => w * (x - x_bar) ** 2);
|
||||
const sigma = Math.sqrt(sigma_numerator / sum_w);
|
||||
|
||||
const r_numerator = sum(({ w, x, y }) => w * (x - x_bar) * (y - y_bar));
|
||||
const r_denominator_x = sum(({ w, x }) => w * (x - x_bar) ** 2);
|
||||
const r_denominator_y = sum(({ w, y }) => w * (y - y_bar) ** 2);
|
||||
const r = r_numerator / Math.sqrt(r_denominator_x * r_denominator_y);
|
||||
|
||||
entry.mu[key] = mu;
|
||||
entry.sigma[key] = sigma;
|
||||
entry.r[key] = isNaN(r) ? 0 : r;
|
||||
});
|
||||
|
||||
averages[id] = entry as AverageDefinitionsEntry;
|
||||
});
|
||||
|
||||
const time = Date.now();
|
||||
const latest = Math.round(time / DAY);
|
||||
const averageDefinitions = {
|
||||
averages,
|
||||
samples,
|
||||
time,
|
||||
} satisfies AverageDefinitions;
|
||||
|
||||
const manifest: AverageDefinitionsManifest = {
|
||||
version: 1,
|
||||
latest,
|
||||
};
|
||||
|
||||
commitAssets('averages', [
|
||||
{
|
||||
path: `averages/${latest}.pb`,
|
||||
content: await encodeToBase64(
|
||||
'average_definitions',
|
||||
'blitzkit.AverageDefinitions',
|
||||
averageDefinitions,
|
||||
),
|
||||
encoding: 'base64',
|
||||
},
|
||||
{
|
||||
path: 'averages/manifest.json',
|
||||
content: JSON.stringify(manifest),
|
||||
encoding: 'utf-8',
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
|
||||
export async function boosterIcons() {
|
||||
const boosterFiles = (await readdir(`${DATA}/Gfx/Shared/boosters`)).filter(
|
||||
(file) => !file.endsWith('@2x.txt.dvpl') && !file.startsWith('texture0'),
|
||||
);
|
||||
const image = sharp(
|
||||
await readDVPLFile(`${DATA}/Gfx/Shared/boosters/texture0.packed.webp.dvpl`),
|
||||
);
|
||||
const changes: FileChange[] = [];
|
||||
|
||||
await Promise.all(
|
||||
boosterFiles.map(async (file) => {
|
||||
const name = file.match(/booster_(.+).txt.dvpl/)?.[1];
|
||||
|
||||
if (name === undefined) return;
|
||||
|
||||
const sizes = (
|
||||
await readStringDVPL(`${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()
|
||||
).toString('base64');
|
||||
|
||||
changes.push({
|
||||
path: `icons/boosters/${name}.webp`,
|
||||
content,
|
||||
encoding: 'base64',
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await commitAssets('booster icons', changes);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
|
||||
export async function camouflageIcons() {
|
||||
console.log('Building camouflage icons...');
|
||||
const content = await readDVPLFile(
|
||||
`${DATA}/Gfx/UI/Hangar/IconCamouflage.packed.webp.dvpl`,
|
||||
).then((content) => content.toString('base64'));
|
||||
const changes: FileChange[] = [
|
||||
{
|
||||
content,
|
||||
encoding: 'base64',
|
||||
path: 'icons/camo.webp',
|
||||
},
|
||||
];
|
||||
|
||||
await commitAssets('camo icons', changes);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import { argv } from 'process';
|
||||
|
||||
const isDepot = argv.includes('--depot');
|
||||
|
||||
export const DATA = isDepot
|
||||
? await (async () => {
|
||||
const [installationVersion] = await readdir(`depots/444202`);
|
||||
return `depots/444202/${installationVersion}/Data`;
|
||||
})()
|
||||
: 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data';
|
||||
@@ -0,0 +1,192 @@
|
||||
import { existsSync } from 'fs';
|
||||
import sharp from 'sharp';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
import { ConsumablesCommon, ProvisionsCommon } from './definitions';
|
||||
|
||||
interface Mappings {
|
||||
Header: { version: number };
|
||||
StyleSheets: {
|
||||
selectors: string[];
|
||||
properties: { 'bg-sprite': string };
|
||||
}[];
|
||||
}
|
||||
|
||||
const listItemsPattern = /<items path="(.+)\.xml"\/>/g;
|
||||
|
||||
export async function consumableProvisionIcons() {
|
||||
console.log('Building consumable and provision icons...');
|
||||
|
||||
const changes: FileChange[] = [];
|
||||
|
||||
const styles = [
|
||||
'UI/Styles/Lobby/Inventory/Event/InventoryBigStyles.yaml',
|
||||
'UI/Styles/Lobby/Inventory/Event/InventoryNormalStyles.yaml',
|
||||
'UI/Styles/Lobby/Inventory/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 readYAMLDVPL<Mappings>(`${DATA}/${path}.dvpl`)).StyleSheets,
|
||||
),
|
||||
),
|
||||
).then((array) => array.flat());
|
||||
|
||||
for (const match of (
|
||||
await readStringDVPL(
|
||||
`${DATA}/XML/item_defs/vehicles/common/consumables/list.xml.dvpl`,
|
||||
)
|
||||
).matchAll(listItemsPattern)) {
|
||||
const consumablesCommon = await readXMLDVPL<{
|
||||
root: ConsumablesCommon;
|
||||
}>(
|
||||
`${DATA}/XML/item_defs/vehicles/common/consumables/${match[1]}.xml.dvpl`,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.values(consumablesCommon.root).map(async (consumable) => {
|
||||
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...`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const configPath = styleSheet.properties['bg-sprite']
|
||||
.replace('~res:/', '')
|
||||
.replace('.psd', '')
|
||||
.replace('.txt', '');
|
||||
|
||||
if (existsSync(`${DATA}/${configPath}.packed.webp.dvpl`)) {
|
||||
const image = sharp(
|
||||
await readDVPLFile(`${DATA}/${configPath}.packed.webp.dvpl`),
|
||||
);
|
||||
const content = (
|
||||
await image.trim({ threshold: 100 }).toBuffer()
|
||||
).toString('base64');
|
||||
|
||||
changes.push({
|
||||
path: `icons/consumables/${consumable.id}.webp`,
|
||||
encoding: 'base64',
|
||||
content,
|
||||
});
|
||||
} else {
|
||||
const consumablesTexture = sharp(
|
||||
await readDVPLFile(
|
||||
`${`${DATA}/${configPath}`.split('/').slice(0, -1).join('/')}/texture0.packed.webp.dvpl`,
|
||||
),
|
||||
);
|
||||
const sizes = (await readStringDVPL(`${DATA}/${configPath}.txt.dvpl`))
|
||||
.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()
|
||||
).toString('base64');
|
||||
|
||||
changes.push({
|
||||
path: `icons/consumables/${consumable.id}.webp`,
|
||||
encoding: 'base64',
|
||||
content,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (const match of (
|
||||
await readStringDVPL(
|
||||
`${DATA}/XML/item_defs/vehicles/common/provisions/list.xml.dvpl`,
|
||||
)
|
||||
).matchAll(listItemsPattern)) {
|
||||
const provisionsCommon = await readXMLDVPL<{
|
||||
root: ProvisionsCommon;
|
||||
}>(`${DATA}/XML/item_defs/vehicles/common/provisions/${match[1]}.xml.dvpl`);
|
||||
await Promise.all(
|
||||
Object.values(provisionsCommon.root).map(async (provision) => {
|
||||
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...`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const configPath = styleSheet.properties['bg-sprite']
|
||||
.replace('~res:/', '')
|
||||
.replace('.psd', '')
|
||||
.replace('.txt', '');
|
||||
|
||||
if (existsSync(`${DATA}/${configPath}.packed.webp.dvpl`)) {
|
||||
const image = sharp(
|
||||
await readDVPLFile(`${DATA}/${configPath}.packed.webp.dvpl`),
|
||||
);
|
||||
const content = (
|
||||
await image.trim({ threshold: 100 }).toBuffer()
|
||||
).toString('base64');
|
||||
|
||||
changes.push({
|
||||
path: `icons/provisions/${provision.id}.webp`,
|
||||
encoding: 'base64',
|
||||
content,
|
||||
});
|
||||
} else {
|
||||
const provisionsTexture = sharp(
|
||||
await readDVPLFile(
|
||||
`${`${DATA}/${configPath}`.split('/').slice(0, -1).join('/')}/texture0.packed.webp.dvpl`,
|
||||
),
|
||||
);
|
||||
const sizes = (await readStringDVPL(`${DATA}/${configPath}.txt.dvpl`))
|
||||
.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()
|
||||
).toString('base64');
|
||||
|
||||
changes.push({
|
||||
path: `icons/provisions/${provision.id}.webp`,
|
||||
encoding: 'base64',
|
||||
content,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await commitAssets('consumable and provision icons', changes);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import sharp from 'sharp';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { assertSecret } from '../../src/core/blitzkit/secret';
|
||||
import { DATA } 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'],
|
||||
];
|
||||
|
||||
type BlitzGlossary = Record<
|
||||
string,
|
||||
{
|
||||
image_url: null | string;
|
||||
}
|
||||
>;
|
||||
|
||||
export async function currencies() {
|
||||
console.log('Building currency icons...');
|
||||
|
||||
const changes: FileChange[] = [];
|
||||
|
||||
await Promise.all(
|
||||
ICONS.map(async ([file, name]) => {
|
||||
const content = (
|
||||
await sharp(
|
||||
await readDVPLFile(`${DATA}/Gfx/Lobby/currency/${file}.dvpl`),
|
||||
)
|
||||
.trim()
|
||||
.toBuffer()
|
||||
).toString('base64');
|
||||
|
||||
changes.push({
|
||||
content,
|
||||
encoding: 'base64',
|
||||
path: `icons/currencies/${name}.webp`,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const glossary = await fetch(assertSecret(process.env.WOTB_GLOSSARY)).then(
|
||||
(response) => response.json() as Promise<BlitzGlossary>,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(glossary)
|
||||
.filter(([key]) => /^prx_season_\d+$/.test(key))
|
||||
.map(async ([key, item]) => {
|
||||
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()
|
||||
).toString('base64');
|
||||
|
||||
changes.push({
|
||||
content,
|
||||
encoding: 'base64',
|
||||
path: `icons/currencies/${key}.webp`,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await commitAssets('currency icons', changes);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,95 @@
|
||||
import sharp from 'sharp';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } 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...');
|
||||
|
||||
const changes: FileChange[] = [];
|
||||
const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>(
|
||||
`${DATA}/XML/item_defs/vehicles/common/optional_devices.xml.dvpl`,
|
||||
);
|
||||
const mappings = await readYAMLDVPL<Mappings>(
|
||||
`${DATA}/UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml.dvpl`,
|
||||
);
|
||||
const image = sharp(
|
||||
await readDVPLFile(
|
||||
`${DATA}/Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp.dvpl`,
|
||||
),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(optionalDevices.root).map(
|
||||
async ([optionalDeviceKey, optionalDevice]) => {
|
||||
if (optionalDeviceKey === 'nextAvailableId') return;
|
||||
|
||||
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...`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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 readDVPLFile(`${DATA}/${configPathWebp}.packed.webp.dvpl`),
|
||||
);
|
||||
const content = (await image.trim().toBuffer()).toString('base64');
|
||||
|
||||
changes.push({
|
||||
path: `icons/equipment/${optionalDevice.id}.webp`,
|
||||
encoding: 'base64',
|
||||
content,
|
||||
});
|
||||
} else {
|
||||
const config = await readStringDVPL(`${DATA}/${configPath}.dvpl`);
|
||||
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()
|
||||
).toString('base64');
|
||||
|
||||
changes.push({
|
||||
path: `icons/equipment/${optionalDevice.id}.webp`,
|
||||
encoding: 'base64',
|
||||
content,
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await commitAssets('equipment icons', changes);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
|
||||
export async function flags() {
|
||||
console.log('Building flags...');
|
||||
|
||||
const changes = [
|
||||
...(await Promise.all(
|
||||
(await readdir(`${DATA}/Gfx/Lobby/flags`))
|
||||
.filter(
|
||||
(flag) =>
|
||||
flag.startsWith('flag_profile-stat_') &&
|
||||
!flag.endsWith('@2x.packed.webp.dvpl'),
|
||||
)
|
||||
.map(async (flag) => {
|
||||
const image = sharp(
|
||||
await readDVPLFile(`${DATA}/Gfx/Lobby/flags/${flag}`),
|
||||
);
|
||||
const content = (
|
||||
await image.trim({ threshold: 100 }).toBuffer()
|
||||
).toString('base64');
|
||||
const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1];
|
||||
|
||||
return {
|
||||
content,
|
||||
encoding: 'base64',
|
||||
path: `flags/circle/${name}.webp`,
|
||||
} satisfies FileChange;
|
||||
}),
|
||||
)),
|
||||
...(await Promise.all(
|
||||
(await readdir(`${DATA}/Gfx/Lobby/flags`))
|
||||
.filter(
|
||||
(flag) =>
|
||||
flag.startsWith('flag_tutor-tank_') &&
|
||||
!flag.endsWith('@2x.packed.webp.dvpl'),
|
||||
)
|
||||
.map(async (flag) => {
|
||||
const content = await readBase64DVPL(
|
||||
`${DATA}/Gfx/Lobby/flags/${flag}`,
|
||||
);
|
||||
const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1];
|
||||
|
||||
return {
|
||||
content,
|
||||
encoding: 'base64',
|
||||
path: `flags/scratched/${name}.webp`,
|
||||
} satisfies FileChange;
|
||||
}),
|
||||
)),
|
||||
...(await Promise.all(
|
||||
(await readdir(`${DATA}/Gfx/Lobby/flags`))
|
||||
.filter(
|
||||
(flag) =>
|
||||
flag.startsWith('flag_filter_') &&
|
||||
flag.endsWith('@2x.packed.webp.dvpl'),
|
||||
)
|
||||
.map(async (flag) => {
|
||||
const content = (
|
||||
await sharp(await readDVPLFile(`${DATA}/Gfx/Lobby/flags/${flag}`))
|
||||
.trim({
|
||||
threshold: 100,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
})
|
||||
.toBuffer()
|
||||
).toString('base64');
|
||||
const name = flag.match(/flag_filter_(.+)@2x\.packed\.webp/)![1];
|
||||
|
||||
return {
|
||||
content,
|
||||
encoding: 'base64',
|
||||
path: `flags/fade_small/${name}.webp`,
|
||||
} satisfies FileChange;
|
||||
}),
|
||||
)),
|
||||
];
|
||||
|
||||
await commitAssets('flags', changes);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import sharp from 'sharp';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
import { SquadBattleTypeStyles } from './definitions';
|
||||
|
||||
export async function gameModeBanners() {
|
||||
console.log('Building game mode banners...');
|
||||
|
||||
const changes: FileChange[] = [];
|
||||
const gameTypeSelectorStyles = await readYAMLDVPL<SquadBattleTypeStyles>(
|
||||
`${DATA}/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml.dvpl`,
|
||||
);
|
||||
const squadBattleTypeStyles = await readYAMLDVPL<SquadBattleTypeStyles>(
|
||||
`${DATA}/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml.dvpl`,
|
||||
);
|
||||
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 readDVPLFile(`${DATA}${path}.packed.webp.dvpl`))
|
||||
.trim({ background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.toBuffer()
|
||||
).toString('base64');
|
||||
|
||||
changes.push({
|
||||
path: `icons/game_mode_banners/${id}.webp`,
|
||||
content,
|
||||
encoding: 'base64',
|
||||
});
|
||||
}
|
||||
|
||||
await commitAssets('game mode banners', changes);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { config } from 'dotenv';
|
||||
import { argv } from 'process';
|
||||
|
||||
config();
|
||||
|
||||
const allTargets = argv.includes('--all-targets');
|
||||
const targets = argv
|
||||
.find((argument) => argument.startsWith('--target'))
|
||||
?.split('=')[1]
|
||||
.split(',');
|
||||
|
||||
import { boosterIcons } from './boosterIcons';
|
||||
import { camouflageIcons } from './camouflageIcons';
|
||||
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 { moduleIcons } from './moduleIcons';
|
||||
import { shellIcons } from './shellIcons';
|
||||
import { skillIcons } from './skillIcons';
|
||||
import { tankArmors } from './tankArmors';
|
||||
import { tankIcons } from './tankIcons';
|
||||
import { tankModels } from './tankModels';
|
||||
|
||||
if (!targets && !allTargets) throw new Error('No target(s) specified');
|
||||
|
||||
const methods = [
|
||||
tankModels,
|
||||
tankArmors,
|
||||
gameModeBanners,
|
||||
definitions,
|
||||
tankIcons,
|
||||
currencies,
|
||||
skillIcons,
|
||||
flags,
|
||||
shellIcons,
|
||||
moduleIcons,
|
||||
equipmentIcons,
|
||||
consumableProvisionIcons,
|
||||
camouflageIcons,
|
||||
boosterIcons,
|
||||
];
|
||||
|
||||
for (const method of methods) {
|
||||
if (allTargets || targets?.includes(method.name)) {
|
||||
try {
|
||||
await method();
|
||||
} catch (error) {
|
||||
console.warn(`Failed method ${method.name}; skipping...`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
|
||||
export async function moduleIcons() {
|
||||
console.log('Building module icons...');
|
||||
|
||||
const changes = await Promise.all(
|
||||
(await readdir(`${DATA}/Gfx/UI/ModulesTechTree`))
|
||||
.filter(
|
||||
(file) =>
|
||||
!file.endsWith('@2x.packed.webp.dvpl') && file.startsWith('vehicle'),
|
||||
)
|
||||
.map(async (file) => {
|
||||
const nameRaw = file.match(/vehicle(.+)\.packed\.webp\.dvpl/)![1];
|
||||
const name = nameRaw[0].toLowerCase() + nameRaw.slice(1);
|
||||
const content = (
|
||||
await sharp(
|
||||
await readDVPLFile(`${DATA}/Gfx/UI/ModulesTechTree/${file}`),
|
||||
)
|
||||
.trim()
|
||||
.toBuffer()
|
||||
).toString('base64');
|
||||
|
||||
return {
|
||||
content,
|
||||
path: `icons/modules/${name}.webp`,
|
||||
encoding: 'base64',
|
||||
} satisfies FileChange;
|
||||
}),
|
||||
);
|
||||
|
||||
await commitAssets('module icons', changes);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
|
||||
export async function shellIcons() {
|
||||
console.log('Building shell icons...');
|
||||
|
||||
const image = sharp(
|
||||
await readDVPLFile(
|
||||
`${DATA}/Gfx/Shared/tank-supply/ammunition/big/texture0.packed.webp.dvpl`,
|
||||
),
|
||||
);
|
||||
|
||||
const changes = await Promise.all(
|
||||
(await readdir(`${DATA}/Gfx/Shared/tank-supply/ammunition/big`))
|
||||
.filter((file) => file.endsWith('_l.txt.dvpl'))
|
||||
.map(async (file) => {
|
||||
const name = file.match(/(.+)_l\.txt\.dvpl/)![1];
|
||||
const sizes = (
|
||||
await readStringDVPL(
|
||||
`${DATA}/Gfx/Shared/tank-supply/ammunition/big/${file}`,
|
||||
)
|
||||
)
|
||||
.split('\n')[4]
|
||||
.split(' ')
|
||||
.map(Number);
|
||||
|
||||
return {
|
||||
content: (
|
||||
await image
|
||||
.clone()
|
||||
.extract({
|
||||
left: sizes[0],
|
||||
top: sizes[1],
|
||||
width: sizes[2],
|
||||
height: sizes[3],
|
||||
})
|
||||
.toBuffer()
|
||||
).toString('base64'),
|
||||
encoding: 'base64',
|
||||
path: `icons/shells/${name}.webp`,
|
||||
} satisfies FileChange;
|
||||
}),
|
||||
);
|
||||
|
||||
await commitAssets('shell icons', changes);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import sharp from 'sharp';
|
||||
import { TankClass } from '../../src/components/Tanks';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
|
||||
interface SkillIcon {
|
||||
name: string;
|
||||
state: number;
|
||||
}
|
||||
|
||||
export interface Avatar {
|
||||
roles: unknown;
|
||||
skillsByClasses: Record<TankClass, string>;
|
||||
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 readXMLDVPL<{ root: Avatar }>(
|
||||
`${DATA}/XML/item_defs/tankmen/avatar.xml.dvpl`,
|
||||
);
|
||||
const changes = await Promise.all(
|
||||
Object.values(avatar.root.skills).map(async (skill) => {
|
||||
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.dvpl`;
|
||||
const image = sharp(await readDVPLFile(path)).trim();
|
||||
const content = (await image.toBuffer()).toString('base64');
|
||||
|
||||
return {
|
||||
content,
|
||||
encoding: 'base64',
|
||||
path: `icons/skills/${name}.webp`,
|
||||
} satisfies FileChange;
|
||||
}),
|
||||
);
|
||||
|
||||
commitAssets('skill icons', changes);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NodeIO } from '@gltf-transform/core';
|
||||
import { readdir } from 'fs/promises';
|
||||
import { extractArmor } from '../../src/core/blitz/extractArmor';
|
||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
import { VehicleDefinitionList } from './definitions';
|
||||
|
||||
export async function tankArmors() {
|
||||
console.log('Building tank armors...');
|
||||
|
||||
const changes: FileChange[] = [];
|
||||
const nodeIO = new NodeIO();
|
||||
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml.dvpl`,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(tanks.root).map(async ([tankKey, tank]) => {
|
||||
if (tankKey.includes('tutorial_bot')) return;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
const model = await extractArmor(DATA, `${nation}-${tankKey}`);
|
||||
|
||||
changes.push({
|
||||
path: `3d/tanks/armor/${id}.glb`,
|
||||
encoding: 'base64',
|
||||
content: Buffer.from(await nodeIO.writeBinary(model)).toString(
|
||||
'base64',
|
||||
),
|
||||
});
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await commitAssets('tank armor', changes);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { Vector3Tuple } from 'three';
|
||||
import { NATION_IDS } from '../../src/constants/nations';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
import { 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<string, number>;
|
||||
};
|
||||
};
|
||||
maskSlice?: {
|
||||
[key: string]:
|
||||
| {
|
||||
enabled: boolean;
|
||||
planePosition: Vector3Tuple;
|
||||
planeNormal: Vector3Tuple;
|
||||
planeAxis: Vector3Tuple;
|
||||
frustumConeBaseRadius: number;
|
||||
frustumConeAngleDegrees: number;
|
||||
planeSliceMode: number;
|
||||
frustumConeSliceMode: number;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export async function tankIcons() {
|
||||
console.log('Building tank icons...');
|
||||
|
||||
const changes: FileChange[] = [];
|
||||
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml.dvpl`,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(tanks.root).map(async ([tankKey, tank]) => {
|
||||
if (tankKey.includes('tutorial_bot')) return;
|
||||
|
||||
const nationVehicleId = tank.id;
|
||||
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||
const parameters = await readYAMLDVPL<TankParameters>(
|
||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml.dvpl`,
|
||||
);
|
||||
const smallPath = `${DATA}/${parameters.resourcesPath.smallIconPath
|
||||
.replace(/~res:\//, '')
|
||||
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
||||
const bigPath = `${DATA}/${parameters.resourcesPath.bigIconPath
|
||||
.replace(/~res:\//, '')
|
||||
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
||||
const big = await sharp(await readDVPLFile(bigPath))
|
||||
.trim()
|
||||
.toBuffer();
|
||||
const small = await sharp(await readDVPLFile(smallPath))
|
||||
.trim()
|
||||
.toBuffer();
|
||||
|
||||
if (big) {
|
||||
changes.push({
|
||||
content: big.toString('base64'),
|
||||
encoding: 'base64',
|
||||
path: `icons/tanks/big/${id}.webp`,
|
||||
});
|
||||
}
|
||||
if (small) {
|
||||
changes.push({
|
||||
content: small.toString('base64'),
|
||||
encoding: 'base64',
|
||||
path: `icons/tanks/small/${id}.webp`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await commitAssets('tank icons', changes);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { NodeIO } from '@gltf-transform/core';
|
||||
import { readdir } from 'fs/promises';
|
||||
import { extractModel } from '../../src/core/blitz/extractModel';
|
||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
import { VehicleDefinitionList } from './definitions';
|
||||
import { TankParameters } from './tankIcons';
|
||||
|
||||
export async function tankModels() {
|
||||
console.log('Building tank models...');
|
||||
|
||||
const nodeIO = new NodeIO();
|
||||
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
|
||||
for (const nationIndex in nations) {
|
||||
const nation = nations[nationIndex];
|
||||
const changes: FileChange[] = [];
|
||||
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml.dvpl`,
|
||||
);
|
||||
|
||||
console.log(`Building models for ${nation}`);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(tanks.root).map(async ([tankKey, tank]) => {
|
||||
if (tankKey.includes('tutorial_bot')) return;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
const parameters = await readYAMLDVPL<TankParameters>(
|
||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml.dvpl`,
|
||||
);
|
||||
const model = await extractModel(
|
||||
DATA,
|
||||
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''),
|
||||
);
|
||||
|
||||
changes.push({
|
||||
path: `3d/tanks/models/${id}.glb`,
|
||||
content: Buffer.from(await nodeIO.writeBinary(model)).toString(
|
||||
'base64',
|
||||
),
|
||||
encoding: 'base64',
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await commitAssets(`tank models ${nation}`, changes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { chunk } from 'lodash';
|
||||
import { argv } from 'process';
|
||||
import {
|
||||
RatingInfo,
|
||||
RatingNeighbors,
|
||||
RatingPlayer,
|
||||
} from '../src/commands/ratingLeaderboard';
|
||||
import { RegionSubdomain } from '../src/constants/regions';
|
||||
import { getAccountInfo } from '../src/core/blitz/getAccountInfo';
|
||||
import regionSubdomainToRegion from '../src/core/blitz/regionSubdomainToRegion';
|
||||
import { commitAssets } from '../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../src/core/blitzkit/commitMultipleFiles.js';
|
||||
import { patientFetchJSON } from '../src/core/blitzkit/patientFetchJSON';
|
||||
import { superCompress } from '../src/core/blitzkit/superCompress';
|
||||
import {
|
||||
BkrlBaseEntry,
|
||||
BkrlFormat,
|
||||
BkrlSuperset1Entry,
|
||||
BkrlWriteStream,
|
||||
} from '../src/core/streams/bkrl';
|
||||
import { CdonValue, CdonWriteStream } from '../src/core/streams/cdon';
|
||||
|
||||
/*
|
||||
* Central North American Time (UTC-5): use 0 5 * * *.
|
||||
* Central European Time (UTC+1): use 0 23 * * *.
|
||||
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
|
||||
*/
|
||||
|
||||
const production = argv.includes('--production');
|
||||
const target = argv
|
||||
.find((arg) => arg.startsWith('--target='))
|
||||
?.split('=')[1] as 'midnight' | 'latest' | undefined;
|
||||
const regionSubdomains = argv
|
||||
.find((arg) => arg.startsWith('--region='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (!target) throw new Error('Target parameter not specified');
|
||||
if (!regionSubdomains) throw new Error('Region parameter not specified');
|
||||
|
||||
regionSubdomains.split(',').forEach(async (regionSubdomain) => {
|
||||
const region = regionSubdomainToRegion(regionSubdomain as RegionSubdomain);
|
||||
|
||||
console.log(`Building ${target} rating leaderboard for ${region}`);
|
||||
|
||||
const NEIGHBORS = 2 ** 8;
|
||||
const PLAYERS = await patientFetchJSON<RatingInfo>(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
).then((data) => (data.detail === undefined ? data.count : undefined));
|
||||
|
||||
if (PLAYERS === undefined) {
|
||||
throw new Error('No current rating season running');
|
||||
}
|
||||
|
||||
const players: Record<number, RatingPlayer> = {};
|
||||
const leaderboard: BkrlBaseEntry[] = [];
|
||||
let registered = 0;
|
||||
|
||||
async function branchFromPlayer(id: number) {
|
||||
const { neighbors } = await patientFetchJSON<RatingNeighbors>(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
||||
);
|
||||
|
||||
const firstPlayer = neighbors[0];
|
||||
const lastPlayer = neighbors[neighbors.length - 1];
|
||||
|
||||
const branches: Promise<void>[] = [];
|
||||
if (!players[firstPlayer.number])
|
||||
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
||||
if (!players[lastPlayer.number])
|
||||
branches.push(branchFromPlayer(lastPlayer.spa_id));
|
||||
|
||||
neighbors.forEach((neighbor) => {
|
||||
if (players[neighbor.number]) return;
|
||||
|
||||
players[neighbor.number] = neighbor;
|
||||
registered++;
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Registered ${registered} / ${PLAYERS} (${(
|
||||
(registered / PLAYERS!) *
|
||||
100
|
||||
).toFixed(2)}%)`,
|
||||
);
|
||||
|
||||
await Promise.all(branches);
|
||||
}
|
||||
|
||||
async function branchFromLeague(id: number) {
|
||||
const { result } = await patientFetchJSON<{ result: RatingPlayer[] }>(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
||||
);
|
||||
|
||||
const firstPlayer = result[0];
|
||||
const lastPlayer = result.at(-1)!;
|
||||
|
||||
await Promise.all([
|
||||
branchFromPlayer(firstPlayer.spa_id),
|
||||
branchFromPlayer(lastPlayer.spa_id),
|
||||
]);
|
||||
}
|
||||
|
||||
console.log('Branching from leagues...');
|
||||
await Promise.all([
|
||||
branchFromLeague(0),
|
||||
branchFromLeague(1),
|
||||
branchFromLeague(2),
|
||||
branchFromLeague(3),
|
||||
branchFromLeague(4),
|
||||
]);
|
||||
|
||||
console.log('Converting to an array...');
|
||||
for (let index = 0; index < PLAYERS; index++) {
|
||||
const listing = players[index + 1];
|
||||
if (listing) {
|
||||
leaderboard[index] = { id: listing.spa_id, score: listing.score };
|
||||
} else {
|
||||
console.warn(`Position ${index + 1} had no player`);
|
||||
leaderboard[index] = { id: 0, score: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Requesting further stats...');
|
||||
const comprehensiveLeaderboard: BkrlSuperset1Entry[] = [];
|
||||
let chunkIndex = 0;
|
||||
for (const leaderboardChunk of chunk(leaderboard, 100)) {
|
||||
console.log(
|
||||
`Chunk ${chunkIndex + 1} of ${Math.ceil(leaderboard.length / 100)}`,
|
||||
);
|
||||
|
||||
const stats = await getAccountInfo(
|
||||
region,
|
||||
leaderboardChunk.map(({ id }) => id),
|
||||
['statistics.rating'],
|
||||
);
|
||||
|
||||
stats.forEach((stat, index) => {
|
||||
const trueIndex = chunkIndex * 100 + index;
|
||||
comprehensiveLeaderboard[trueIndex] =
|
||||
leaderboardChunk[index].id === 0 ||
|
||||
stat?.statistics.rating === undefined
|
||||
? {
|
||||
battles: 0,
|
||||
damage: 0,
|
||||
id: 0,
|
||||
kills: 0,
|
||||
score: 0,
|
||||
survived: 0,
|
||||
wins: 0,
|
||||
}
|
||||
: {
|
||||
id: leaderboardChunk[index].id,
|
||||
score: leaderboardChunk[index].score,
|
||||
battles: stat.statistics.rating.battles,
|
||||
damage: stat.statistics.rating.damage_dealt,
|
||||
kills: stat.statistics.rating.frags,
|
||||
survived: stat.statistics.rating.survived_battles,
|
||||
wins: stat.statistics.rating.wins,
|
||||
};
|
||||
});
|
||||
|
||||
chunkIndex++;
|
||||
}
|
||||
|
||||
const info = await patientFetchJSON<RatingInfo & { detail: undefined }>(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
);
|
||||
const normalizedServer = regionSubdomain === 'na' ? 'com' : regionSubdomain;
|
||||
const infoContent = new CdonWriteStream().cdon(
|
||||
info as unknown as CdonValue,
|
||||
).uint8Array;
|
||||
const leaderboardWriteStream = new BkrlWriteStream();
|
||||
|
||||
leaderboardWriteStream.bkrl({
|
||||
format: BkrlFormat.Superset1,
|
||||
entries: comprehensiveLeaderboard,
|
||||
});
|
||||
|
||||
const changes: FileChange[] = [
|
||||
{
|
||||
path: `regions/${normalizedServer}/rating/${info.current_season}/info.cdon.lz4`,
|
||||
content: superCompress(infoContent),
|
||||
encoding: 'base64',
|
||||
},
|
||||
{
|
||||
path: `regions/${normalizedServer}/rating/${info.current_season}/${target}.bkrl`,
|
||||
content: Buffer.from(leaderboardWriteStream.uint8Array).toString(
|
||||
'base64',
|
||||
),
|
||||
encoding: 'base64',
|
||||
},
|
||||
];
|
||||
|
||||
commitAssets(`rating leaderboard ${regionSubdomain} ${target}`, changes);
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import { chunk, times, uniq } from 'lodash';
|
||||
import { compress, decompress } from 'lz4js';
|
||||
import { REGIONS, Region } from '../src/constants/regions';
|
||||
import { retryAbleBlitzFetchEvent } from '../src/core/blitz/fetchBlitz';
|
||||
import { getAccountInfo } from '../src/core/blitz/getAccountInfo';
|
||||
import { MIN_IDS, idToRegion } from '../src/core/blitz/idToRegion';
|
||||
import { asset } from '../src/core/blitzkit/asset';
|
||||
import { commitAssets } from '../src/core/blitzkit/commitAssets';
|
||||
import { DiscoveredIdsDefinitions } from '../src/core/blitzkit/discoveredIdDefinitions';
|
||||
import { DidsReadStream, DidsWriteStream } from '../src/core/streams/dids';
|
||||
|
||||
const CHUNK_SIZE = 2 ** 21;
|
||||
const RUN_TIME = 1000 * 60 * 60 * 5.5;
|
||||
const PROGRESS_UPDATE_FREQUENCY = 1000 * 60;
|
||||
const MAX_REQUESTS = 10;
|
||||
const ACCOUNTS_PER_CALL = 100;
|
||||
const TERMINATION_THRESHOLD = 1000;
|
||||
const startTime = Date.now();
|
||||
const indexableRegions = [...REGIONS];
|
||||
let ids: number[] = [];
|
||||
const preDiscoveredManifest = (await fetch(asset('ids/manifest.json')).then(
|
||||
(response) => response.json(),
|
||||
)) as DiscoveredIdsDefinitions;
|
||||
|
||||
console.log(
|
||||
`Fetching ${preDiscoveredManifest.chunks} pre-discovered chunks...`,
|
||||
);
|
||||
|
||||
let chunkIndex = 0;
|
||||
while (chunkIndex < preDiscoveredManifest.chunks) {
|
||||
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();
|
||||
},
|
||||
);
|
||||
|
||||
// no spread syntax: https://github.com/oven-sh/bun/issues/11734
|
||||
ids = ids.concat(preDiscovered);
|
||||
|
||||
console.log(
|
||||
`Pre-discovered ${preDiscovered.length} ids (chunk ${chunkIndex})`,
|
||||
);
|
||||
|
||||
chunkIndex++;
|
||||
}
|
||||
|
||||
const regionalIdIndex: Record<Region, number> = {
|
||||
asia: ids.findLast((id) => idToRegion(id) === 'asia') ?? MIN_IDS.asia,
|
||||
com: ids.findLast((id) => idToRegion(id) === 'com') ?? MIN_IDS.com,
|
||||
eu: ids.findLast((id) => idToRegion(id) === 'eu') ?? MIN_IDS.eu,
|
||||
};
|
||||
const zeroStreak: Record<Region, number> = { asia: 0, com: 0, eu: 0 };
|
||||
let regionIndex = 0;
|
||||
let outOfTimeFlagged = false;
|
||||
|
||||
async function verify(region: Region, ids: number[]) {
|
||||
const infos = await getAccountInfo(region, ids, undefined, {
|
||||
// cheeky way of requesting nothing
|
||||
fields: 'account_id,-account_id',
|
||||
});
|
||||
const filtered = ids.filter((_, index) => {
|
||||
const info = infos[index];
|
||||
return info !== null;
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
let discardAttempts = 0;
|
||||
let lastProgressUpdate = 0;
|
||||
|
||||
retryAbleBlitzFetchEvent.on(() => discardAttempts++);
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
if (discardAttempts > 0) return discardAttempts--;
|
||||
|
||||
const region = indexableRegions[regionIndex];
|
||||
const idsToVerify = times(
|
||||
ACCOUNTS_PER_CALL,
|
||||
(index) => regionalIdIndex[region] + index,
|
||||
);
|
||||
const verified = await verify(region, idsToVerify);
|
||||
|
||||
if (Date.now() - lastProgressUpdate > PROGRESS_UPDATE_FREQUENCY) {
|
||||
lastProgressUpdate = Date.now();
|
||||
console.log(`Discovered ${ids.length} ids in ${Date.now() - startTime}ms`);
|
||||
}
|
||||
|
||||
if (verified.length === 0) {
|
||||
zeroStreak[region]++;
|
||||
|
||||
if (zeroStreak[region] >= TERMINATION_THRESHOLD) {
|
||||
const sliceableIndex = indexableRegions.indexOf(region);
|
||||
|
||||
if (sliceableIndex !== -1) {
|
||||
console.log(`Stopped discovery for ${region}`);
|
||||
indexableRegions.splice(sliceableIndex, 1);
|
||||
|
||||
if (indexableRegions.length === 0) {
|
||||
console.log('All players discovered exhaustively!');
|
||||
post();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
zeroStreak[region] = 0;
|
||||
ids.push(...verified);
|
||||
}
|
||||
|
||||
if (Date.now() - startTime > RUN_TIME && !outOfTimeFlagged) {
|
||||
outOfTimeFlagged = true;
|
||||
console.log('Out of time');
|
||||
post();
|
||||
}
|
||||
|
||||
regionIndex = (regionIndex + 1) % indexableRegions.length;
|
||||
regionalIdIndex[region] += ACCOUNTS_PER_CALL;
|
||||
}, 1000 / MAX_REQUESTS);
|
||||
|
||||
function post() {
|
||||
clearInterval(interval);
|
||||
ids = uniq(ids).sort((a, b) => a - b);
|
||||
console.log(`Uploading ${ids.length} ids...`);
|
||||
|
||||
const idsChunked = chunk(ids, CHUNK_SIZE);
|
||||
const files = idsChunked.map((chunk, index) => {
|
||||
const didsWriteStream = new DidsWriteStream().dids(chunk);
|
||||
const buffer = didsWriteStream.uint8Array;
|
||||
const compressed = compress(buffer);
|
||||
|
||||
console.log(
|
||||
`Chunk ${index} with ${chunk.length} ids (compressed: ${compressed.length}; uncompressed: ${buffer.length})`,
|
||||
);
|
||||
|
||||
const content = Buffer.from(compressed).toString('base64');
|
||||
|
||||
return {
|
||||
content,
|
||||
encoding: 'base64' as const,
|
||||
path: `ids/${index}.dids.lz4`,
|
||||
};
|
||||
});
|
||||
|
||||
commitAssets('discovered ids', [
|
||||
...files,
|
||||
{
|
||||
content: JSON.stringify({
|
||||
chunks: idsChunked.length,
|
||||
count: ids.length,
|
||||
time: Date.now(),
|
||||
} satisfies DiscoveredIdsDefinitions),
|
||||
encoding: 'utf-8',
|
||||
path: 'ids/manifest.json',
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { assertSecret } from '../src/core/blitzkit/secret';
|
||||
|
||||
assertSecret(process.env.ZIP_URL);
|
||||
@@ -0,0 +1,131 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import { google } from 'googleapis';
|
||||
import { cloneDeep, uniqBy } from 'lodash';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { youtubers } from '../src/constants/youtubers';
|
||||
import { readXMLDVPL } from '../src/core/blitz/readXMLDVPL';
|
||||
import { readYAMLDVPL } from '../src/core/blitz/readYAMLDVPL';
|
||||
import { toUniqueId } from '../src/core/blitz/toUniqueId';
|
||||
import { asset } from '../src/core/blitzkit/asset';
|
||||
import { commitAssets } from '../src/core/blitzkit/commitAssets';
|
||||
import { Reviews, Video } from '../src/core/blitzkit/reviews';
|
||||
import { decode } from '../src/core/protobuf/decode';
|
||||
import { encodeToBase64 } from '../src/core/protobuf/encodeToBase64';
|
||||
import { DATA } from './buildAssets/constants';
|
||||
import {
|
||||
BlitzStrings,
|
||||
botPattern,
|
||||
VehicleDefinitionList,
|
||||
} from './buildAssets/definitions';
|
||||
|
||||
const MAX_QUERIES = 128;
|
||||
|
||||
console.log('Finding reviews...');
|
||||
|
||||
const currentReviews = await fetch(asset('definitions/reviews.pb'))
|
||||
.then((response) => response.arrayBuffer())
|
||||
.then((buffer) =>
|
||||
decode<Reviews>('reviews', 'blitzkit.Reviews', new Uint8Array(buffer)),
|
||||
);
|
||||
const auth = await google.auth.getClient({
|
||||
scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
|
||||
});
|
||||
const youtube = google.youtube({ version: 'v3', auth });
|
||||
|
||||
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
const stringsPreInstalled = await readYAMLDVPL<BlitzStrings>(
|
||||
`${DATA}/Strings/en.yaml.dvpl`,
|
||||
);
|
||||
const stringsCache = await fetch(
|
||||
'https://stufficons.wgcdn.co/localizations/en.yaml',
|
||||
)
|
||||
.then((response) => response.text())
|
||||
.then((string) => parseYaml(string) as BlitzStrings);
|
||||
const strings = {
|
||||
...stringsPreInstalled,
|
||||
...stringsCache,
|
||||
};
|
||||
const tanks: { id: number; name: string; tier: number }[] = [];
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml.dvpl`,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(tankList.root).map(async ([tankKey, tank]) => {
|
||||
if (botPattern.test(tankKey)) return null;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
const name =
|
||||
(tank.shortUserString ? strings[tank.shortUserString] : undefined) ??
|
||||
strings[tank.userString];
|
||||
const tier = tank.level;
|
||||
|
||||
tanks.push({ id, name, tier });
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const tanksSanitized = tanks
|
||||
.sort((a, b) => b.tier - a.tier)
|
||||
.sort((a, b) => {
|
||||
const lastUpdatedA = currentReviews.reviews[a.id]?.last_updated ?? 0;
|
||||
const lastUpdatedB = currentReviews.reviews[b.id]?.last_updated ?? 0;
|
||||
return lastUpdatedA - lastUpdatedB;
|
||||
});
|
||||
const reviews: Reviews = cloneDeep(currentReviews);
|
||||
|
||||
let done = 0;
|
||||
for (const tank of tanksSanitized) {
|
||||
if (done >= MAX_QUERIES) {
|
||||
console.log(`Limit reached with ${done} queries; stopping...`);
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await youtube.search.list({
|
||||
part: ['snippet'],
|
||||
q: `World of Tanks Blitz ${tank.name}`,
|
||||
});
|
||||
|
||||
if (!results.data.items || results.data.items.length === 0) continue;
|
||||
|
||||
const items = uniqBy(
|
||||
results.data.items.filter(
|
||||
(item) =>
|
||||
item.snippet?.channelId &&
|
||||
item.id?.videoId &&
|
||||
youtubers.some(({ id }) => id === item.snippet?.channelId),
|
||||
),
|
||||
(item) => item.snippet!.channelId,
|
||||
);
|
||||
|
||||
reviews.reviews[tank.id] = {
|
||||
last_updated: Date.now(),
|
||||
videos: items.map(
|
||||
(item) =>
|
||||
({
|
||||
id: item.id!.videoId!,
|
||||
author: item.snippet!.channelId!,
|
||||
}) satisfies Video,
|
||||
),
|
||||
};
|
||||
|
||||
console.log(`${++done} done; found ${items.length} for ${tank.name}`);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 / 5));
|
||||
} catch (error) {
|
||||
console.warn(`Limit reached with ${done} queries`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const content = await encodeToBase64('reviews', 'blitzkit.Reviews', reviews);
|
||||
|
||||
await commitAssets('reviews', [
|
||||
{ content, encoding: 'base64', path: 'definitions/reviews.pb' },
|
||||
]);
|
||||
@@ -0,0 +1,33 @@
|
||||
import decompress from 'decompress';
|
||||
import { existsSync } from 'fs';
|
||||
import { mkdir, readdir, writeFile } from 'fs/promises';
|
||||
|
||||
if (!existsSync('temp')) {
|
||||
console.log('Creating temp directory...');
|
||||
await mkdir('temp');
|
||||
}
|
||||
|
||||
console.log('Downloading depot downloader...');
|
||||
await fetch(
|
||||
'https://api.github.com/repos/SteamRE/DepotDownloader/releases/latest',
|
||||
)
|
||||
.then(
|
||||
(response) =>
|
||||
response.json() as Promise<{
|
||||
assets: { browser_download_url: string; name: string }[];
|
||||
}>,
|
||||
)
|
||||
.then(({ assets }) => {
|
||||
const asset = assets.find(({ name }) => name.includes('linux-x64'));
|
||||
if (!asset) throw new Error('No asset found');
|
||||
return fetch(asset.browser_download_url);
|
||||
})
|
||||
.then((response) => response.arrayBuffer())
|
||||
.then((arrayBuffer) =>
|
||||
writeFile('temp/depotDownloader.zip', new Uint8Array(arrayBuffer)),
|
||||
);
|
||||
|
||||
console.log('Unzipping...');
|
||||
await decompress('temp/depotDownloader.zip', 'temp/depotDownloader');
|
||||
|
||||
console.log('Download complete; files:', await readdir('temp/depotDownloader'));
|
||||
@@ -0,0 +1,26 @@
|
||||
import { tankDefinitions } from '../src/core/blitzkit/tankDefinitions';
|
||||
|
||||
// random vercel bug forces require import
|
||||
const { writeFileSync } = require('fs');
|
||||
|
||||
console.log('Building sitemap...');
|
||||
|
||||
const awaitedTankDefinitions = await tankDefinitions;
|
||||
const values = Object.values(awaitedTankDefinitions);
|
||||
const txt = values
|
||||
.map((tank) => `https://blitzkit.app/tools/tankopedia/${tank.id}/`)
|
||||
.join('\n');
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
${values
|
||||
.map(
|
||||
(tank) =>
|
||||
`<url><loc>https://blitzkit.app/tools/tankopedia/${tank.id}/</loc></url>`,
|
||||
)
|
||||
.join('\n')}
|
||||
</urlset>`;
|
||||
|
||||
writeFileSync('public/sitemap.txt', txt);
|
||||
writeFileSync('public/sitemap.xml', xml);
|
||||
|
||||
console.log(`Built sitemap for ${values.length} tanks`);
|
||||
Reference in new issue
Block a user