From 61fe8c7e7c1488331a42ad6c42b4b492b50dc84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:35:37 -0500 Subject: [PATCH] feat: type errors in definitions --- .../src/blitzkit/averageDefinitions/index.ts | 9 +- .../src/blitzkit/camouflageDefinitions.ts | 9 +- .../src/blitzkit/consumableDefinitions.ts | 9 +- .../core/src/blitzkit/equipmentDefinitions.ts | 9 +- packages/core/src/blitzkit/gameDefinitions.ts | 9 +- packages/core/src/blitzkit/mapDefinitions.ts | 14 +- .../core/src/blitzkit/modelDefinitions.ts | 9 +- .../core/src/blitzkit/provisionDefinitions.ts | 9 +- packages/core/src/blitzkit/reviews.ts | 9 +- .../core/src/blitzkit/skillDefinitions.ts | 9 +- .../src/blitzkit/tankDefinitions/index.ts | 9 +- packages/core/src/index.ts | 20 +- packages/core/src/protobuf/encodePB64.ts | 5 + packages/core/src/protobuf/fetchPB.ts | 8 + packages/core/src/protobuf/index.ts | 3 + .../core/src/{types => protobuf}/pbMap.ts | 0 .../src/protos/consumable_definitions.proto | 9 +- .../core/src/protos/map_definitions.proto | 12 + .../core/src/protos/tank_definitions.proto | 73 +- packages/core/src/types/index.ts | 1 - .../src/buildAssets/LEGACY_definitions.ts | 1522 --------------- .../scripts/src/buildAssets/definitions.ts | 1663 ++++++++++++++++- 22 files changed, 1728 insertions(+), 1692 deletions(-) create mode 100644 packages/core/src/protobuf/encodePB64.ts create mode 100644 packages/core/src/protobuf/fetchPB.ts create mode 100644 packages/core/src/protobuf/index.ts rename packages/core/src/{types => protobuf}/pbMap.ts (100%) create mode 100644 packages/core/src/protos/map_definitions.proto delete mode 100644 packages/scripts/src/buildAssets/LEGACY_definitions.ts diff --git a/packages/core/src/blitzkit/averageDefinitions/index.ts b/packages/core/src/blitzkit/averageDefinitions/index.ts index 7d3a190cd..d04e5c38e 100644 --- a/packages/core/src/blitzkit/averageDefinitions/index.ts +++ b/packages/core/src/blitzkit/averageDefinitions/index.ts @@ -1,4 +1,5 @@ import { AverageDefinitions } from '../../protos'; +import { fetchPB } from '../../types'; import { asset } from '../asset'; export interface AverageDefinitionsManifest { @@ -13,14 +14,10 @@ export async function fetchAverageDefinitions() { const manifestResponse = await fetch(asset('averages/manifest.json')); const manifestJson = (await manifestResponse.json()) as AverageDefinitionsManifest; - const averageDefinitionsResponse = await fetch( + return await fetchPB( asset(`averages/${manifestJson.latest}.pb`), + AverageDefinitions, ); - const averageDefinitionsBuffer = - await averageDefinitionsResponse.arrayBuffer(); - const averageDefinitionsArray = new Uint8Array(averageDefinitionsBuffer); - - return AverageDefinitions.deserializeBinary(averageDefinitionsArray); } export * from './constants'; diff --git a/packages/core/src/blitzkit/camouflageDefinitions.ts b/packages/core/src/blitzkit/camouflageDefinitions.ts index 5a25c4417..0ef38bc5e 100644 --- a/packages/core/src/blitzkit/camouflageDefinitions.ts +++ b/packages/core/src/blitzkit/camouflageDefinitions.ts @@ -1,10 +1,7 @@ import { CamouflageDefinitions } from '../protos'; +import { fetchPB } from '../types'; import { asset } from './asset'; -export async function fetchCamouflageDefinitions() { - const response = await fetch(asset('definitions/camoufles.pb')); - const buffer = await response.arrayBuffer(); - const array = new Uint8Array(buffer); - - return CamouflageDefinitions.deserializeBinary(array).toObject(); +export function fetchCamouflageDefinitions() { + return fetchPB(asset('definitions/camoufles.pb'), CamouflageDefinitions); } diff --git a/packages/core/src/blitzkit/consumableDefinitions.ts b/packages/core/src/blitzkit/consumableDefinitions.ts index 0e5f22b56..fd972e1fa 100644 --- a/packages/core/src/blitzkit/consumableDefinitions.ts +++ b/packages/core/src/blitzkit/consumableDefinitions.ts @@ -1,10 +1,7 @@ import { ConsumableDefinitions } from '../protos'; +import { fetchPB } from '../types'; import { asset } from './asset'; -export async function fetchConsumableDefinitions() { - const response = await fetch(asset('definitions/consumables.pb')); - const buffer = await response.arrayBuffer(); - const array = new Uint8Array(buffer); - - return ConsumableDefinitions.deserializeBinary(array).toObject(); +export function fetchConsumableDefinitions() { + return fetchPB(asset('definitions/consumables.pb'), ConsumableDefinitions); } diff --git a/packages/core/src/blitzkit/equipmentDefinitions.ts b/packages/core/src/blitzkit/equipmentDefinitions.ts index e46d21813..a012b0655 100644 --- a/packages/core/src/blitzkit/equipmentDefinitions.ts +++ b/packages/core/src/blitzkit/equipmentDefinitions.ts @@ -1,10 +1,7 @@ import { EquipmentDefinitions } from '../protos'; +import { fetchPB } from '../types'; import { asset } from './asset'; -export async function fetchEquipmentDefinitions() { - const response = await fetch(asset('definitions/equipment.pb')); - const buffer = await response.arrayBuffer(); - const array = new Uint8Array(buffer); - - return EquipmentDefinitions.deserializeBinary(array).toObject(); +export function fetchEquipmentDefinitions() { + return fetchPB(asset('definitions/equipment.pb'), EquipmentDefinitions); } diff --git a/packages/core/src/blitzkit/gameDefinitions.ts b/packages/core/src/blitzkit/gameDefinitions.ts index 0b7f6ff3b..60f5905f2 100644 --- a/packages/core/src/blitzkit/gameDefinitions.ts +++ b/packages/core/src/blitzkit/gameDefinitions.ts @@ -1,10 +1,7 @@ import { GameDefinitions } from '../protos'; +import { fetchPB } from '../types'; import { asset } from './asset'; -export async function fetchGameDefinitions() { - const response = await fetch(asset('definitions/game.pb')); - const buffer = await response.arrayBuffer(); - const array = new Uint8Array(buffer); - - return GameDefinitions.deserializeBinary(array).toObject(); +export function fetchGameDefinitions() { + return fetchPB(asset('definitions/game.pb'), GameDefinitions); } diff --git a/packages/core/src/blitzkit/mapDefinitions.ts b/packages/core/src/blitzkit/mapDefinitions.ts index fb47c7007..ee3001384 100644 --- a/packages/core/src/blitzkit/mapDefinitions.ts +++ b/packages/core/src/blitzkit/mapDefinitions.ts @@ -1,13 +1,7 @@ +import { MapDefinitions } from '../protos'; +import { fetchPB } from '../types'; import { asset } from './asset'; -import { fetchCdonLz4 } from './fetchCdonLz4'; -export interface MapDefinitions { - [id: number]: { - id: number; - name: string; - }; +export function fetchMapDefinitions() { + return fetchPB(asset('definitions/maps.pb'), MapDefinitions); } - -export const mapDefinitions = fetchCdonLz4( - asset('definitions/maps.cdon.lz4'), -); diff --git a/packages/core/src/blitzkit/modelDefinitions.ts b/packages/core/src/blitzkit/modelDefinitions.ts index ba0bbaccc..47006e61d 100644 --- a/packages/core/src/blitzkit/modelDefinitions.ts +++ b/packages/core/src/blitzkit/modelDefinitions.ts @@ -1,10 +1,7 @@ import { ModelDefinitions } from '../protos'; +import { fetchPB } from '../types'; import { asset } from './asset'; -export async function fetchModelDefinitions() { - const response = await fetch(asset('definitions/models.pb')); - const buffer = await response.arrayBuffer(); - const array = new Uint8Array(buffer); - - return ModelDefinitions.deserializeBinary(array).toObject(); +export function fetchModelDefinitions() { + return fetchPB(asset('definitions/models.pb'), ModelDefinitions); } diff --git a/packages/core/src/blitzkit/provisionDefinitions.ts b/packages/core/src/blitzkit/provisionDefinitions.ts index 3c7b59ae8..cc2faaa8f 100644 --- a/packages/core/src/blitzkit/provisionDefinitions.ts +++ b/packages/core/src/blitzkit/provisionDefinitions.ts @@ -1,10 +1,7 @@ import { ProvisionDefinitions } from '../protos'; +import { fetchPB } from '../types'; import { asset } from './asset'; -export async function fetchProvisionDefinitions() { - const response = await fetch(asset('definitions/provisions.pb')); - const buffer = await response.arrayBuffer(); - const array = new Uint8Array(buffer); - - return ProvisionDefinitions.deserializeBinary(array).toObject(); +export function fetchProvisionDefinitions() { + return fetchPB(asset('definitions/provisions.pb'), ProvisionDefinitions); } diff --git a/packages/core/src/blitzkit/reviews.ts b/packages/core/src/blitzkit/reviews.ts index a90caf12d..1852b1bd5 100644 --- a/packages/core/src/blitzkit/reviews.ts +++ b/packages/core/src/blitzkit/reviews.ts @@ -1,10 +1,7 @@ import { Reviews } from '../protos'; +import { fetchPB } from '../types'; import { asset } from './asset'; -export async function fetchReviews() { - const response = await fetch(asset('definitions/reviews.pb')); - const buffer = await response.arrayBuffer(); - const array = new Uint8Array(buffer); - - return Reviews.deserializeBinary(array).toObject(); +export function fetchReviews() { + return fetchPB(asset('definitions/reviews.pb'), Reviews); } diff --git a/packages/core/src/blitzkit/skillDefinitions.ts b/packages/core/src/blitzkit/skillDefinitions.ts index 35a81963d..6e074577f 100644 --- a/packages/core/src/blitzkit/skillDefinitions.ts +++ b/packages/core/src/blitzkit/skillDefinitions.ts @@ -1,10 +1,7 @@ import { SkillDefinitions } from '../protos'; +import { fetchPB } from '../types'; import { asset } from './asset'; -export async function fetchSkillDefinitions() { - const response = await fetch(asset('definitions/skills.pb')); - const buffer = await response.arrayBuffer(); - const array = new Uint8Array(buffer); - - return SkillDefinitions.deserializeBinary(array).toObject(); +export function fetchSkillDefinitions() { + return fetchPB(asset('definitions/skills.pb'), SkillDefinitions); } diff --git a/packages/core/src/blitzkit/tankDefinitions/index.ts b/packages/core/src/blitzkit/tankDefinitions/index.ts index 7b37b387f..2e2229f86 100644 --- a/packages/core/src/blitzkit/tankDefinitions/index.ts +++ b/packages/core/src/blitzkit/tankDefinitions/index.ts @@ -1,12 +1,9 @@ import { TankDefinitions } from '../../protos'; +import { fetchPB } from '../../types'; import { asset } from '../asset'; -export async function fetchTankDefinitions() { - const response = await fetch(asset('definitions/tanks.pb')); - const buffer = await response.arrayBuffer(); - const array = new Uint8Array(buffer); - - return TankDefinitions.deserializeBinary(array).toObject(); +export function fetchTankDefinitions() { + return fetchPB(asset('definitions/tanks.pb'), TankDefinitions); } export * from './constants'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ff4ce2df1..cae496f70 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,11 +1,11 @@ -// export * from './blitz'; -// export * from './blitzkit'; +export * from './blitz'; +export * from './blitzkit'; +export * from './databases'; +export * from './imgur'; +export * from './math'; +export * from './patreon'; +export * from './protobuf'; export * from './protos'; -// export * from './blitzstars'; -// export * from './databases'; -// export * from './imgur'; -// export * from './math'; -// export * from './patreon'; -// export * from './statistics'; -// export * from './streams'; -// export * from './types'; +export * from './statistics'; +export * from './streams'; +export * from './types'; diff --git a/packages/core/src/protobuf/encodePB64.ts b/packages/core/src/protobuf/encodePB64.ts new file mode 100644 index 000000000..24280f104 --- /dev/null +++ b/packages/core/src/protobuf/encodePB64.ts @@ -0,0 +1,5 @@ +import { MessageFns } from '../protos'; + +export function encodePB64(message: MessageFns, data: Type) { + return Buffer.from(message.encode(data).finish()).toString('base64'); +} diff --git a/packages/core/src/protobuf/fetchPB.ts b/packages/core/src/protobuf/fetchPB.ts new file mode 100644 index 000000000..4d725ee00 --- /dev/null +++ b/packages/core/src/protobuf/fetchPB.ts @@ -0,0 +1,8 @@ +import { MessageFns } from '../protos'; + +export async function fetchPB(url: string, message: MessageFns) { + const response = await fetch(url); + const buffer = await response.arrayBuffer(); + const array = new Uint8Array(buffer); + return message.decode(array); +} diff --git a/packages/core/src/protobuf/index.ts b/packages/core/src/protobuf/index.ts new file mode 100644 index 000000000..8d92a6d2f --- /dev/null +++ b/packages/core/src/protobuf/index.ts @@ -0,0 +1,3 @@ +export * from './encodePB64'; +export * from './fetchPB'; +export * from './pbMap'; diff --git a/packages/core/src/types/pbMap.ts b/packages/core/src/protobuf/pbMap.ts similarity index 100% rename from packages/core/src/types/pbMap.ts rename to packages/core/src/protobuf/pbMap.ts diff --git a/packages/core/src/protos/consumable_definitions.proto b/packages/core/src/protos/consumable_definitions.proto index ac40fbef0..833e50f0a 100644 --- a/packages/core/src/protos/consumable_definitions.proto +++ b/packages/core/src/protos/consumable_definitions.proto @@ -20,10 +20,10 @@ message Consumable { message ConsumableTankFilter { oneof filter_type { - ConsumableTankTierFilter tier = 1; + ConsumableTankTierFilter tiers = 1; ConsumableTankIdsFilter ids = 2; - ConsumableTankCategoryFilter category = 3; - ConsumableTankNationFilter nation = 4; + ConsumableTankCategoryFilter categories = 3; + ConsumableTankNationFilter nations = 4; } } @@ -41,8 +41,7 @@ message ConsumableTankCategoryFilter { } enum ConsumableTankCategoryFilterCategory { - TANK_CATEGORY_UNSPECIFIED = 0; - TANK_CATEGORY_CLIP = 1; + TANK_CATEGORY_CLIP = 0; } message ConsumableTankNationFilter { diff --git a/packages/core/src/protos/map_definitions.proto b/packages/core/src/protos/map_definitions.proto new file mode 100644 index 000000000..ae8ae4722 --- /dev/null +++ b/packages/core/src/protos/map_definitions.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package blitzkit; + +message MapDefinitions { + map maps = 1; +} + +message MapDefinition { + uint32 id = 1; + string name = 2; +} \ No newline at end of file diff --git a/packages/core/src/protos/tank_definitions.proto b/packages/core/src/protos/tank_definitions.proto index 7243fec14..0cda39bb3 100644 --- a/packages/core/src/protos/tank_definitions.proto +++ b/packages/core/src/protos/tank_definitions.proto @@ -41,9 +41,8 @@ message TankDefinition { } enum TankPriceType { - TANK_PRICE_TYPE_UNSPECIFIED = 0; - TANK_PRICE_TYPE_CREDITS = 1; - TANK_PRICE_TYPE_GOLD = 2; + TANK_PRICE_TYPE_CREDITS = 0; + TANK_PRICE_TYPE_GOLD = 1; } message TankPrice { @@ -162,11 +161,10 @@ message ShellDefinition { } enum ShellType { - SHELL_TYPE_UNSPECIFIED = 0; - SHELL_TYPE_AP = 1; - SHELL_TYPE_APCR = 2; - SHELL_TYPE_HEAT = 3; - SHELL_TYPE_HE = 4; + SHELL_TYPE_AP = 0; + SHELL_TYPE_APCR = 1; + SHELL_TYPE_HEAT = 2; + SHELL_TYPE_HE = 3; } message ShellPenetration { @@ -186,41 +184,37 @@ message UnlockCost { } enum ModuleType { - MODULE_TYPE_UNSPECIFIED = 0; - MODULE_TYPE_VEHICLE = 1; - MODULE_TYPE_ENGINE = 2; - MODULE_TYPE_TRACKS = 3; - MODULE_TYPE_TURRET = 4; - MODULE_TYPE_GUN = 5; + MODULE_TYPE_VEHICLE = 0; + MODULE_TYPE_ENGINE = 1; + MODULE_TYPE_TRACKS = 2; + MODULE_TYPE_TURRET = 3; + MODULE_TYPE_GUN = 4; } enum TankClass { - TANK_CLASS_UNSPECIFIED = 0; - TANK_CLASS_LIGHT = 1; - TANK_CLASS_MEDIUM = 2; - TANK_CLASS_HEAVY = 3; - TANK_CLASS_TANK_DESTROYER = 4; + TANK_CLASS_LIGHT = 0; + TANK_CLASS_MEDIUM = 1; + TANK_CLASS_HEAVY = 2; + TANK_CLASS_TANK_DESTROYER = 3; } enum Tier { - TIER_UNSPECIFIED = 0; - TIER_I = 1; - TIER_II = 2; - TIER_III = 3; - TIER_IV = 4; - TIER_V = 5; - TIER_VI = 6; - TIER_VII = 7; - TIER_VIII = 8; - TIER_IX = 9; - TIER_X = 10; + TIER_I = 0; + TIER_II = 1; + TIER_III = 2; + TIER_IV = 3; + TIER_V = 4; + TIER_VI = 5; + TIER_VII = 6; + TIER_VIII = 7; + TIER_IX = 8; + TIER_X = 9; } enum TankType { - TANK_TYPE_UNSPECIFIED = 0; - TANK_TYPE_RESEARCHABLE = 1; - TANK_TYPE_PREMIUM = 2; - TANK_TYPE_COLLECTOR = 3; + TANK_TYPE_RESEARCHABLE = 0; + TANK_TYPE_PREMIUM = 1; + TANK_TYPE_COLLECTOR = 2; } message Crew { @@ -230,10 +224,9 @@ message Crew { } enum CrewType { - CREW_TYPE_UNSPECIFIED = 0; - CREW_TYPE_COMMANDER = 1; - CREW_TYPE_RADIOMAN = 2; - CREW_TYPE_GUNNER = 3; - CREW_TYPE_DRIVER = 4; - CREW_TYPE_LOADER = 5; + CREW_TYPE_COMMANDER = 0; + CREW_TYPE_RADIOMAN = 1; + CREW_TYPE_GUNNER = 2; + CREW_TYPE_DRIVER = 3; + CREW_TYPE_LOADER = 4; } \ No newline at end of file diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 12b89b8b6..3e6ab7af7 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -1,2 +1 @@ -export * from './pbMap'; export * from './tanksStats'; diff --git a/packages/scripts/src/buildAssets/LEGACY_definitions.ts b/packages/scripts/src/buildAssets/LEGACY_definitions.ts deleted file mode 100644 index ee16f22d6..000000000 --- a/packages/scripts/src/buildAssets/LEGACY_definitions.ts +++ /dev/null @@ -1,1522 +0,0 @@ -import { - assertSecret, - CamouflageDefinitions, - ConsumableDefinitions, - ConsumableEntry, - Crew, - CrewMember, - EquipmentDefinitions, - EquipmentPreset, - EquipmentRow, - GameDefinitions, - GunDefinition, - MapDefinitions, - ModelArmor, - ModelDefinitions, - ProvisionDefinitions, - ProvisionEntry, - ShellType, - SkillDefinitions, - superCompress, - TankClass, - TankDefinitions, - TankFilterDefinitionCategory, - TankPrice, - Tier, - toUniqueId, - Unlock, -} from '@blitzkit/core'; -import { readdir } from 'fs/promises'; -import { parse as parsePath } from 'path'; -import { Vector3Tuple } from 'three'; -import { parse as parseYaml } from 'yaml'; -import { readStringDVPL } from '../core/blitz/readStringDVPL'; -import { readXMLDVPL } from '../core/blitz/readXMLDVPL'; -import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL'; -import { commitAssets } from '../core/github/commitAssets'; -import { DATA } from './constants'; -import { Avatar } from './skillIcons'; -import { TankParameters } from './tankIcons'; - -export interface BlitzStrings { - [key: string]: string; -} -type ShellKind = - | 'ARMOR_PIERCING' - | 'ARMOR_PIERCING_CR' - | 'HIGH_EXPLOSIVE' - | 'HOLLOW_CHARGE'; -type ShellDefinitionsList = Record< - string, - { - id: number; - userString: string; - icon: string; - kind: ShellKind; - caliber: number; - damage: { armor: number; devices: number }; - normalizationAngle: number; - ricochetAngle: number; - explosionRadius?: number; - } -> & { - icons: Record; -}; -interface EngineDefinitionsList { - nextAvailableId: number; - ids: Record; - shared: { - [key: string]: { - userString: string; - level: number; - fireStartingChance: number; - weight: number; - power: number; - }; - }; -} -export interface ChassisDefinitionsList { - nextAvailableId: number; - ids: Record; -} -type VehicleDefinitionArmor = Record< - string, - number | { vehicleDamageFactor: 0; '#text': number } ->; -interface UnlocksInner { - cost: number | string; - '#text': number; -} -type Unlocks = { - [key in 'vehicle' | 'engine' | 'chassis' | 'turret' | 'gun']: - | UnlocksInner - | UnlocksInner[]; -}; -type UnlocksListing = Unlocks | undefined; -interface VehicleDefinitions { - invisibility: { - moving: number; - still: number; - firePenalty: number; - }; - crew: Record; - speedLimits: { - forward: number; - backward: number; - }; - consumableSlots: number; - provisionSlots: number; - optDevicePreset: string | string[]; - hull: { - armor: VehicleDefinitionArmor; - turretPositions: { turret: string }; - turretInitialRotation?: { yaw: 0; pitch: 6.5; roll: 0 }; - maxHealth: number; - weight: number; - }; - chassis: { - [key: string]: { - unlocks: UnlocksListing; - weight: number; - terrainResistance: string; - rotationSpeed: number; - userString: string; - shotDispersionFactors: { - vehicleMovement: number; - vehicleRotation: number; - }; - level: number; - hullPosition: string; - armor: { - leftTrack: - | number - | { - chanceToHitByProjectile: number; - chanceToHitByExplosion: number; - '#text': number; - }; - rightTrack: - | number - | { - chanceToHitByProjectile: number; - chanceToHitByExplosion: number; - '#text': number; - }; - }; - }; - }; - engines: { - [key: string]: { - unlocks: UnlocksListing; - }; - }; - turrets0: { - [key: string]: { - unlocks: UnlocksListing; - rotationSpeed: number; - weight: number; - circularVisionRadius: number; - maxHealth: number; - armor: VehicleDefinitionArmor; - userString: number; - level: number; - yawLimits: string | string[]; - gunPosition: string | string[]; - models: { undamaged: string }; - hitTester: { - collisionModel: string; - }; - guns: { - [key: string]: { - unlocks: UnlocksListing; - armor: VehicleDefinitionArmor & { - gun?: - | number - | { - chanceToHitByProjectile: number; - chanceToHitByExplosion: number; - '#text': number; - }; - }; - invisibilityFactorAtShot: number | number[]; - reloadTime: number; - aimingTime?: number; - shotDispersionRadius: number; - shotDispersionFactors?: { - turretRotation: number; - afterShot: number; - whileGunDamaged: number; - }; - maxAmmo: number; - extraPitchLimits?: { - front?: string; - back?: string; - transition?: number | number[]; - }; - pitchLimits?: string | string[]; - pumpGunMode?: boolean; - pumpGunReloadTimes?: string; - clip?: { count: number; rate: number }; - models: { undamaged: string }; - }; - }; - }; - }; - extras?: { - armorsStatesController?: { - state: [ - { - id: 0; - armors: string; - type: 'default'; - enabled: true; - factorsModifiers: ''; - default: ''; - }, - { - id: 1; - armors: string; - type: 'default'; - enabled: false; - factorsModifiers: ''; - }, - ]; - }; - }; -} -export interface VehicleDefinitionList { - [key: string]: { - id: number; - userString: string; - shortUserString?: string; - description: string; - price: number | { gold: ''; '#text': number }; - sellPrice?: { gold: ''; '#text': number }; - enrichmentPermanentCost: number; - notInShop?: boolean; - tags: string; - level: number; - combatRole?: Record; - configurationModes: string; - }; -} -interface TurretDefinitionsList { - nextAvailableId: number; - ids: Record; -} -interface GunDefinitionsList { - nextAvailableId: number; - ids: Record; - shared: { - [key: string]: { - rotationSpeed: number; - weight: number; - shotDispersionFactors: { - turretRotation: number; - afterShot: number; - whileGunDamaged: number; - }; - aimingTime: number; - shotDispersionRadius: number; - userString: string; - tags: string; - level: number; - pitchLimits: string; - burst?: { count: number; rate: number }; - clip?: { count: number; rate: number }; - shots: { - [key: string]: { - speed: number; - piercingPower: string; - }; - }; - }; - }; -} - -export interface OptionalDevices { - [key: string]: { - id: number; - userString: string; - description: string; - icon: string; - script: unknown; - display_params: unknown; - }; -} - -interface OptionalDeviceSlots { - presets: { - [key: string]: { - level0: OptionalDeviceSlotRow; - level1: OptionalDeviceSlotRow; - level2: OptionalDeviceSlotRow; - }; - }; -} - -interface OptionalDeviceSlotRow { - [key: string]: { device0: string; device1: string }; -} -export interface ConsumablesCommon { - [key: string]: { - id: number; - userString: string; - description: string; - icon: string; - category: string; - tags: string; - vehicleFilter?: { - include: { vehicle: ConsumablesVehicleFilter; nations?: string }; - exclude?: { vehicle: ConsumablesVehicleFilter; nations?: string }; - }; - script: { - '#text': string; - automatic?: boolean; - cooldown: number; - duration?: number; - shotEffect?: string; - bonusValues?: { [key: string]: number }; - } & Record; - }; -} -export interface ProvisionsCommon { - [key: string]: { - id: number; - userString: string; - description: string; - icon: string; - category: string; - tags: string; - vehicleFilter?: { - include: { vehicle: ConsumablesVehicleFilter; nations?: string }; - exclude?: { vehicle: ConsumablesVehicleFilter; nations?: string }; - }; - script: { - '#text': string; - automatic?: boolean; - shotEffect?: string; - bonusValues?: { [key: string]: number }; - } & Record; - }; -} - -type CamouflagesInclude = - | { - nations?: string; - } - | { - vehicle?: { name?: string; minLevel?: number; maxLevel?: number }; - }; - -interface CamouflagesXML { - camouflages: Record< - string, - { - id: number; - userString: string; - description: string; - category: string; - group: string; - kind: string; - notInShop: boolean; - unlockCostCategory?: string; - vehicleFilter: { - include?: CamouflagesInclude | CamouflagesInclude[]; - }; - script: string[] | string; - icon?: string; - unlockPremium?: string; - unlockQuest?: string; - unlockClanLevel?: number; - unlockShopBundle?: string; - unlockTankRank?: number; - } - >; -} - -type CamouflagesYaml = Record< - string, - { - userString?: string; - shortUserString?: string; - } ->; - -type ConsumablesVehicleFilter = - | { minLevel: number; maxLevel: number } - | { name: string } - | { extendedTags: string }; - -export interface Maps { - maps: { - [key: string]: { - id: number; - tags?: string; - localName: string; - avaliableInTrainingRoom: boolean; // lol wg typo - spriteFrame: number; - supremacyPointsThreshold?: number; - availableModes: number[]; - shadowMapsAvailable?: boolean; - assaultRespawnPoints: { - allies: MapAlly[]; - enemies: MapAlly[]; - }; - levels?: number[]; - }; - }; -} - -export interface MapAlly { - respawnNumber: number; - points: Array; -} - -interface AvailableNationsYaml { - available_nations: string[]; -} - -export interface SquadBattleTypeStyles { - Prototypes: { - components: { - UIDataLocalBindingsComponent: { - data: [string, string, string][]; - }; - }; - }[]; -} - -type CombatRolesYaml = Record< - string, - { - id: number; - default_abilities: string[]; - } ->; - -const blitzShellKindToBlitzkit: Record = { - ARMOR_PIERCING: ShellType.AP, - ARMOR_PIERCING_CR: ShellType.APCR, - HIGH_EXPLOSIVE: ShellType.HE, - HOLLOW_CHARGE: ShellType.HEAT, -}; -const missingStrings: Record = { - '#artefacts:tungstentip/name': 'Tungsten Shells', -}; -export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU(R?)))$/; - -export async function definitions() { - console.log('Building definitions...'); - - const wargamingTankopedia = (await fetch( - `https://api.wotblitz.com/wotb/encyclopedia/vehicles/?application_id=${assertSecret( - process.env.PUBLIC_WARGAMING_APPLICATION_ID, - )}&fields=description`, - ).then((response) => response.json())) as { - data: { [key: number]: { description: null | string } }; - }; - - const gameDefinitions: GameDefinitions = { - version: (await readStringDVPL(`${DATA}/version.txt.dvpl`)).split(' ')[0], - nations: ( - await readYAMLDVPL( - `${DATA}/available_nations.yaml.dvpl`, - ) - ).available_nations, - gameModes: {}, - roles: {}, - }; - const tankDefinitions: TankDefinitions = {}; - const camouflageDefinitions: CamouflageDefinitions = {}; - const modelDefinitions: ModelDefinitions = {}; - const mapDefinitions: MapDefinitions = {}; - const equipmentDefinitions: EquipmentDefinitions = { - presets: {}, - equipments: {}, - }; - const consumableDefinitions: ConsumableDefinitions = {}; - const provisionDefinitions: ProvisionDefinitions = {}; - const skillDefinitions: SkillDefinitions = { - classes: { lightTank: [], mediumTank: [], heavyTank: [], 'AT-SPG': [] }, - }; - const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then( - (nations) => nations.filter((nation) => nation !== 'common'), - ); - const tankStringIdMap: Record = {}; - const stringsPreInstalled = await readYAMLDVPL( - `${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 optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>( - `${DATA}/XML/item_defs/vehicles/common/optional_devices.xml.dvpl`, - ); - const strings = { - ...stringsPreInstalled, - ...stringsCache, - }; - const optionalDeviceSlots = await readXMLDVPL<{ - root: OptionalDeviceSlots; - }>(`${DATA}/XML/item_defs/vehicles/common/optional_device_slots.xml.dvpl`); - const consumables: ConsumablesCommon = {}; - const provisions: ProvisionsCommon = {}; - - for (const match of ( - await readStringDVPL( - `${DATA}/XML/item_defs/vehicles/common/consumables/list.xml.dvpl`, - ) - ).matchAll(//g)) { - if (match[1] === 'prototypes') continue; - - Object.assign( - consumables, - ( - await readXMLDVPL<{ root: ConsumablesCommon }>( - `${DATA}/XML/item_defs/vehicles/common/consumables/${match[1]}.xml.dvpl`, - ) - ).root, - ); - } - - for (const match of ( - await readStringDVPL( - `${DATA}/XML/item_defs/vehicles/common/provisions/list.xml.dvpl`, - ) - ).matchAll(//g)) { - if (match[1] === 'prototypes') continue; - - Object.assign( - provisions, - ( - await readXMLDVPL<{ root: ConsumablesCommon }>( - `${DATA}/XML/item_defs/vehicles/common/provisions/${match[1]}.xml.dvpl`, - ) - ).root, - ); - } - - const avatar = await readXMLDVPL<{ root: Avatar }>( - `${DATA}/XML/item_defs/tankmen/avatar.xml.dvpl`, - ); - const maps = await readYAMLDVPL(`${DATA}/maps.yaml.dvpl`); - const tankXps = new Map(); - const camouflagesXml = await readXMLDVPL<{ root: CamouflagesXML }>( - `${DATA}/XML/item_defs/vehicles/common/camouflages.xml.dvpl`, - ); - const camouflagesYaml = await readYAMLDVPL( - `${DATA}/camouflages.yaml.dvpl`, - ); - const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages); - const squadBattleTypeStyles = await readYAMLDVPL( - `${DATA}/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml.dvpl`, - ); - const gameTypeSelectorStyles = await readYAMLDVPL( - `${DATA}/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml.dvpl`, - ); - const gameModeNativeNames: Record = {}; - const combatRoles = await readYAMLDVPL( - `${DATA}/XML/item_defs/vehicles/common/combat_roles.yaml.dvpl`, - ); - const consumableNativeNames: Record = {}; - const provisionNativeNames: Record = {}; - - 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 = strings[match[2]]; - - gameModeNativeNames[match[3]] = id; - gameDefinitions.gameModes[id] = { - name, - }; - } - - for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( - /eGameMode\.([a-zA-Z]+) -> "~res:\/Gfx\/UI\/Hangar\/GameTypes\/battle-type_([^"]+)"/g, - )) { - Object.entries(gameModeNativeNames).forEach(([key, value]) => { - if (key.toLowerCase() === match[2].toLowerCase()) { - gameModeNativeNames[match[1]] = value; - } - }); - } - - camouflagesXmlEntries.forEach(([camoKey, camo]) => { - const yamlEntry = camouflagesYaml[camoKey]; - const fullName = yamlEntry.userString - ? strings[yamlEntry.userString] - : undefined; - const shortName = yamlEntry.shortUserString - ? strings[yamlEntry.shortUserString] - : undefined; - const resolvedTankName = shortName ?? fullName; - const resolvedTankNameFull = - resolvedTankName === fullName ? undefined : fullName; - - camouflageDefinitions[camo.id] = { - id: camo.id, - name: strings[camo.userString], - tankName: resolvedTankName, - tankNameFull: resolvedTankNameFull, - }; - }); - - await Promise.all( - nations.map(async (nation) => { - const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>( - `${DATA}/XML/item_defs/vehicles/${nation}/list.xml.dvpl`, - ); - const turretList = await readXMLDVPL<{ - root: TurretDefinitionsList; - }>( - `${DATA}/XML/item_defs/vehicles/${nation}/components/turrets.xml.dvpl`, - ); - const gunList = await readXMLDVPL<{ - root: GunDefinitionsList; - }>(`${DATA}/XML/item_defs/vehicles/${nation}/components/guns.xml.dvpl`); - const shellList = await readXMLDVPL<{ - root: ShellDefinitionsList; - }>(`${DATA}/XML/item_defs/vehicles/${nation}/components/shells.xml.dvpl`); - const enginesList = await readXMLDVPL<{ - root: EngineDefinitionsList; - }>( - `${DATA}/XML/item_defs/vehicles/${nation}/components/engines.xml.dvpl`, - ); - const chassisList = await readXMLDVPL<{ - root: ChassisDefinitionsList; - }>( - `${DATA}/XML/item_defs/vehicles/${nation}/components/chassis.xml.dvpl`, - ); - - function resolveUnlocks(unlocks?: Unlocks) { - if (!unlocks) return undefined; - - return Object.entries(unlocks) - .map(([type, list]) => - (Array.isArray(list) ? list : [list]).map((item) => { - const typeTyped = type as keyof Unlocks; - let rawId: number; - - if (typeTyped === 'chassis') { - rawId = chassisList.root.ids[item['#text']]; - } else if (typeTyped === 'engine') { - rawId = enginesList.root.ids[item['#text']]; - } else if (typeTyped === 'gun') { - rawId = gunList.root.ids[item['#text']]; - } else if (typeTyped === 'turret') { - rawId = turretList.root.ids[item['#text']]; - } else if (typeTyped === 'vehicle') { - rawId = tankList.root[item['#text']].id; - } - - return { - type: typeTyped, - id: toUniqueId(nation, rawId!), - cost: { - type: - typeof item.cost === 'number' - ? 'xp' - : item.cost.split(':')[0], - value: - typeof item.cost === 'number' - ? item.cost - : Number(item.cost.split(':')[1]), - }, - } satisfies Unlock; - }), - ) - .flat(); - } - - for (const tankKey in tankList.root) { - if (botPattern.test(tankKey)) continue; - - const gunXps = new Map(); - const turretXps = new Map(); - const engineXps = new Map(); - const trackXps = new Map(); - const tank = tankList.root[tankKey]; - let tankPrice: TankPrice; - const tankDefinition = await readXMLDVPL<{ root: VehicleDefinitions }>( - `${DATA}/XML/item_defs/vehicles/${nation}/${tankKey}.xml.dvpl`, - ); - const tankParameters = await readYAMLDVPL( - `${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml.dvpl`, - ); - const turretOrigin = tankDefinition.root.hull.turretPositions.turret - .split(' ') - .map(Number) as Vector3Tuple; - const tankId = toUniqueId(nation, tank.id); - const tankTags = tank.tags.split(' '); - const hullArmor: ModelArmor = { thickness: {} }; - const equipment = tankDefinition.root.optDevicePreset; - tankStringIdMap[`${nation}:${tankKey}`] = tankId; - - if (tank.sellPrice) { - tankPrice = { - type: 'gold', - value: tank.sellPrice['#text'] * 2, - }; - } else if (typeof tank.price === 'number') { - tankPrice = { - type: 'credits', - value: tank.price, - }; - } else { - tankPrice = { - type: 'credits', - value: tank.price['#text'] * 400, - }; - } - - Object.keys(tankDefinition.root.hull.armor) - .filter((name) => name.startsWith('armor_')) - .forEach((name) => { - const armorIdString = name.match(/armor_(\d+)/)?.[1]; - - if (armorIdString === undefined) { - throw new SyntaxError(`Invalid armor id: ${name}`); - } - - const armorId = parseInt(armorIdString); - const armorRaw = tankDefinition.root.hull.armor[name]; - - if (typeof armorRaw === 'number') { - hullArmor.thickness[armorId] = armorRaw; - } else { - if (!hullArmor.spaced) hullArmor.spaced = []; - hullArmor.thickness[armorId] = armorRaw['#text']; - hullArmor.spaced.push(armorId); - } - }); - const crew: Crew[] = []; - const fixedCamouflage = tankTags.includes('eventCamouflage_user'); - const totalUnlocks: UnlocksListing[] = []; - - Object.entries(tankDefinition.root.crew).forEach(([crewKey, value]) => { - let entry: Crew; - const index = crew.findIndex(({ type }) => type === crewKey); - if (index === -1) { - entry = { type: crewKey as CrewMember }; - crew.push(entry); - } else { - entry = crew[index]; - } - - if (typeof value === 'string') { - if (entry.count === undefined) { - entry.count = 1; - } else { - entry.count++; - } - - if (value !== '') { - entry.substitute = value - .split('\n') - .map((member) => member.trim()) as CrewMember[]; - } - } else { - if (entry.count === undefined) { - entry.count = value.length; - } else { - entry.count += value.length; - } - } - - if (entry.count === 1) { - delete entry.count; - } - }); - - const camouflages = camouflagesXmlEntries - .filter(([, camo]) => { - if (!camo.vehicleFilter.include) return false; - if (camo.unlockCostCategory !== 'legendary-skins-gold') - return false; - - const includeArray = Array.isArray(camo.vehicleFilter.include) - ? camo.vehicleFilter.include - : [camo.vehicleFilter.include]; - - return includeArray.some((filter) => { - if ('vehicle' in filter && filter.vehicle?.name) { - return filter.vehicle.name === `${nation}:${tankKey}`; - } - - return false; - }); - }) - .map(([, camo]) => camo.id); - - tankDefinitions[tankId] = { - id: tankId, - roles: {}, - camouflages: camouflages.length === 0 ? undefined : camouflages, - description: - wargamingTankopedia.data[tankId]?.description ?? undefined, - fixedCamouflage: fixedCamouflage ? true : undefined, - crew, - weight: tankDefinition.root.hull.weight, - health: tankDefinition.root.hull.maxHealth, - speed: { - forwards: tankDefinition.root.speedLimits.forward, - backwards: tankDefinition.root.speedLimits.backward, - }, - equipment_preset: - typeof equipment === 'string' ? equipment : equipment.at(-1)!, - consumables: tankDefinition.root.consumableSlots, - provisions: tankDefinition.root.provisionSlots, - name: - (tank.shortUserString - ? strings[tank.shortUserString] - : undefined) ?? strings[tank.userString], - nameFull: strings[tank.userString], - nation, - treeType: tankTags.includes('collectible') - ? 'collector' - : (typeof tank.price === 'number' ? false : 'gold' in tank.price) - ? 'premium' - : 'researchable', - tier: tank.level as Tier, - class: tankTags[0] as TankClass, - testing: tankTags.includes('testTank'), - deprecated: tankTags.includes('deprecated'), - price: tankPrice, - camouflage: { - still: tankDefinition.root.invisibility.still, - moving: tankDefinition.root.invisibility.moving, - onFire: tankDefinition.root.invisibility.firePenalty, - }, - turrets: [], - engines: [], - tracks: [], - }; - - if (tank.combatRole) { - Object.entries(tank.combatRole).forEach(([gameMode, role]) => { - const id = Object.entries(gameModeNativeNames).find( - ([key]) => key.toLowerCase() === gameMode.toLowerCase(), - )?.[1]; - - if (id === undefined) { - throw new Error( - `Unknown game mode in tank ${tankKey}: ${gameMode}`, - ); - } - - tankDefinitions[tankId].roles[id] = combatRoles[role].id; - }); - } - - if (tankDefinitions[tankId].name === tankDefinitions[tankId].nameFull) { - delete tankDefinitions[tankId].nameFull; - } - - modelDefinitions[tankId] = { - armor: hullArmor, - turretOrigin, - turretRotation: tankDefinition.root.hull.turretInitialRotation - ? { - yaw: tankDefinition.root.hull.turretInitialRotation.yaw, - pitch: tankDefinition.root.hull.turretInitialRotation.pitch, - roll: tankDefinition.root.hull.turretInitialRotation.roll, - } - : undefined, - boundingBox: tankParameters.collision.hull.bbox, - turrets: {}, - tracks: {}, - }; - - Object.keys(tankDefinition.root.chassis).forEach((key) => { - const track = tankDefinition.root.chassis[key]; - const trackId = toUniqueId(nation, chassisList.root.ids[key]); - const terrainResistances = track.terrainResistance - .split(' ') - .map(Number); - const trackArmorRaw = track.armor.leftTrack; - const hullOrigin = track.hullPosition - .split(' ') - .map(Number) as Vector3Tuple; - - totalUnlocks.push(track.unlocks); - tankDefinitions[tankId].tracks.push({ - id: trackId, - weight: track.weight, - name: strings[track.userString], - traverse_speed: track.rotationSpeed, - dispersion: { - move: track.shotDispersionFactors.vehicleMovement, - traverse: track.shotDispersionFactors.vehicleRotation, - }, - resistance: { - hard: terrainResistances[0], - medium: terrainResistances[1], - soft: terrainResistances[2], - }, - tier: track.level as Tier, - unlocks: resolveUnlocks(track.unlocks), - }); - - modelDefinitions[tankId].tracks[trackId] = { - thickness: - typeof trackArmorRaw === 'number' - ? trackArmorRaw - : trackArmorRaw['#text'], - origin: hullOrigin, - }; - }); - - Object.keys(tankDefinition.root.engines).forEach((engineKey) => { - const engine = tankDefinition.root.engines[engineKey]; - const engineListEntry = enginesList.root.shared[engineKey]; - const engineId = toUniqueId(nation, enginesList.root.ids[engineKey]); - - totalUnlocks.push(engine.unlocks); - tankDefinitions[tankId].engines.push({ - id: engineId, - name: strings[engineListEntry.userString], - fire_chance: engineListEntry.fireStartingChance, - tier: engineListEntry.level as Tier, - weight: engineListEntry.weight, - power: engineListEntry.power, - unlocks: resolveUnlocks(engine.unlocks), - }); - }); - - Object.keys(tankDefinition.root.turrets0).forEach( - (turretKey, turretIndex) => { - const turret = tankDefinition.root.turrets0[turretKey]; - const turretModel = Number( - parsePath(turret.models.undamaged).name.split('_')[1], - ); - const turretId = toUniqueId(nation, turretList.root.ids[turretKey]); - const turretYaw = ( - typeof turret.yawLimits === 'string' - ? turret.yawLimits - : turret.yawLimits.at(-1)! - ) - .split(' ') - .map(Number) as [number, number]; - const gunOrigin = ( - typeof turret.gunPosition === 'string' - ? turret.gunPosition - : turret.gunPosition[0] - ) - .split(' ') - .map(Number) as Vector3Tuple; - const turretArmor: ModelArmor = { thickness: {} }; - - totalUnlocks.push(turret.unlocks); - Object.keys(turret.armor) - .filter((name) => name.startsWith('armor_')) - .forEach((name) => { - const armorIdString = name.match(/armor_(\d+)/)?.[1]; - - if (armorIdString === undefined) { - throw new SyntaxError(`Invalid armor id: ${name}`); - } - - const armorId = parseInt(armorIdString); - const armorRaw = turret.armor[name]; - - if (typeof armorRaw === 'number') { - turretArmor.thickness[armorId] = armorRaw; - } else { - if (!turretArmor.spaced) turretArmor.spaced = []; - turretArmor.thickness[armorId] = armorRaw['#text']; - turretArmor.spaced.push(armorId); - } - }); - - tankDefinitions[tankId].turrets.push({ - id: turretId, - traverseSpeed: turret.rotationSpeed, - name: - strings[turret.userString] ?? - turretKey - .replaceAll('_', ' ') - .replace(/^(Turret ([0-9] )?)+/, ''), - tier: turret.level as Tier, - guns: [], - health: turret.maxHealth, - viewRange: turret.circularVisionRadius, - weight: turret.weight, - unlocks: resolveUnlocks(turret.unlocks), - }); - - modelDefinitions[tankId].turrets[turretId] = { - armor: turretArmor, - gunOrigin, - model: turretModel, - yaw: - -turretYaw[0] + turretYaw[1] === 360 - ? undefined - : { min: turretYaw[0], max: turretYaw[1] }, - guns: {}, - boundingBox: - tankParameters.collision[ - parsePath(turret.hitTester.collisionModel).name.toLowerCase() - ].bbox, - }; - - Object.keys(turret.guns).forEach((gunKey, gunIndex) => { - const gun = turret.guns[gunKey]; - const gunId = toUniqueId(nation, gunList.root.ids[gunKey]); - const gunListEntry = gunList.root.shared[gunKey]; - const pitchLimitsRaw = - gun.pitchLimits ?? gunListEntry.pitchLimits; - const gunPitch = ( - typeof pitchLimitsRaw === 'string' - ? pitchLimitsRaw - : pitchLimitsRaw.at(-1)! - ) - .split(' ') - .map(Number) as [number, number]; - const gunModel = Number( - parsePath(gun.models.undamaged).name.split('_')[1], - ); - const gunName = - strings[gunListEntry.userString] ?? gunKey.replaceAll('_', ' '); - const gunType = - 'clip' in gun - ? gun.pumpGunMode - ? 'autoReloader' - : 'autoLoader' - : 'regular'; - const gunReload = - gunType === 'autoReloader' - ? gun.pumpGunReloadTimes!.split(' ').map(Number) - : gun.reloadTime; - const gunClipCount = - gunType === 'regular' ? undefined : gun.clip!.count; - const gunIntraClip = - gunType === 'regular' ? undefined : 60 / gun.clip!.rate; - const front = gun.extraPitchLimits?.front - ? gun.extraPitchLimits.front.split(' ').map(Number) - : undefined; - const back = gun.extraPitchLimits?.back - ? gun.extraPitchLimits.back.split(' ').map(Number) - : undefined; - const transition = gun.extraPitchLimits?.transition - ? typeof gun.extraPitchLimits.transition === 'number' - ? gun.extraPitchLimits.transition - : gun.extraPitchLimits.transition.at(-1)! - : undefined; - const gunArmor: ModelArmor = { thickness: {} }; - const shotDispersionFactors = - gun.shotDispersionFactors ?? gunListEntry.shotDispersionFactors; - const maskName = `gun_${gunModel.toString().padStart(2, '0')}`; - const maskEnabled = - tankParameters.maskSlice?.[maskName]?.enabled ?? false; - let mask: number | undefined; - - if (maskEnabled) { - const maskRaw = tankParameters.maskSlice![maskName]!; - mask = maskRaw.planePosition[1]; - } else { - mask = undefined; - } - - totalUnlocks.push(gun.unlocks); - Object.keys(gun.armor) - .filter((name) => name.startsWith('armor_')) - .forEach((name) => { - const armorIdString = name.match(/armor_(\d+)/)?.[1]; - if (armorIdString === undefined) { - throw new SyntaxError(`Invalid armor id: ${name}`); - } - const armorId = parseInt(armorIdString); - const armorRaw = gun.armor[name]; - if (typeof armorRaw === 'number') { - gunArmor.thickness[armorId] = armorRaw; - } else { - if (!gunArmor.spaced) gunArmor.spaced = []; - gunArmor.thickness[armorId] = armorRaw['#text']; - gunArmor.spaced.push(armorId); - } - }); - - tankDefinitions[tankId].turrets[turretIndex].guns.push({ - id: gunId, - weight: gunListEntry.weight, - rotationSpeed: gunListEntry.rotationSpeed, - name: gunName, - tier: gunListEntry.level as Tier, - shells: [], - type: gunType, - reload: gunReload, - count: gunClipCount, - intraClip: gunIntraClip, - camouflageLoss: - typeof gun.invisibilityFactorAtShot === 'number' - ? gun.invisibilityFactorAtShot - : gun.invisibilityFactorAtShot.at(-1)!, - aimTime: gun.aimingTime ?? gunListEntry.aimingTime, - dispersion: { - base: - gun.shotDispersionRadius ?? - gunListEntry.shotDispersionRadius, - damaged: shotDispersionFactors.whileGunDamaged, - shot: shotDispersionFactors.afterShot, - traverse: shotDispersionFactors.turretRotation, - }, - unlocks: resolveUnlocks(gun.unlocks), - } as GunDefinition); - - modelDefinitions[tankId].turrets[turretId].guns[gunId] = { - armor: gunArmor, - model: gunModel, - mask, - thickness: - gun.armor.gun === undefined - ? 0 - : typeof gun.armor.gun === 'number' - ? gun.armor.gun - : gun.armor.gun['#text'], - pitch: { - min: gunPitch[0], - max: gunPitch[1], - - front: front - ? { - min: front[0], - max: front[1], - range: front[2], - } - : undefined, - back: back - ? { - min: back[0], - max: back[1], - range: back[2], - } - : undefined, - transition, - }, - }; - - Object.keys(gunListEntry.shots).forEach((shellKey) => { - const gunShellEntry = gunListEntry.shots[shellKey]; - const shell = shellList.root[shellKey]; - const shellId = toUniqueId(nation, shell.id); - const shellName = - strings[shell.userString] ?? shellKey.replaceAll('_', ' '); - const penetrationRaw = gunShellEntry.piercingPower - .split(' ') - .filter((penetrationString) => penetrationString !== '') - .map(Number); - - tankDefinitions[tankId].turrets[turretIndex].guns[ - gunIndex - ].shells.push({ - id: shellId, - name: shellName, - speed: gunShellEntry.speed, - damage: { - armor: shell.damage.armor, - module: shell.damage.devices, - }, - caliber: shell.caliber, - normalization: shell.normalizationAngle, - ricochet: shell.ricochetAngle, - type: blitzShellKindToBlitzkit[shell.kind], - explosionRadius: - shell.kind === 'HIGH_EXPLOSIVE' - ? (shell.explosionRadius ?? 0) - : undefined, - icon: shell.icon, - penetration: - penetrationRaw[0] === penetrationRaw[1] - ? penetrationRaw[0] - : [penetrationRaw[0], penetrationRaw[1]], - }); - }); - }); - }, - ); - - totalUnlocks.forEach((unlocks) => { - if (unlocks === undefined) return; - - Object.entries(unlocks).forEach(([key, value]) => { - (Array.isArray(value) ? value : [value]).forEach((vehicle) => { - switch (key as keyof Unlocks) { - case 'vehicle': { - const tankListEntry = tankList.root[vehicle['#text']]; - const currentTank = tankDefinitions[tankId]; - const successorId = toUniqueId(nation, tankListEntry.id); - - tankXps.set(successorId, vehicle.cost as number); - - if (currentTank.successors === undefined) { - currentTank.successors = []; - } - if (!currentTank.successors!.includes(successorId)) { - currentTank.successors!.push(successorId); - } - break; - } - - case 'gun': { - gunXps.set( - toUniqueId(nation, gunList.root.ids[vehicle['#text']]), - vehicle.cost as number, - ); - break; - } - - case 'turret': { - turretXps.set( - toUniqueId(nation, turretList.root.ids[vehicle['#text']]), - vehicle.cost as number, - ); - break; - } - - case 'engine': { - engineXps.set( - toUniqueId(nation, enginesList.root.ids[vehicle['#text']]), - vehicle.cost as number, - ); - break; - } - - case 'chassis': { - trackXps.set( - toUniqueId(nation, chassisList.root.ids[vehicle['#text']]), - vehicle.cost as number, - ); - break; - } - } - }); - }); - }); - - Object.values(tankDefinitions[tankId].turrets).forEach((turret) => { - turret.xp = turretXps.get(turret.id); - - Object.values(turret.guns).forEach((gun) => { - gun.xp = gunXps.get(gun.id); - }); - }); - - Object.values(tankDefinitions[tankId].engines).forEach((engine) => { - engine.xp = engineXps.get(engine.id); - }); - - Object.values(tankDefinitions[tankId].tracks).forEach((track) => { - track.xp = trackXps.get(track.id); - }); - } - }), - ); - - Object.values(tankDefinitions).forEach((tank) => { - tank.xp = tankXps.get(tank.id); - }); - - Object.values(tankDefinitions).forEach((tank) => { - tank.successors?.forEach((predecessorId) => { - if (tankDefinitions[predecessorId].ancestors === undefined) { - tankDefinitions[predecessorId].ancestors = []; - } - - if (!tankDefinitions[predecessorId].ancestors?.includes(tank.id)) { - tankDefinitions[predecessorId].ancestors?.push(tank.id); - } - }); - }); - - Object.entries(optionalDevices.root).forEach( - ([optionalDeviceKey, optionalDeviceEntry]) => { - if (optionalDeviceKey === 'nextAvailableId') return; - - equipmentDefinitions.equipments[optionalDeviceEntry.id] = { - name: strings[optionalDeviceEntry.userString], - description: strings[optionalDeviceEntry.description], - }; - }, - ); - - Object.entries(optionalDeviceSlots.root.presets).forEach( - ([optionalDeviceSlotKey, optionalDeviceSlotEntry]) => { - if (optionalDeviceSlotKey === 'emptyPreset') return; - - equipmentDefinitions.presets[optionalDeviceSlotKey] = Object.values( - optionalDeviceSlotEntry, - ).map( - (level) => - Object.values(level).map((options) => { - return [ - optionalDevices.root[options.device0].id, - optionalDevices.root[options.device1].id, - ]; - }) as EquipmentRow, - ) as EquipmentPreset; - }, - ); - - Object.entries(consumables).forEach(([key, consumable]) => { - consumableNativeNames[key] = consumable.id; - - const entry = { - id: consumable.id, - gameMode: 'gameModeFilter' in consumable, - cooldown: consumable.script.cooldown, - duration: consumable.script.duration, - name: - strings[consumable.userString] ?? - missingStrings[consumable.userString] ?? - `Unknown ${consumable.id}`, - } as ConsumableEntry; - consumableDefinitions[consumable.id] = entry; - - const includeRaw = consumable.vehicleFilter?.include.vehicle; - const excludeRaw = consumable.vehicleFilter?.exclude?.vehicle; - - if (!entry.gameMode) { - if (includeRaw) { - entry.include = []; - - if ('minLevel' in includeRaw) { - entry.include.push({ - type: 'tier', - min: includeRaw.minLevel, - max: includeRaw.maxLevel, - }); - } else if ('name' in includeRaw) { - entry.include.push({ - type: 'ids', - ids: includeRaw.name.split(' ').map((key) => tankStringIdMap[key]), - }); - } else throw new SyntaxError('Unhandled include type'); - - if (consumable.vehicleFilter?.include.nations) { - entry.include.push({ - type: 'nation', - nations: consumable.vehicleFilter.include.nations.split(' '), - }); - } - } - - if (excludeRaw) { - entry.exclude = []; - - if ('name' in excludeRaw) { - entry.exclude!.push({ - type: 'ids', - ids: excludeRaw.name.split(' ').map((key) => tankStringIdMap[key]), - }); - } else if ('extendedTags' in excludeRaw) { - entry.exclude!.push({ - type: 'category', - categories: excludeRaw.extendedTags.split( - ' ', - ) as TankFilterDefinitionCategory[], - }); - } else throw new SyntaxError('Unhandled exclude type'); - - if (consumable.vehicleFilter?.exclude?.nations) { - entry.exclude!.push({ - type: 'nation', - nations: consumable.vehicleFilter.exclude.nations.split(' '), - }); - } - } - } - }); - - Object.entries(provisions).forEach(([key, provision]) => { - provisionNativeNames[key] = provision.id; - - const entry = { - id: provision.id, - gameMode: 'gameModeFilter' in provision, - name: - strings[provision.userString] ?? - missingStrings[provision.userString] ?? - `Unknown ${provision.id}`, - } as ProvisionEntry; - provisionDefinitions[provision.id] = entry; - - const includeRaw = provision.vehicleFilter?.include.vehicle; - const excludeRaw = provision.vehicleFilter?.exclude?.vehicle; - - if (!entry.gameMode) { - if (includeRaw) { - entry.include = []; - - if ('minLevel' in includeRaw) { - entry.include.push({ - type: 'tier', - min: includeRaw.minLevel, - max: includeRaw.maxLevel, - }); - } else if ('name' in includeRaw) { - entry.include.push({ - type: 'ids', - ids: includeRaw.name.split(' ').map((key) => tankStringIdMap[key]), - }); - } else throw new SyntaxError('Unhandled include type'); - - if (provision.vehicleFilter?.include.nations) { - entry.include.push({ - type: 'nation', - nations: provision.vehicleFilter.include.nations.split(' '), - }); - } - } - - if (excludeRaw) { - entry.exclude = []; - - if ('name' in excludeRaw) { - entry.exclude!.push({ - type: 'ids', - ids: excludeRaw.name.split(' ').map((key) => tankStringIdMap[key]), - }); - } else if ('extendedTags' in excludeRaw) { - entry.exclude!.push({ - type: 'category', - categories: excludeRaw.extendedTags.split( - ' ', - ) as TankFilterDefinitionCategory[], - }); - } else throw new SyntaxError('Unhandled exclude type'); - - if (provision.vehicleFilter?.exclude?.nations) { - entry.exclude!.push({ - type: 'nation', - nations: provision.vehicleFilter.exclude.nations.split(' '), - }); - } - } - } - - if (provision.script.bonusValues?.crewLevelIncrease !== undefined) { - provisionDefinitions[provision.id].crew = - provision.script.bonusValues?.crewLevelIncrease; - } - }); - - Object.entries(combatRoles).forEach(([, value]) => { - gameDefinitions.roles[value.id] = { provisions: [], consumables: [] }; - - value.default_abilities.forEach((ability) => { - if (ability in consumableNativeNames) { - gameDefinitions.roles[value.id].consumables.push( - consumableNativeNames[ability], - ); - } else if (ability in provisionNativeNames) { - gameDefinitions.roles[value.id].provisions.push( - provisionNativeNames[ability], - ); - } else throw new Error(`Unknown ability ${ability}`); - }); - }); - - Object.entries(avatar.root.skillsByClasses).forEach(([tankClass, skills]) => { - skillDefinitions.classes[tankClass as TankClass] = skills.split(' '); - }); - - Object.entries(maps.maps).forEach(([key, map]) => { - mapDefinitions[map.id] = { - id: map.id, - name: strings[`#maps:${key}:${map.localName}`], - }; - }); - - await commitAssets('definitions', [ - { - content: superCompress(gameDefinitions), - encoding: 'base64', - path: 'definitions/game.cdon.lz4', - }, - { - content: superCompress(tankDefinitions), - encoding: 'base64', - path: 'definitions/tanks.cdon.lz4', - }, - { - content: superCompress(modelDefinitions), - encoding: 'base64', - path: 'definitions/models.cdon.lz4', - }, - { - content: superCompress(equipmentDefinitions), - encoding: 'base64', - path: 'definitions/equipment.cdon.lz4', - }, - { - content: superCompress(consumableDefinitions), - encoding: 'base64', - path: 'definitions/consumables.cdon.lz4', - }, - { - content: superCompress(provisionDefinitions), - encoding: 'base64', - path: 'definitions/provisions.cdon.lz4', - }, - { - content: superCompress(skillDefinitions), - encoding: 'base64', - path: 'definitions/skills.cdon.lz4', - }, - { - content: superCompress(mapDefinitions), - encoding: 'base64', - path: 'definitions/maps.cdon.lz4', - }, - { - content: superCompress(camouflageDefinitions), - encoding: 'base64', - path: 'definitions/camouflages.cdon.lz4', - }, - ]); -} diff --git a/packages/scripts/src/buildAssets/definitions.ts b/packages/scripts/src/buildAssets/definitions.ts index 9cdf488d8..cec78d613 100644 --- a/packages/scripts/src/buildAssets/definitions.ts +++ b/packages/scripts/src/buildAssets/definitions.ts @@ -1,52 +1,1627 @@ -import { TankClass, TankDefinitions, TankPriceType } from '@blitzkit/core'; +import { + Armor, + assertSecret, + CamouflageDefinitions, + Consumable, + ConsumableDefinitions, + ConsumableTankCategoryFilterCategory, + Crew, + CrewType, + encodePB64, + EquipmentDefinitions, + EquipmentSlot, + GameDefinitions, + GunDefinition, + MapDefinitions, + ModelDefinitions, + ModuleType, + Provision, + ProvisionDefinitions, + ShellType, + SkillDefinitions, + TankClass, + TankDefinitions, + TankPrice, + TankPriceType, + TankType, + Tier, + toUniqueId, + Unlock, + Vector3, +} from '@blitzkit/core'; +import { readdir } from 'fs/promises'; +import { parse as parsePath } from 'path'; +import { Vector3Tuple } from 'three'; +import { parse as parseYaml } from 'yaml'; +import { readStringDVPL } from '../core/blitz/readStringDVPL'; +import { readXMLDVPL } from '../core/blitz/readXMLDVPL'; +import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL'; +import { commitAssets } from '../core/github/commitAssets'; +import { DATA } from './constants'; +import { Avatar } from './skillIcons'; +import { TankParameters } from './tankIcons'; + +type BlitzTankFilterDefinitionCategory = 'clip'; +const blitzTankFilterDefinitionCategoryToBlitzkit: Record< + BlitzTankFilterDefinitionCategory, + ConsumableTankCategoryFilterCategory +> = { + clip: ConsumableTankCategoryFilterCategory.TANK_CATEGORY_CLIP, +}; +function vector3TupleToBlitzkit(tuple: Vector3Tuple) { + return { x: tuple[0], y: tuple[1], z: tuple[2] } satisfies Vector3; +} +type BlitzTankClass = 'lightTank' | 'mediumTank' | 'heavyTank' | 'AT-SPG'; +const blitzTankClassToBlitzkit: Record = { + lightTank: TankClass.TANK_CLASS_LIGHT, + 'AT-SPG': TankClass.TANK_CLASS_TANK_DESTROYER, + heavyTank: TankClass.TANK_CLASS_HEAVY, + mediumTank: TankClass.TANK_CLASS_MEDIUM, +}; +export interface BlitzStrings { + [key: string]: string; +} +type ShellKind = + | 'ARMOR_PIERCING' + | 'ARMOR_PIERCING_CR' + | 'HIGH_EXPLOSIVE' + | 'HOLLOW_CHARGE'; +type ShellDefinitionsList = Record< + string, + { + id: number; + userString: string; + icon: string; + kind: ShellKind; + caliber: number; + damage: { armor: number; devices: number }; + normalizationAngle: number; + ricochetAngle: number; + explosionRadius?: number; + } +> & { + icons: Record; +}; +interface EngineDefinitionsList { + nextAvailableId: number; + ids: Record; + shared: { + [key: string]: { + userString: string; + level: number; + fireStartingChance: number; + weight: number; + power: number; + }; + }; +} +export interface ChassisDefinitionsList { + nextAvailableId: number; + ids: Record; +} +type VehicleDefinitionArmor = Record< + string, + number | { vehicleDamageFactor: 0; '#text': number } +>; +interface UnlocksInner { + cost: number | string; + '#text': number; +} +type BlitzModuleType = { + [key in 'vehicle' | 'engine' | 'chassis' | 'turret' | 'gun']: + | UnlocksInner + | UnlocksInner[]; +}; +type UnlocksListing = BlitzModuleType | undefined; +type BlitzCrewType = 'commander' | 'radioman' | 'gunner' | 'driver' | 'loader'; +const blitzCrewTypeToBlitzkit: Record = { + commander: CrewType.CREW_TYPE_COMMANDER, + driver: CrewType.CREW_TYPE_DRIVER, + gunner: CrewType.CREW_TYPE_GUNNER, + loader: CrewType.CREW_TYPE_LOADER, + radioman: CrewType.CREW_TYPE_RADIOMAN, +}; +const blitzkitCrewTypeToBlitz: Record = { + [CrewType.CREW_TYPE_COMMANDER]: 'commander', + [CrewType.CREW_TYPE_DRIVER]: 'driver', + [CrewType.CREW_TYPE_GUNNER]: 'gunner', + [CrewType.CREW_TYPE_LOADER]: 'loader', + [CrewType.CREW_TYPE_RADIOMAN]: 'radioman', + + // this should never happen... i hope 😢 + [CrewType.UNRECOGNIZED]: 'radioman', +}; +interface VehicleDefinitions { + invisibility: { + moving: number; + still: number; + firePenalty: number; + }; + crew: Record; + speedLimits: { + forward: number; + backward: number; + }; + consumableSlots: number; + provisionSlots: number; + optDevicePreset: string | string[]; + hull: { + armor: VehicleDefinitionArmor; + turretPositions: { turret: string }; + turretInitialRotation?: { yaw: 0; pitch: 6.5; roll: 0 }; + maxHealth: number; + weight: number; + }; + chassis: { + [key: string]: { + unlocks: UnlocksListing; + weight: number; + terrainResistance: string; + rotationSpeed: number; + userString: string; + shotDispersionFactors: { + vehicleMovement: number; + vehicleRotation: number; + }; + level: number; + hullPosition: string; + armor: { + leftTrack: + | number + | { + chanceToHitByProjectile: number; + chanceToHitByExplosion: number; + '#text': number; + }; + rightTrack: + | number + | { + chanceToHitByProjectile: number; + chanceToHitByExplosion: number; + '#text': number; + }; + }; + }; + }; + engines: { + [key: string]: { + unlocks: UnlocksListing; + }; + }; + turrets0: { + [key: string]: { + unlocks: UnlocksListing; + rotationSpeed: number; + weight: number; + circularVisionRadius: number; + maxHealth: number; + armor: VehicleDefinitionArmor; + userString: number; + level: number; + yawLimits: string | string[]; + gunPosition: string | string[]; + models: { undamaged: string }; + hitTester: { + collisionModel: string; + }; + guns: { + [key: string]: { + unlocks: UnlocksListing; + armor: VehicleDefinitionArmor & { + gun?: + | number + | { + chanceToHitByProjectile: number; + chanceToHitByExplosion: number; + '#text': number; + }; + }; + invisibilityFactorAtShot: number | number[]; + reloadTime: number; + aimingTime?: number; + shotDispersionRadius: number; + shotDispersionFactors?: { + turretRotation: number; + afterShot: number; + whileGunDamaged: number; + }; + maxAmmo: number; + extraPitchLimits?: { + front?: string; + back?: string; + transition?: number | number[]; + }; + pitchLimits?: string | string[]; + pumpGunMode?: boolean; + pumpGunReloadTimes?: string; + clip?: { count: number; rate: number }; + models: { undamaged: string }; + }; + }; + }; + }; + extras?: { + armorsStatesController?: { + state: [ + { + id: 0; + armors: string; + type: 'default'; + enabled: true; + factorsModifiers: ''; + default: ''; + }, + { + id: 1; + armors: string; + type: 'default'; + enabled: false; + factorsModifiers: ''; + }, + ]; + }; + }; +} +export interface VehicleDefinitionList { + [key: string]: { + id: number; + userString: string; + shortUserString?: string; + description: string; + price: number | { gold: ''; '#text': number }; + sellPrice?: { gold: ''; '#text': number }; + enrichmentPermanentCost: number; + notInShop?: boolean; + tags: string; + level: number; + combatRole?: Record; + configurationModes: string; + }; +} +interface TurretDefinitionsList { + nextAvailableId: number; + ids: Record; +} +interface GunDefinitionsList { + nextAvailableId: number; + ids: Record; + shared: { + [key: string]: { + rotationSpeed: number; + weight: number; + shotDispersionFactors: { + turretRotation: number; + afterShot: number; + whileGunDamaged: number; + }; + aimingTime: number; + shotDispersionRadius: number; + userString: string; + tags: string; + level: number; + pitchLimits: string; + burst?: { count: number; rate: number }; + clip?: { count: number; rate: number }; + shots: { + [key: string]: { + speed: number; + piercingPower: string; + }; + }; + }; + }; +} + +export interface OptionalDevices { + [key: string]: { + id: number; + userString: string; + description: string; + icon: string; + script: unknown; + display_params: unknown; + }; +} + +interface OptionalDeviceSlots { + presets: { + [key: string]: { + level0: OptionalDeviceSlotRow; + level1: OptionalDeviceSlotRow; + level2: OptionalDeviceSlotRow; + }; + }; +} + +interface OptionalDeviceSlotRow { + [key: string]: { device0: string; device1: string }; +} +export interface ConsumablesCommon { + [key: string]: { + id: number; + userString: string; + description: string; + icon: string; + category: string; + tags: string; + vehicleFilter?: { + include: { vehicle: ConsumablesVehicleFilter; nations?: string }; + exclude?: { vehicle: ConsumablesVehicleFilter; nations?: string }; + }; + script: { + '#text': string; + automatic?: boolean; + cooldown: number; + duration?: number; + shotEffect?: string; + bonusValues?: { [key: string]: number }; + } & Record; + }; +} +export interface ProvisionsCommon { + [key: string]: { + id: number; + userString: string; + description: string; + icon: string; + category: string; + tags: string; + vehicleFilter?: { + include: { vehicle: ConsumablesVehicleFilter; nations?: string }; + exclude?: { vehicle: ConsumablesVehicleFilter; nations?: string }; + }; + script: { + '#text': string; + automatic?: boolean; + shotEffect?: string; + bonusValues?: { [key: string]: number }; + } & Record; + }; +} + +type CamouflagesInclude = + | { + nations?: string; + } + | { + vehicle?: { name?: string; minLevel?: number; maxLevel?: number }; + }; + +interface CamouflagesXML { + camouflages: Record< + string, + { + id: number; + userString: string; + description: string; + category: string; + group: string; + kind: string; + notInShop: boolean; + unlockCostCategory?: string; + vehicleFilter: { + include?: CamouflagesInclude | CamouflagesInclude[]; + }; + script: string[] | string; + icon?: string; + unlockPremium?: string; + unlockQuest?: string; + unlockClanLevel?: number; + unlockShopBundle?: string; + unlockTankRank?: number; + } + >; +} + +type CamouflagesYaml = Record< + string, + { + userString?: string; + shortUserString?: string; + } +>; + +type ConsumablesVehicleFilter = + | { minLevel: number; maxLevel: number } + | { name: string } + | { extendedTags: string }; + +export interface Maps { + maps: { + [key: string]: { + id: number; + tags?: string; + localName: string; + avaliableInTrainingRoom: boolean; // lol wg typo + spriteFrame: number; + supremacyPointsThreshold?: number; + availableModes: number[]; + shadowMapsAvailable?: boolean; + assaultRespawnPoints: { + allies: MapAlly[]; + enemies: MapAlly[]; + }; + levels?: number[]; + }; + }; +} + +export interface MapAlly { + respawnNumber: number; + points: Array; +} + +interface AvailableNationsYaml { + available_nations: string[]; +} + +export interface SquadBattleTypeStyles { + Prototypes: { + components: { + UIDataLocalBindingsComponent: { + data: [string, string, string][]; + }; + }; + }[]; +} + +type CombatRolesYaml = Record< + string, + { + id: number; + default_abilities: string[]; + } +>; + +const blitzShellKindToBlitzkit: Record = { + ARMOR_PIERCING: ShellType.SHELL_TYPE_AP, + ARMOR_PIERCING_CR: ShellType.SHELL_TYPE_APCR, + HIGH_EXPLOSIVE: ShellType.SHELL_TYPE_HE, + HOLLOW_CHARGE: ShellType.SHELL_TYPE_HEAT, +}; +const missingStrings: Record = { + '#artefacts:tungstentip/name': 'Tungsten Shells', +}; +export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU(R?)))$/; +const blitzModuleTypeToBlitzkit: Record = { + chassis: ModuleType.MODULE_TYPE_TRACKS, + engine: ModuleType.MODULE_TYPE_ENGINE, + gun: ModuleType.MODULE_TYPE_GUN, + turret: ModuleType.MODULE_TYPE_TURRET, + vehicle: ModuleType.MODULE_TYPE_VEHICLE, +}; export async function definitions() { console.log('Building definitions...'); - const buffer = TankDefinitions.encode({ - tanks: { - 1: { - name: 'asd', - ancestors: [], - camouflageMoving: 1, - camouflageOnFire: 1, - camouflages: [], - camouflageStill: 1, - class: TankClass.TANK_CLASS_HEAVY, - description: 'asd', - crew: [], - deprecated: false, - engines: [], - equipmentPreset: '', - fixedCamouflage: false, - health: 1, - id: 1, - maxConsumables: 1, - maxProvisions: 1, - nation: 'usa', - price: { - type: TankPriceType.TANK_PRICE_TYPE_CREDITS, - value: 1, - }, - roles: {}, - speedBackwards: 1, - speedForwards: 1, - successors: [], - testing: false, - tier: 1, - tracks: [], - turrets: [], - type: 1, - xp: 1, - nameFull: '', - weight: 1, - }, + const wargamingTankopedia = (await fetch( + `https://api.wotblitz.com/wotb/encyclopedia/vehicles/?application_id=${assertSecret( + process.env.PUBLIC_WARGAMING_APPLICATION_ID, + )}&fields=description`, + ).then((response) => response.json())) as { + data: { [key: number]: { description: null | string } }; + }; + + const gameDefinitions: GameDefinitions = { + version: (await readStringDVPL(`${DATA}/version.txt.dvpl`)).split(' ')[0], + nations: ( + await readYAMLDVPL( + `${DATA}/available_nations.yaml.dvpl`, + ) + ).available_nations, + gameModes: {}, + roles: {}, + }; + const tankDefinitions: TankDefinitions = { tanks: {} }; + const camouflageDefinitions: CamouflageDefinitions = { camouflages: {} }; + const modelDefinitions: ModelDefinitions = { models: {} }; + const mapDefinitions: MapDefinitions = { maps: {} }; + const equipmentDefinitions: EquipmentDefinitions = { + presets: {}, + equipments: {}, + }; + const consumableDefinitions: ConsumableDefinitions = { consumables: {} }; + const provisionDefinitions: ProvisionDefinitions = { provisions: {} }; + const skillDefinitions: SkillDefinitions = { + classes: { + [TankClass.TANK_CLASS_LIGHT]: { skills: [] }, + [TankClass.TANK_CLASS_MEDIUM]: { skills: [] }, + [TankClass.TANK_CLASS_HEAVY]: { skills: [] }, + [TankClass.TANK_CLASS_TANK_DESTROYER]: { skills: [] }, }, - }).finish(); + }; + const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then( + (nations) => nations.filter((nation) => nation !== 'common'), + ); + const tankStringIdMap: Record = {}; + const stringsPreInstalled = await readYAMLDVPL( + `${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 optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>( + `${DATA}/XML/item_defs/vehicles/common/optional_devices.xml.dvpl`, + ); + const strings = { + ...stringsPreInstalled, + ...stringsCache, + }; + const optionalDeviceSlots = await readXMLDVPL<{ + root: OptionalDeviceSlots; + }>(`${DATA}/XML/item_defs/vehicles/common/optional_device_slots.xml.dvpl`); + const consumables: ConsumablesCommon = {}; + const provisions: ProvisionsCommon = {}; - console.log(buffer.length); + for (const match of ( + await readStringDVPL( + `${DATA}/XML/item_defs/vehicles/common/consumables/list.xml.dvpl`, + ) + ).matchAll(//g)) { + if (match[1] === 'prototypes') continue; - console.log(TankDefinitions.decode(buffer)); + Object.assign( + consumables, + ( + await readXMLDVPL<{ root: ConsumablesCommon }>( + `${DATA}/XML/item_defs/vehicles/common/consumables/${match[1]}.xml.dvpl`, + ) + ).root, + ); + } + + for (const match of ( + await readStringDVPL( + `${DATA}/XML/item_defs/vehicles/common/provisions/list.xml.dvpl`, + ) + ).matchAll(//g)) { + if (match[1] === 'prototypes') continue; + + Object.assign( + provisions, + ( + await readXMLDVPL<{ root: ConsumablesCommon }>( + `${DATA}/XML/item_defs/vehicles/common/provisions/${match[1]}.xml.dvpl`, + ) + ).root, + ); + } + + const avatar = await readXMLDVPL<{ root: Avatar }>( + `${DATA}/XML/item_defs/tankmen/avatar.xml.dvpl`, + ); + const maps = await readYAMLDVPL(`${DATA}/maps.yaml.dvpl`); + const tankXps = new Map(); + const camouflagesXml = await readXMLDVPL<{ root: CamouflagesXML }>( + `${DATA}/XML/item_defs/vehicles/common/camouflages.xml.dvpl`, + ); + const camouflagesYaml = await readYAMLDVPL( + `${DATA}/camouflages.yaml.dvpl`, + ); + const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages); + const squadBattleTypeStyles = await readYAMLDVPL( + `${DATA}/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml.dvpl`, + ); + const gameTypeSelectorStyles = await readYAMLDVPL( + `${DATA}/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml.dvpl`, + ); + const gameModeNativeNames: Record = {}; + const combatRoles = await readYAMLDVPL( + `${DATA}/XML/item_defs/vehicles/common/combat_roles.yaml.dvpl`, + ); + const consumableNativeNames: Record = {}; + const provisionNativeNames: Record = {}; + + 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 = strings[match[2]]; + + gameModeNativeNames[match[3]] = id; + gameDefinitions.gameModes[id] = { + name, + }; + } + + for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( + /eGameMode\.([a-zA-Z]+) -> "~res:\/Gfx\/UI\/Hangar\/GameTypes\/battle-type_([^"]+)"/g, + )) { + Object.entries(gameModeNativeNames).forEach(([key, value]) => { + if (key.toLowerCase() === match[2].toLowerCase()) { + gameModeNativeNames[match[1]] = value; + } + }); + } + + camouflagesXmlEntries.forEach(([camoKey, camo]) => { + const yamlEntry = camouflagesYaml[camoKey]; + const fullName = yamlEntry.userString + ? strings[yamlEntry.userString] + : undefined; + const shortName = yamlEntry.shortUserString + ? strings[yamlEntry.shortUserString] + : undefined; + const resolvedTankName = shortName ?? fullName; + const resolvedTankNameFull = + resolvedTankName === fullName ? undefined : fullName; + + camouflageDefinitions.camouflages[camo.id] = { + id: camo.id, + name: strings[camo.userString], + tankName: resolvedTankName, + tankNameFull: resolvedTankNameFull, + }; + }); + + await Promise.all( + nations.map(async (nation) => { + const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>( + `${DATA}/XML/item_defs/vehicles/${nation}/list.xml.dvpl`, + ); + const turretList = await readXMLDVPL<{ + root: TurretDefinitionsList; + }>( + `${DATA}/XML/item_defs/vehicles/${nation}/components/turrets.xml.dvpl`, + ); + const gunList = await readXMLDVPL<{ + root: GunDefinitionsList; + }>(`${DATA}/XML/item_defs/vehicles/${nation}/components/guns.xml.dvpl`); + const shellList = await readXMLDVPL<{ + root: ShellDefinitionsList; + }>(`${DATA}/XML/item_defs/vehicles/${nation}/components/shells.xml.dvpl`); + const enginesList = await readXMLDVPL<{ + root: EngineDefinitionsList; + }>( + `${DATA}/XML/item_defs/vehicles/${nation}/components/engines.xml.dvpl`, + ); + const chassisList = await readXMLDVPL<{ + root: ChassisDefinitionsList; + }>( + `${DATA}/XML/item_defs/vehicles/${nation}/components/chassis.xml.dvpl`, + ); + + function resolveUnlocks(unlocks?: BlitzModuleType) { + if (!unlocks) return []; + + return Object.entries(unlocks) + .map(([type, list]) => + (Array.isArray(list) ? list : [list]).map((item) => { + const typeTyped = type as keyof BlitzModuleType; + let rawId: number; + + if (typeTyped === 'chassis') { + rawId = chassisList.root.ids[item['#text']]; + } else if (typeTyped === 'engine') { + rawId = enginesList.root.ids[item['#text']]; + } else if (typeTyped === 'gun') { + rawId = gunList.root.ids[item['#text']]; + } else if (typeTyped === 'turret') { + rawId = turretList.root.ids[item['#text']]; + } else if (typeTyped === 'vehicle') { + rawId = tankList.root[item['#text']].id; + } + + return { + id: toUniqueId(nation, rawId!), + type: blitzModuleTypeToBlitzkit[typeTyped], + cost: { + type: + typeof item.cost === 'number' + ? 'xp' + : item.cost.split(':')[0], + value: + typeof item.cost === 'number' + ? item.cost + : Number(item.cost.split(':')[1]), + }, + } satisfies Unlock; + }), + ) + .flat(); + } + + for (const tankKey in tankList.root) { + if (botPattern.test(tankKey)) continue; + + const gunXps = new Map(); + const turretXps = new Map(); + const engineXps = new Map(); + const trackXps = new Map(); + const tank = tankList.root[tankKey]; + let tankPrice: TankPrice; + const tankDefinition = await readXMLDVPL<{ root: VehicleDefinitions }>( + `${DATA}/XML/item_defs/vehicles/${nation}/${tankKey}.xml.dvpl`, + ); + const tankParameters = await readYAMLDVPL( + `${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml.dvpl`, + ); + const turretOrigin = tankDefinition.root.hull.turretPositions.turret + .split(' ') + .map(Number) as Vector3Tuple; + const tankId = toUniqueId(nation, tank.id); + const tankTags = tank.tags.split(' '); + const hullArmor: Armor = { spaced: [], thickness: {} }; + const equipment = tankDefinition.root.optDevicePreset; + tankStringIdMap[`${nation}:${tankKey}`] = tankId; + + if (tank.sellPrice) { + tankPrice = { + type: TankPriceType.TANK_PRICE_TYPE_GOLD, + value: tank.sellPrice['#text'] * 2, + }; + } else if (typeof tank.price === 'number') { + tankPrice = { + type: TankPriceType.TANK_PRICE_TYPE_CREDITS, + value: tank.price, + }; + } else { + tankPrice = { + type: TankPriceType.TANK_PRICE_TYPE_CREDITS, + value: tank.price['#text'] * 400, + }; + } + + Object.keys(tankDefinition.root.hull.armor) + .filter((name) => name.startsWith('armor_')) + .forEach((name) => { + const armorIdString = name.match(/armor_(\d+)/)?.[1]; + + if (armorIdString === undefined) { + throw new SyntaxError(`Invalid armor id: ${name}`); + } + + const armorId = parseInt(armorIdString); + const armorRaw = tankDefinition.root.hull.armor[name]; + + if (typeof armorRaw === 'number') { + hullArmor.thickness[armorId] = armorRaw; + } else { + if (!hullArmor.spaced) hullArmor.spaced = []; + hullArmor.thickness[armorId] = armorRaw['#text']; + hullArmor.spaced.push(armorId); + } + }); + const crew: Crew[] = []; + const fixedCamouflage = tankTags.includes('eventCamouflage_user'); + const totalUnlocks: UnlocksListing[] = []; + + Object.entries(tankDefinition.root.crew).forEach(([crewKey, value]) => { + let entry: Crew; + const index = crew.findIndex( + ({ type }) => blitzkitCrewTypeToBlitz[type] === crewKey, + ); + if (index === -1) { + entry = { + type: blitzCrewTypeToBlitzkit[crewKey as BlitzCrewType], + count: 1, + substitute: [], + }; + crew.push(entry); + } else { + entry = crew[index]; + } + + if (typeof value === 'string') { + entry.count++; + + if (value !== '') { + entry.substitute = value + .split('\n') + .map( + (member) => + blitzCrewTypeToBlitzkit[member.trim() as BlitzCrewType], + ); + } + } else { + if (entry.count === undefined) { + entry.count = value.length; + } else { + entry.count += value.length; + } + } + }); + + const camouflages = camouflagesXmlEntries + .filter(([, camo]) => { + if (!camo.vehicleFilter.include) return false; + if (camo.unlockCostCategory !== 'legendary-skins-gold') + return false; + + const includeArray = Array.isArray(camo.vehicleFilter.include) + ? camo.vehicleFilter.include + : [camo.vehicleFilter.include]; + + return includeArray.some((filter) => { + if ('vehicle' in filter && filter.vehicle?.name) { + return filter.vehicle.name === `${nation}:${tankKey}`; + } + + return false; + }); + }) + .map(([, camo]) => camo.id); + + tankDefinitions.tanks[tankId] = { + ancestors: [], + successors: [], + id: tankId, + roles: {}, + camouflages: camouflages, + description: + wargamingTankopedia.data[tankId]?.description ?? undefined, + fixedCamouflage: fixedCamouflage, + crew, + weight: tankDefinition.root.hull.weight, + health: tankDefinition.root.hull.maxHealth, + speedForwards: tankDefinition.root.speedLimits.forward, + speedBackwards: tankDefinition.root.speedLimits.backward, + equipmentPreset: + typeof equipment === 'string' ? equipment : equipment.at(-1)!, + maxConsumables: tankDefinition.root.consumableSlots, + maxProvisions: tankDefinition.root.provisionSlots, + name: + (tank.shortUserString + ? strings[tank.shortUserString] + : undefined) ?? strings[tank.userString], + nameFull: strings[tank.userString], + nation, + type: tankTags.includes('collectible') + ? TankType.TANK_TYPE_COLLECTOR + : (typeof tank.price === 'number' ? false : 'gold' in tank.price) + ? TankType.TANK_TYPE_PREMIUM + : TankType.TANK_TYPE_RESEARCHABLE, + tier: tank.level as Tier, + class: blitzTankClassToBlitzkit[tankTags[0] as BlitzTankClass], + testing: tankTags.includes('testTank'), + deprecated: tankTags.includes('deprecated'), + price: tankPrice, + camouflageStill: tankDefinition.root.invisibility.still, + camouflageMoving: tankDefinition.root.invisibility.moving, + camouflageOnFire: tankDefinition.root.invisibility.firePenalty, + turrets: [], + engines: [], + tracks: [], + }; + + if (tank.combatRole) { + Object.entries(tank.combatRole).forEach(([gameMode, role]) => { + const id = Object.entries(gameModeNativeNames).find( + ([key]) => key.toLowerCase() === gameMode.toLowerCase(), + )?.[1]; + + if (id === undefined) { + throw new Error( + `Unknown game mode in tank ${tankKey}: ${gameMode}`, + ); + } + + tankDefinitions.tanks[tankId].roles[id] = combatRoles[role].id; + }); + } + + if ( + tankDefinitions.tanks[tankId].name === + tankDefinitions.tanks[tankId].nameFull + ) { + delete tankDefinitions.tanks[tankId].nameFull; + } + + modelDefinitions.models[tankId] = { + armor: hullArmor, + turretOrigin: vector3TupleToBlitzkit(turretOrigin), + initialTurretRotation: tankDefinition.root.hull.turretInitialRotation + ? { + yaw: tankDefinition.root.hull.turretInitialRotation.yaw, + pitch: tankDefinition.root.hull.turretInitialRotation.pitch, + roll: tankDefinition.root.hull.turretInitialRotation.roll, + } + : undefined, + boundingBox: { + min: vector3TupleToBlitzkit(tankParameters.collision.hull.bbox.min), + max: vector3TupleToBlitzkit(tankParameters.collision.hull.bbox.max), + }, + turrets: {}, + tracks: {}, + }; + + Object.keys(tankDefinition.root.chassis).forEach((key) => { + const track = tankDefinition.root.chassis[key]; + const trackId = toUniqueId(nation, chassisList.root.ids[key]); + const terrainResistances = track.terrainResistance + .split(' ') + .map(Number); + const trackArmorRaw = track.armor.leftTrack; + const hullOrigin = track.hullPosition + .split(' ') + .map(Number) as Vector3Tuple; + + totalUnlocks.push(track.unlocks); + tankDefinitions.tanks[tankId].tracks.push({ + id: trackId, + weight: track.weight, + name: strings[track.userString], + traverseSpeed: track.rotationSpeed, + dispersionMove: track.shotDispersionFactors.vehicleMovement, + dispersionTraverse: track.shotDispersionFactors.vehicleRotation, + resistanceHard: terrainResistances[0], + resistanceMedium: terrainResistances[1], + resistanceSoft: terrainResistances[2], + tier: track.level as Tier, + unlocks: resolveUnlocks(track.unlocks), + }); + + modelDefinitions.models[tankId].tracks[trackId] = { + thickness: + typeof trackArmorRaw === 'number' + ? trackArmorRaw + : trackArmorRaw['#text'], + origin: vector3TupleToBlitzkit(hullOrigin), + }; + }); + + Object.keys(tankDefinition.root.engines).forEach((engineKey) => { + const engine = tankDefinition.root.engines[engineKey]; + const engineListEntry = enginesList.root.shared[engineKey]; + const engineId = toUniqueId(nation, enginesList.root.ids[engineKey]); + + totalUnlocks.push(engine.unlocks); + tankDefinitions.tanks[tankId].engines.push({ + id: engineId, + name: strings[engineListEntry.userString], + fireChance: engineListEntry.fireStartingChance, + tier: engineListEntry.level as Tier, + weight: engineListEntry.weight, + power: engineListEntry.power, + unlocks: resolveUnlocks(engine.unlocks), + }); + }); + + Object.keys(tankDefinition.root.turrets0).forEach( + (turretKey, turretIndex) => { + const turret = tankDefinition.root.turrets0[turretKey]; + const turretModel = Number( + parsePath(turret.models.undamaged).name.split('_')[1], + ); + const turretId = toUniqueId(nation, turretList.root.ids[turretKey]); + const turretYaw = ( + typeof turret.yawLimits === 'string' + ? turret.yawLimits + : turret.yawLimits.at(-1)! + ) + .split(' ') + .map(Number) as [number, number]; + const gunOrigin = ( + typeof turret.gunPosition === 'string' + ? turret.gunPosition + : turret.gunPosition[0] + ) + .split(' ') + .map(Number) as Vector3Tuple; + const turretArmor: Armor = { thickness: {}, spaced: [] }; + + totalUnlocks.push(turret.unlocks); + Object.keys(turret.armor) + .filter((name) => name.startsWith('armor_')) + .forEach((name) => { + const armorIdString = name.match(/armor_(\d+)/)?.[1]; + + if (armorIdString === undefined) { + throw new SyntaxError(`Invalid armor id: ${name}`); + } + + const armorId = parseInt(armorIdString); + const armorRaw = turret.armor[name]; + + if (typeof armorRaw === 'number') { + turretArmor.thickness[armorId] = armorRaw; + } else { + if (!turretArmor.spaced) turretArmor.spaced = []; + turretArmor.thickness[armorId] = armorRaw['#text']; + turretArmor.spaced.push(armorId); + } + }); + + tankDefinitions.tanks[tankId].turrets.push({ + id: turretId, + traverseSpeed: turret.rotationSpeed, + name: + strings[turret.userString] ?? + turretKey + .replaceAll('_', ' ') + .replace(/^(Turret ([0-9] )?)+/, ''), + tier: turret.level as Tier, + guns: [], + health: turret.maxHealth, + viewRange: turret.circularVisionRadius, + weight: turret.weight, + unlocks: resolveUnlocks(turret.unlocks), + }); + + modelDefinitions.models[tankId].turrets[turretId] = { + armor: turretArmor, + gunOrigin: vector3TupleToBlitzkit(gunOrigin), + modelId: turretModel, + yaw: + -turretYaw[0] + turretYaw[1] === 360 + ? undefined + : { min: turretYaw[0], max: turretYaw[1] }, + guns: {}, + boundingBox: { + min: vector3TupleToBlitzkit( + tankParameters.collision[ + parsePath( + turret.hitTester.collisionModel, + ).name.toLowerCase() + ].bbox.min, + ), + max: vector3TupleToBlitzkit( + tankParameters.collision[ + parsePath( + turret.hitTester.collisionModel, + ).name.toLowerCase() + ].bbox.max, + ), + }, + }; + + Object.keys(turret.guns).forEach((gunKey, gunIndex) => { + const gun = turret.guns[gunKey]; + const gunId = toUniqueId(nation, gunList.root.ids[gunKey]); + const gunListEntry = gunList.root.shared[gunKey]; + const pitchLimitsRaw = + gun.pitchLimits ?? gunListEntry.pitchLimits; + const gunPitch = ( + typeof pitchLimitsRaw === 'string' + ? pitchLimitsRaw + : pitchLimitsRaw.at(-1)! + ) + .split(' ') + .map(Number) as [number, number]; + const gunModel = Number( + parsePath(gun.models.undamaged).name.split('_')[1], + ); + const gunName = + strings[gunListEntry.userString] ?? gunKey.replaceAll('_', ' '); + const gunType = + 'clip' in gun + ? gun.pumpGunMode + ? 'autoReloader' + : 'autoLoader' + : 'regular'; + const gunReload = + gunType === 'autoReloader' + ? gun.pumpGunReloadTimes!.split(' ').map(Number) + : gun.reloadTime; + const gunClipCount = + gunType === 'regular' ? undefined : gun.clip!.count; + const gunIntraClip = + gunType === 'regular' ? undefined : 60 / gun.clip!.rate; + const front = gun.extraPitchLimits?.front + ? gun.extraPitchLimits.front.split(' ').map(Number) + : undefined; + const back = gun.extraPitchLimits?.back + ? gun.extraPitchLimits.back.split(' ').map(Number) + : undefined; + const transition = gun.extraPitchLimits?.transition + ? typeof gun.extraPitchLimits.transition === 'number' + ? gun.extraPitchLimits.transition + : gun.extraPitchLimits.transition.at(-1)! + : undefined; + const gunArmor: Armor = { thickness: {}, spaced: [] }; + const shotDispersionFactors = + gun.shotDispersionFactors ?? gunListEntry.shotDispersionFactors; + const maskName = `gun_${gunModel.toString().padStart(2, '0')}`; + const maskEnabled = + tankParameters.maskSlice?.[maskName]?.enabled ?? false; + let mask: number | undefined; + + if (maskEnabled) { + const maskRaw = tankParameters.maskSlice![maskName]!; + mask = maskRaw.planePosition[1]; + } else { + mask = undefined; + } + + totalUnlocks.push(gun.unlocks); + Object.keys(gun.armor) + .filter((name) => name.startsWith('armor_')) + .forEach((name) => { + const armorIdString = name.match(/armor_(\d+)/)?.[1]; + if (armorIdString === undefined) { + throw new SyntaxError(`Invalid armor id: ${name}`); + } + const armorId = parseInt(armorIdString); + const armorRaw = gun.armor[name]; + if (typeof armorRaw === 'number') { + gunArmor.thickness[armorId] = armorRaw; + } else { + if (!gunArmor.spaced) gunArmor.spaced = []; + gunArmor.thickness[armorId] = armorRaw['#text']; + gunArmor.spaced.push(armorId); + } + }); + + tankDefinitions.tanks[tankId].turrets[turretIndex].guns.push({ + id: gunId, + weight: gunListEntry.weight, + rotationSpeed: gunListEntry.rotationSpeed, + name: gunName, + tier: gunListEntry.level as Tier, + shells: [], + type: gunType, + reload: gunReload, + count: gunClipCount, + intraClip: gunIntraClip, + camouflageLoss: + typeof gun.invisibilityFactorAtShot === 'number' + ? gun.invisibilityFactorAtShot + : gun.invisibilityFactorAtShot.at(-1)!, + aimTime: gun.aimingTime ?? gunListEntry.aimingTime, + dispersion: { + base: + gun.shotDispersionRadius ?? + gunListEntry.shotDispersionRadius, + damaged: shotDispersionFactors.whileGunDamaged, + shot: shotDispersionFactors.afterShot, + traverse: shotDispersionFactors.turretRotation, + }, + unlocks: resolveUnlocks(gun.unlocks), + } as GunDefinition); + + modelDefinitions.models[tankId].turrets[turretId].guns[gunId] = { + armor: gunArmor, + modelId: gunModel, + mask, + thickness: + gun.armor.gun === undefined + ? 0 + : typeof gun.armor.gun === 'number' + ? gun.armor.gun + : gun.armor.gun['#text'], + pitch: { + min: gunPitch[0], + max: gunPitch[1], + + front: front + ? { + min: front[0], + max: front[1], + range: front[2], + } + : undefined, + back: back + ? { + min: back[0], + max: back[1], + range: back[2], + } + : undefined, + transition, + }, + }; + + Object.keys(gunListEntry.shots).forEach((shellKey) => { + const gunShellEntry = gunListEntry.shots[shellKey]; + const shell = shellList.root[shellKey]; + const shellId = toUniqueId(nation, shell.id); + const shellName = + strings[shell.userString] ?? shellKey.replaceAll('_', ' '); + const penetrationRaw = gunShellEntry.piercingPower + .split(' ') + .filter((penetrationString) => penetrationString !== '') + .map(Number); + + Object.values( + tankDefinitions.tanks[tankId].turrets[turretIndex].guns[ + gunIndex + ], + ).forEach((gun) => { + gun.shells.push({ + id: shellId, + name: shellName, + speed: gunShellEntry.speed, + damage: { + armor: shell.damage.armor, + module: shell.damage.devices, + }, + caliber: shell.caliber, + normalization: shell.normalizationAngle, + ricochet: shell.ricochetAngle, + type: blitzShellKindToBlitzkit[shell.kind], + explosionRadius: + shell.kind === 'HIGH_EXPLOSIVE' + ? (shell.explosionRadius ?? 0) + : undefined, + icon: shell.icon, + penetration: + penetrationRaw[0] === penetrationRaw[1] + ? penetrationRaw[0] + : [penetrationRaw[0], penetrationRaw[1]], + }); + }); + }); + }); + }, + ); + + totalUnlocks.forEach((unlocks) => { + if (unlocks === undefined) return; + + Object.entries(unlocks).forEach(([key, value]) => { + (Array.isArray(value) ? value : [value]).forEach((vehicle) => { + switch (key as keyof BlitzModuleType) { + case 'vehicle': { + const tankListEntry = tankList.root[vehicle['#text']]; + const currentTank = tankDefinitions.tanks[tankId]; + const successorId = toUniqueId(nation, tankListEntry.id); + + tankXps.set(successorId, vehicle.cost as number); + + if (currentTank.successors === undefined) { + currentTank.successors = []; + } + if (!currentTank.successors!.includes(successorId)) { + currentTank.successors!.push(successorId); + } + break; + } + + case 'gun': { + gunXps.set( + toUniqueId(nation, gunList.root.ids[vehicle['#text']]), + vehicle.cost as number, + ); + break; + } + + case 'turret': { + turretXps.set( + toUniqueId(nation, turretList.root.ids[vehicle['#text']]), + vehicle.cost as number, + ); + break; + } + + case 'engine': { + engineXps.set( + toUniqueId(nation, enginesList.root.ids[vehicle['#text']]), + vehicle.cost as number, + ); + break; + } + + case 'chassis': { + trackXps.set( + toUniqueId(nation, chassisList.root.ids[vehicle['#text']]), + vehicle.cost as number, + ); + break; + } + } + }); + }); + }); + + Object.values(tankDefinitions.tanks[tankId].turrets).forEach( + (turret) => { + turret.xp = turretXps.get(turret.id); + + Object.values(turret.guns).forEach((gunRaw) => { + Object.values(gunRaw).forEach((gun) => { + gun.xp = gunXps.get(gun.id); + }); + }); + }, + ); + + Object.values(tankDefinitions.tanks[tankId].engines).forEach( + (engine) => { + engine.xp = engineXps.get(engine.id); + }, + ); + + Object.values(tankDefinitions.tanks[tankId].tracks).forEach((track) => { + track.xp = trackXps.get(track.id); + }); + } + }), + ); + + Object.values(tankDefinitions.tanks).forEach((tank) => { + tank.xp = tankXps.get(tank.id); + }); + + Object.values(tankDefinitions.tanks).forEach((tank) => { + tank.successors?.forEach((predecessorId) => { + if (!tankDefinitions.tanks[predecessorId].ancestors?.includes(tank.id)) { + tankDefinitions.tanks[predecessorId].ancestors?.push(tank.id); + } + }); + }); + + Object.entries(optionalDevices.root).forEach( + ([optionalDeviceKey, optionalDeviceEntry]) => { + if (optionalDeviceKey === 'nextAvailableId') return; + + equipmentDefinitions.equipments[optionalDeviceEntry.id] = { + name: strings[optionalDeviceEntry.userString], + description: strings[optionalDeviceEntry.description], + }; + }, + ); + + Object.entries(optionalDeviceSlots.root.presets).forEach( + ([optionalDeviceSlotKey, optionalDeviceSlotEntry]) => { + if (optionalDeviceSlotKey === 'emptyPreset') return; + + equipmentDefinitions.presets[optionalDeviceSlotKey].slots = Object.values( + optionalDeviceSlotEntry, + ) + .map((level) => { + return Object.values(level).map((options) => { + return { + left: optionalDevices.root[options.device0].id, + right: optionalDevices.root[options.device1].id, + } satisfies EquipmentSlot; + }); + }) + .flat(); + }, + ); + + Object.entries(consumables).forEach(([key, consumable]) => { + consumableNativeNames[key] = consumable.id; + + const entry: Consumable = { + id: consumable.id, + gameModeExclusive: 'gameModeFilter' in consumable, + cooldown: consumable.script.cooldown, + duration: consumable.script.duration, + name: + strings[consumable.userString] ?? + missingStrings[consumable.userString] ?? + `Unknown ${consumable.id}`, + exclude: [], + include: [], + }; + consumableDefinitions.consumables[consumable.id] = entry; + + const includeRaw = consumable.vehicleFilter?.include.vehicle; + const excludeRaw = consumable.vehicleFilter?.exclude?.vehicle; + + if (!entry.gameModeExclusive) { + if (includeRaw) { + entry.include = []; + + if ('minLevel' in includeRaw) { + entry.include.push({ + tiers: { min: includeRaw.minLevel, max: includeRaw.maxLevel }, + }); + } else if ('name' in includeRaw) { + entry.include.push({ + ids: { + ids: includeRaw.name + .split(' ') + .map((key) => tankStringIdMap[key]), + }, + }); + } else throw new SyntaxError('Unhandled include type'); + + if (consumable.vehicleFilter?.include.nations) { + entry.include.push({ + nations: { + nations: consumable.vehicleFilter.include.nations.split(' '), + }, + }); + } + } + + if (excludeRaw) { + entry.exclude = []; + + if ('name' in excludeRaw) { + entry.exclude!.push({ + ids: { + ids: excludeRaw.name + .split(' ') + .map((key) => tankStringIdMap[key]), + }, + }); + } else if ('extendedTags' in excludeRaw) { + entry.exclude!.push({ + categories: { + categories: excludeRaw.extendedTags + .split(' ') + .map( + (item) => + blitzTankFilterDefinitionCategoryToBlitzkit[ + item as BlitzTankFilterDefinitionCategory + ], + ), + }, + }); + } else throw new SyntaxError('Unhandled exclude type'); + + if (consumable.vehicleFilter?.exclude?.nations) { + entry.exclude!.push({ + nations: { + nations: consumable.vehicleFilter.exclude.nations.split(' '), + }, + }); + } + } + } + }); + + Object.entries(provisions).forEach(([key, provision]) => { + provisionNativeNames[key] = provision.id; + + const entry: Provision = { + id: provision.id, + exclude: [], + include: [], + gameModeExclusive: 'gameModeFilter' in provision, + name: + strings[provision.userString] ?? + missingStrings[provision.userString] ?? + `Unknown ${provision.id}`, + }; + provisionDefinitions.provisions[provision.id] = entry; + + const includeRaw = provision.vehicleFilter?.include.vehicle; + const excludeRaw = provision.vehicleFilter?.exclude?.vehicle; + + if (!entry.gameModeExclusive) { + if (includeRaw) { + entry.include = []; + + if ('minLevel' in includeRaw) { + entry.include.push({ + tiers: { + min: includeRaw.minLevel, + max: includeRaw.maxLevel, + }, + }); + } else if ('name' in includeRaw) { + entry.include.push({ + ids: { + ids: includeRaw.name + .split(' ') + .map((key) => tankStringIdMap[key]), + }, + }); + } else throw new SyntaxError('Unhandled include type'); + + if (provision.vehicleFilter?.include.nations) { + entry.include.push({ + nations: { + nations: provision.vehicleFilter.include.nations.split(' '), + }, + }); + } + } + + if (excludeRaw) { + entry.exclude = []; + + if ('name' in excludeRaw) { + entry.exclude!.push({ + ids: { + ids: excludeRaw.name + .split(' ') + .map((key) => tankStringIdMap[key]), + }, + }); + } else if ('extendedTags' in excludeRaw) { + entry.exclude!.push({ + categories: { + categories: excludeRaw.extendedTags + .split(' ') + .map( + (item) => + blitzTankFilterDefinitionCategoryToBlitzkit[ + item as BlitzTankFilterDefinitionCategory + ], + ), + }, + }); + } else throw new SyntaxError('Unhandled exclude type'); + + if (provision.vehicleFilter?.exclude?.nations) { + entry.exclude!.push({ + nations: { + nations: provision.vehicleFilter.exclude.nations.split(' '), + }, + }); + } + } + } + + if (provision.script.bonusValues?.crewLevelIncrease !== undefined) { + provisionDefinitions.provisions[provision.id].crew = + provision.script.bonusValues?.crewLevelIncrease; + } + }); + + Object.entries(combatRoles).forEach(([, value]) => { + gameDefinitions.roles[value.id] = { provisions: [], consumables: [] }; + + value.default_abilities.forEach((ability) => { + if (ability in consumableNativeNames) { + gameDefinitions.roles[value.id].consumables.push( + consumableNativeNames[ability], + ); + } else if (ability in provisionNativeNames) { + gameDefinitions.roles[value.id].provisions.push( + provisionNativeNames[ability], + ); + } else throw new Error(`Unknown ability ${ability}`); + }); + }); + + Object.entries(avatar.root.skillsByClasses).forEach(([tankClass, skills]) => { + skillDefinitions.classes[ + blitzTankClassToBlitzkit[tankClass as BlitzTankClass] + ] = { + skills: skills.split(' '), + }; + }); + + Object.entries(maps.maps).forEach(([key, map]) => { + mapDefinitions.maps[map.id] = { + id: map.id, + name: strings[`#maps:${key}:${map.localName}`], + }; + }); + + await commitAssets('definitions', [ + { + content: encodePB64(GameDefinitions, gameDefinitions), + encoding: 'base64', + path: 'definitions/game.cdon.lz4', + }, + { + content: encodePB64(TankDefinitions, tankDefinitions), + encoding: 'base64', + path: 'definitions/tanks.cdon.lz4', + }, + { + content: encodePB64(ModelDefinitions, modelDefinitions), + encoding: 'base64', + path: 'definitions/models.cdon.lz4', + }, + { + content: encodePB64(EquipmentDefinitions, equipmentDefinitions), + encoding: 'base64', + path: 'definitions/equipment.cdon.lz4', + }, + { + content: encodePB64(ConsumableDefinitions, consumableDefinitions), + encoding: 'base64', + path: 'definitions/consumables.cdon.lz4', + }, + { + content: encodePB64(ProvisionDefinitions, provisionDefinitions), + encoding: 'base64', + path: 'definitions/provisions.cdon.lz4', + }, + { + content: encodePB64(SkillDefinitions, skillDefinitions), + encoding: 'base64', + path: 'definitions/skills.cdon.lz4', + }, + { + content: encodePB64(MapDefinitions, mapDefinitions), + encoding: 'base64', + path: 'definitions/maps.cdon.lz4', + }, + { + content: encodePB64(CamouflageDefinitions, camouflageDefinitions), + encoding: 'base64', + path: 'definitions/camouflages.cdon.lz4', + }, + ]); } - -definitions();