import { config } from 'dotenv'; import { existsSync } from 'fs'; import { mkdir, readdir, rm } from 'fs/promises'; import { argv } from 'process'; import { TankType } from '../src/components/Tanks'; import { NATION_IDS } from '../src/constants/nations'; import { SCPGStream } from '../src/core/blitz/SCPGStream'; import { readBase64DVPL } from '../src/core/blitz/readBase64DVPL'; import { readXMLDVPL } from '../src/core/blitz/readXMLDVPL'; import { readYAMLDVPL } from '../src/core/blitz/readYAMLDVPL'; import { toUniqueId } from '../src/core/blitz/toUniqueId'; import commitMultipleFiles, { FileChange, } from '../src/core/blitzkrieg/commitMultipleFiles'; import { BlitzkriegGunDefinitions } from '../src/core/blitzkrieg/definitions/guns'; import { BlitzkriegTankDefinitions, Tier, } from '../src/core/blitzkrieg/definitions/tanks'; import { BlitzkriegTurretDefinitions } from '../src/core/blitzkrieg/definitions/turrets'; config(); // WARNING! MOST OF THESE TYPES ARE NOT EXHAUSTIVE! interface VehicleDefinitionList { [key: string]: { id: number; userString: string; shortUserString?: string; description: string; price: number | { gold: ''; '#text': number }; sellPrice?: { gold: ''; '#text': number }; enrichmentPermanentCost: number; notInShop?: boolean; tags: string; level: number; combatRole?: Record; configurationModes: string; }; } interface TurretDefinitionsList { nextAvailableId: number; ids: Record; } interface GunDefinitionsList { nextAvailableId: number; ids: Record; shared: { [key: string]: { userString: string; tags: string; level: number; pitchLimits: string; }; }; } interface VehicleDefinitions { turrets0: { [key: string]: { userString: number; level: number; yawLimits: string | string[]; guns: { [key: string]: { maxAmmo: number; pitchLimits?: string | string[]; }; }; }; }; } interface Strings { [key: string]: string; } type BlitzTankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank'; interface TankParameters { resourcesPath: { smallIconPath: string; bigIconPath: string; blitzModelPath: string; }; } const blitzTankTypeToBlitzkrieg: Record = { 'AT-SPG': 'tank_destroyer', lightTank: 'light', mediumTank: 'medium', heavyTank: 'heavy', }; const DATA = 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data'; const LANGUAGE = 'en'; const DOI = { vehicleDefinitions: 'XML/item_defs/vehicles', strings: 'Strings', tankParameters: '3d/Tanks/Parameters', smallIcons: 'Gfx/UI/BattleScreenHUD/SmallTankIcons', bigIcons: 'Gfx/UI/BigTankIcons', flags: 'Gfx/Lobby/flags', '3d': '3d', }; const allTargets = argv.includes('--all-targets'); const targets = argv .find((argument) => argument.startsWith('--target')) ?.split('=')[1] .split(','); if (!targets && !allTargets) throw new Error('No target(s) specified'); if (allTargets || targets?.includes('definitions')) { console.log('Building tank definitions...'); const tankDefinitions: BlitzkriegTankDefinitions = {}; const turretDefinitions: BlitzkriegTurretDefinitions = {}; const gunDefinitions: BlitzkriegGunDefinitions = {}; const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then( (nations) => nations.filter((nation) => nation !== 'common'), ); const strings = await readYAMLDVPL( `${DATA}/${DOI.strings}/${LANGUAGE}.yaml.dvpl`, ); await Promise.all( nations.map(async (nation) => { const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>( `${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`, ); const turretList = await readXMLDVPL<{ root: TurretDefinitionsList; }>( `${DATA}/${DOI.vehicleDefinitions}/${nation}/components/turrets.xml.dvpl`, ); const gunList = await readXMLDVPL<{ root: GunDefinitionsList; }>( `${DATA}/${DOI.vehicleDefinitions}/${nation}/components/guns.xml.dvpl`, ); for (const tankKey in tankList.root) { const tank = tankList.root[tankKey]; const tankDefinition = await readXMLDVPL<{ root: VehicleDefinitions }>( `${DATA}/${DOI.vehicleDefinitions}/${nation}/${tankKey}.xml.dvpl`, ); const tankId = toUniqueId(nation, tank.id); const tankTags = tank.tags.split(' '); const tankTurrets = Object.keys(tankDefinition.root.turrets0).map( (turretKey) => { const turret = tankDefinition.root.turrets0[turretKey]; const turretId = toUniqueId(nation, turretList.root.ids[turretKey]); const turretGuns = Object.keys(turret.guns).map((gunKey) => toUniqueId(nation, gunList.root.ids[gunKey]), ); turretDefinitions[turretId] = { id: turretId, name: strings[turret.userString] ?? turretKey .replaceAll('_', ' ') .replace(/^(Turret ([0-9] )?)+/, ''), tier: turret.level, yaw: (typeof turret.yawLimits === 'string' ? turret.yawLimits : turret.yawLimits[0] ) .split(' ') .map(Number) as [number, number], guns: turretGuns, }; return turretId; }, ); tankDefinitions[tankId] = { id: tankId, name: (tank.shortUserString ? strings[tank.shortUserString] : undefined) ?? strings[tank.userString] ?? tankKey.replaceAll('_', ' '), nation, tree_type: (tank.sellPrice ? 'gold' in tank.sellPrice : false) ? 'collector' : (typeof tank.price === 'number' ? false : 'gold' in tank.price) ? 'premium' : 'researchable', tier: tank.level as Tier, type: blitzTankTypeToBlitzkrieg[tankTags[0] as BlitzTankType], testing: tankTags.includes('testTank') ? true : undefined, turrets: tankTurrets, }; } Object.keys(gunList.root.ids).forEach((gunKey) => { const gun = gunList.root.shared[gunKey]; const gunId = toUniqueId(nation, gunList.root.ids[gunKey]); gunDefinitions[gunId] = { id: gunId, name: strings[gun.userString] ?? gunKey.replaceAll('_', ' '), tier: gun.level, pitch: gun.pitchLimits.split(' ').map(Number) as [number, number], }; }); }), ); // rmSync('dist/definitions', { force: true, recursive: true }); // mkdirSync('dist/definitions', { recursive: true }); // writeFile( // 'dist/definitions/tanks.json', // JSON.stringify(tankDefinitions, null, 2), // ); // writeFile( // 'dist/definitions/turrets.json', // JSON.stringify(turretDefinitions, null, 2), // ); // writeFile( // 'dist/definitions/guns.json', // JSON.stringify(gunDefinitions, null, 2), // ); // console.log('Committing tank definitions...'); // await commitMultipleFiles( // 'tresabhi', // 'blitzkrieg-assets', // 'main', // 'tank definitions', // [ // { // content: JSON.stringify(tankDefinitions), // encoding: 'utf-8', // path: 'definitions/tanks.json', // }, // ], // true, // ); } if ( allTargets || targets?.includes('bigTankIcons') || targets?.includes('smallTankIcons') ) { console.log('Building tank icons...'); const changes: FileChange[] = []; const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then( (nations) => nations.filter((nation) => nation !== 'common'), ); await Promise.all( nations.map(async (nation) => { const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( `${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`, ); for (const tankIndex in tanks.root) { const tank = tanks.root[tankIndex]; const nationVehicleId = tank.id; const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; const parameters = await readYAMLDVPL( `${DATA}/${DOI.tankParameters}/${nation}/${tankIndex}.yaml.dvpl`, ); const small = `${DATA}/${parameters.resourcesPath.smallIconPath .replace(/~res:\//, '') .replace(/\..+/, '')}.packed.webp.dvpl`; const big = `${DATA}/${parameters.resourcesPath.bigIconPath .replace(/~res:\//, '') .replace(/\..+/, '')}.packed.webp.dvpl`; if (allTargets || targets?.includes('bigTankIcons')) { changes.push({ content: await readBase64DVPL(big), encoding: 'base64', path: `icons/big/${id}.webp`, }); } if (allTargets || targets?.includes('smallTankIcons')) { changes.push({ content: await readBase64DVPL(small), encoding: 'base64', path: `icons/small/${id}.webp`, }); } } }), ); console.log('Committing tank icons...'); await commitMultipleFiles( 'tresabhi', 'blitzkrieg-assets', 'main', 'tank icons', changes, true, ); } if (allTargets || targets?.includes('scratchedFlags')) { console.log('Building scratched flags...'); const flags = await readdir(`${DATA}/${DOI.flags}`); console.log('Committing scratched flags...'); commitMultipleFiles( 'tresabhi', 'blitzkrieg-assets', 'main', 'scratched flags', await Promise.all( flags .filter( (flag) => flag.startsWith('flag_tutor-tank_') && !flag.endsWith('@2x.packed.webp.dvpl'), ) .map(async (flag) => { const content = await readBase64DVPL(`${DATA}/${DOI.flags}/${flag}`); const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1]; return { content, encoding: 'base64', path: `flags/scratched/${name}.webp`, } satisfies FileChange; }), ), true, ); } if (allTargets || targets?.includes('circleFlags')) { const flags = await readdir(`${DATA}/${DOI.flags}`); commitMultipleFiles( 'tresabhi', 'blitzkrieg-assets', 'main', 'circle flags', await Promise.all( flags .filter( (flag) => flag.startsWith('flag_profile-stat_') && !flag.endsWith('@2x.packed.webp.dvpl'), ) .map(async (flag) => { const content = await readBase64DVPL(`${DATA}/${DOI.flags}/${flag}`); const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1]; return { content, encoding: 'base64', path: `flags/circle/${name}.webp`, } satisfies FileChange; }), ), true, ); } if (allTargets || targets?.includes('tankModels')) { console.log('Building tank models...'); if (existsSync('dist/assets/models')) { await rm('dist/assets/models', { recursive: true }); } await mkdir('dist/assets/models', { recursive: true }); const changes: FileChange[] = []; const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then( (nations) => nations.filter((nation) => nation !== 'common'), ); await Promise.all( nations.map(async (nation) => { const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( `${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`, ); for (const tankIndex in tanks.root) { const tank = tanks.root[tankIndex]; const nationVehicleId = tank.id; const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; // if (id !== 897) continue; console.log(`Building model ${id} @ ${nation}/${tankIndex}`); const parameters = await readYAMLDVPL( `${DATA}/${DOI.tankParameters}/${nation}/${tankIndex}.yaml.dvpl`, ); const sc2Path = parameters.resourcesPath.blitzModelPath; const scgPath = parameters.resourcesPath.blitzModelPath.replace( /\.sc2$/, '.scg', ); const sc2Stream = await SCPGStream.fromDVPLFile( `${DATA}/${DOI['3d']}/${sc2Path}.dvpl`, ); const scgStream = await SCPGStream.fromDVPLFile( `${DATA}/${DOI['3d']}/${scgPath}.dvpl`, ); const sc2 = sc2Stream.consumeSC2(); const scg = scgStream.consumeSCG(); } }), ); }