diff --git a/packages/core/src/blitzkit/api/server.ts b/packages/core/src/blitzkit/api/server.ts index 377d7e8ba..d0ea56e14 100644 --- a/packages/core/src/blitzkit/api/server.ts +++ b/packages/core/src/blitzkit/api/server.ts @@ -16,10 +16,14 @@ import { OptionalDeviceSlots, ProvisionDefinitions, ProvisionsCommon, + ResearchCost, SkillDefinitions, + sluggify, SquadBattleTypeStylesYaml, TankDefinitions, TankmenAvatar, + toUniqueId, + VehicleDefinitionList, } from "@blitzkit/core"; import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n"; import locales from "@blitzkit/i18n/locales.json"; @@ -29,11 +33,23 @@ import { BlitzKitAPI } from "./base"; export class ServerBlitzKitAPI extends BlitzKitAPI { private vfs: AbstractVFS; - private stringsI18n: Record> = {}; + private nationSlugDiscriminators = { + china: "cn", + european: "eu", + france: "fr", + germany: "de", + japan: "jp", + other: "ot", + uk: "gb", + usa: "us", + ussr: "ru", + }; + private stringsI18n: Record> = {}; private nationsDir?: string[]; - consumablesCommon: ConsumablesCommon = {}; - provisionsCommon: ProvisionsCommon = {}; + private tankStringIdMap: Record = {}; + private consumablesCommon: ConsumablesCommon = {}; + private provisionsCommon: ProvisionsCommon = {}; // TODO: suffix with "yaml"s and "xml"s private optionalDevices?: { root: OptionalDevices }; @@ -45,6 +61,7 @@ export class ServerBlitzKitAPI extends BlitzKitAPI { private squadBattleTypeStyles?: SquadBattleTypeStylesYaml; private gameTypeSelectorStyles?: SquadBattleTypeStylesYaml; private combatRoles?: CombatRolesYaml; + private tierEqualizer?: string[][]; constructor(vfs: AbstractVFS) { super(); @@ -82,6 +99,10 @@ export class ServerBlitzKitAPI extends BlitzKitAPI { this.combatRoles = await this.vfs.yaml( `Data/XML/item_defs/vehicles/common/combat_roles.yaml`, ); + this.tierEqualizer = await this.vfs.csv( + `Data/XML/item_defs/vehicles/common/tier_equializer.csv`, + { delimiter: ";" }, + ); await Promise.all( locales.supported.map(async ({ locale }) => { @@ -161,12 +182,144 @@ export class ServerBlitzKitAPI extends BlitzKitAPI { async tankDefinitions() { const tankDefinitions = TankDefinitions.create(); + const gameModeNativeNames: Record = {}; + const squadBattleTypeGameModeNativeNameMatches = + this.squadBattleTypeStyles!.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( + /"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g, + ); + const gameTypeGameModeNativeNameMatches = + this.gameTypeSelectorStyles!.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( + /eGameMode\.([a-zA-Z]+) -> "~res:\/Gfx\/UI\/Hangar\/GameTypes\/battle-type_([^"]+)"/g, + ); + + for (const match of squadBattleTypeGameModeNativeNameMatches) { + const id = Number(match[1]); + gameModeNativeNames[match[3]] = id; + } + + for (const match of gameTypeGameModeNativeNameMatches) { + Object.entries(gameModeNativeNames).forEach(([key, value]) => { + if (key.toLowerCase() === match[2].toLowerCase()) { + gameModeNativeNames[match[1]] = value; + } + }); + } + + const botPattern = /^.+((tutorial_bot(\d+)?)|(TU))$/; + const slugRequesters = new Map(); + const idToNation: Record = {}; + + for (const nation of this.nationsDir!) { + const tankList = await this.vfs.xml<{ root: VehicleDefinitionList }>( + `Data/XML/item_defs/vehicles/${nation}/list.xml`, + ); + + for (const tankKey in tankList.root) { + if (botPattern.test(tankKey)) continue; + + const tank = tankList.root[tankKey]; + const tankId = toUniqueId(nation, tank.id); + + const name = ( + (tank.shortUserString + ? this.getString(tank.shortUserString) + : undefined) ?? this.getString(tank.userString) + ).locales.en; + + let slug = sluggify(name); + + idToNation[tankId] = nation; + + if (slugRequesters.has(slug)) { + slugRequesters.get(slug)!.push({ id: tankId, key: tankKey }); + } else { + slugRequesters.set(slug, [{ id: tankId, key: tankKey }]); + } + } + } + + const slugs = new Map(); + + slugRequesters.forEach((requesters, slug) => { + if (requesters.length === 1) { + slugs.set(requesters[0].id, slug); + return; + } + + console.warn( + `Multiple tanks share slug ${slug}: ${requesters + .map(({ key }) => key) + .join(", ")}`, + ); + + if (requesters.length !== 2) { + throw new Error("Unresolvable number of duplicates :("); + } + + const nonCanonical = requesters.find(({ key }) => key.endsWith("TUR")); + + if (nonCanonical === undefined) { + console.log("Using nations to discriminate"); + // both are non-tutorial tanks, will have to discriminate using nation + + requesters.forEach(({ id, key }) => { + const nation = idToNation[id]; + const discriminator = + this.nationSlugDiscriminators[ + nation as keyof typeof this.nationSlugDiscriminators + ]; + + console.log(`Solution: ${key} -> ${slug}-${discriminator}`); + slugs.set(id, `${slug}-${discriminator}`); + }); + } else { + console.log("Using tutorial bot suffix to discriminate"); + + const canonical = requesters.find(({ key }) => !key.endsWith("TUR")); + + if (canonical === undefined) { + throw new Error( + "Two tutorial bots share the same slug? The world is truly broken.", + ); + } + + console.log(`Solution: ${canonical.key} -> ${slug}`); + slugs.set(canonical.id, slug); + console.log(`Solution: ${nonCanonical.key} -> ${slug}-tur`); + slugs.set(nonCanonical.id, `${slug}-tur`); + } + }); + + const tankXps = new Map(); + return tankDefinitions; } async camouflageDefinitions() { const camouflageDefinitions = CamouflageDefinitions.create(); + for (const camoKey in this.camouflagesXml!.root.camouflages) { + const camo = this.camouflagesXml!.root.camouflages[camoKey]; + + const yamlEntry = this.camouflagesYaml![camoKey]; + const fullName = yamlEntry.userString + ? this.getString(yamlEntry.userString) + : undefined; + const shortName = yamlEntry.shortUserString + ? this.getString(yamlEntry.shortUserString) + : undefined; + const resolvedTankName = shortName ?? fullName; + const resolvedTankNameFull = + resolvedTankName === fullName ? undefined : fullName; + + camouflageDefinitions.camouflages[camo.id] = { + id: camo.id, + name: this.getString(camo.userString), + tank_name: resolvedTankName, + tank_name_full: resolvedTankNameFull, + }; + } + return camouflageDefinitions; } diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 72dfb7218..83bea7b00 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -13,3 +13,4 @@ export * from "./provisionsCommon"; export * from "./squadBattleTypeStylesYaml"; export * from "./tankmenAvatar"; export * from "./tanksStats"; +export * from "./vehicleDefinitionList"; diff --git a/packages/core/src/types/vehicleDefinitionList.ts b/packages/core/src/types/vehicleDefinitionList.ts new file mode 100644 index 000000000..9997ed50a --- /dev/null +++ b/packages/core/src/types/vehicleDefinitionList.ts @@ -0,0 +1,16 @@ +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; + }; +} diff --git a/packages/scripts/src/buildAssets/definitions.ts b/packages/scripts/src/buildAssets/definitions.ts index b4a79511d..dfbc375ea 100644 --- a/packages/scripts/src/buildAssets/definitions.ts +++ b/packages/scripts/src/buildAssets/definitions.ts @@ -20,7 +20,6 @@ import { ResearchCost, ShellType, SkillDefinitions, - sluggify, TankClass, TankDefinitions, TankPrice, @@ -30,24 +29,11 @@ import { Unlock, Vector3, } from "@blitzkit/core"; -import { readFile } from "fs/promises"; import { parse as parsePath } from "path"; import type { Vector3Tuple } from "three"; import { vfs } from "./constants"; import type { TankParameters } from "./tankIcons"; -const nationSlugDiscriminators = { - china: "cn", - european: "eu", - france: "fr", - germany: "de", - japan: "jp", - other: "ot", - uk: "gb", - usa: "us", - ussr: "ru", -}; - function parseResearchCost(raw: number | string) { if (typeof raw === "number") { return { @@ -293,22 +279,6 @@ interface VehicleDefinitions { }; }; } -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; @@ -406,7 +376,6 @@ const blitzShellKindToBlitzkit: Record = { HIGH_EXPLOSIVE: ShellType.SHELL_TYPE_HE, HOLLOW_CHARGE: ShellType.SHELL_TYPE_HEAT, }; -export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU))$/; const blitzModuleTypeToBlitzkit: Record = { chassis: ModuleType.MODULE_TYPE_TRACKS, engine: ModuleType.MODULE_TYPE_ENGINE, @@ -432,155 +401,6 @@ function assignArmor( } export async function definitions() { - const tankStringIdMap: Record = {}; - const tankXps = new Map(); - - const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages); - - const gameModeNativeNames: Record = {}; - - const consumableNativeNames: Record = {}; - const provisionNativeNames: Record = {}; - let equalizerDefinitions: string[][]; - - if ( - await vfs.resolve("Data/XML/item_defs/vehicles/common/tier_equializer.csv") - ) { - equalizerDefinitions = await vfs.csv( - `Data/XML/item_defs/vehicles/common/tier_equializer.csv`, - { delimiter: ";" }, - ); - } else { - const stubEqualizerDefinitions = await readFile("stub/equalizer.csv").then( - (buffer) => buffer.toString(), - ); - - equalizerDefinitions = await vfs._csv(stubEqualizerDefinitions, { - delimiter: ";", - }); - } - - for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll( - /"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g, - )) { - const id = Number(match[1]); - - gameModeNativeNames[match[3]] = id; - } - - 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 - ? getString(yamlEntry.userString) - : undefined; - const shortName = yamlEntry.shortUserString - ? getString(yamlEntry.shortUserString) - : undefined; - const resolvedTankName = shortName ?? fullName; - const resolvedTankNameFull = - resolvedTankName === fullName ? undefined : fullName; - - camouflageDefinitions.camouflages[camo.id] = { - id: camo.id, - name: getString(camo.userString), - tank_name: resolvedTankName, - tank_name_full: resolvedTankNameFull, - }; - }); - - const slugRequesters = new Map(); - const idToNation: Record = {}; - - for (const nation of nationsDir) { - const tankList = await vfs.xml<{ root: VehicleDefinitionList }>( - `Data/XML/item_defs/vehicles/${nation}/list.xml`, - ); - - for (const tankKey in tankList.root) { - if (botPattern.test(tankKey)) continue; - - const tank = tankList.root[tankKey]; - const tankId = toUniqueId(nation, tank.id); - - const name = ( - (tank.shortUserString ? getString(tank.shortUserString) : undefined) ?? - getString(tank.userString) - ).locales.en; - - let slug = sluggify(name); - - idToNation[tankId] = nation; - - if (slugRequesters.has(slug)) { - slugRequesters.get(slug)!.push({ id: tankId, key: tankKey }); - } else { - slugRequesters.set(slug, [{ id: tankId, key: tankKey }]); - } - } - } - - const slugs = new Map(); - - slugRequesters.forEach((requesters, slug) => { - if (requesters.length === 1) { - slugs.set(requesters[0].id, slug); - return; - } - - console.warn( - `Multiple tanks share slug ${slug}: ${requesters - .map(({ key }) => key) - .join(", ")}`, - ); - - if (requesters.length !== 2) { - throw new Error("Unresolvable number of duplicates :("); - } - - const nonCanonical = requesters.find(({ key }) => key.endsWith("TUR")); - - if (nonCanonical === undefined) { - console.log("Using nations to discriminate"); - // both are non-tutorial tanks, will have to discriminate using nation - - requesters.forEach(({ id, key }) => { - const nation = idToNation[id]; - const discriminator = - nationSlugDiscriminators[ - nation as keyof typeof nationSlugDiscriminators - ]; - - console.log(`Solution: ${key} -> ${slug}-${discriminator}`); - slugs.set(id, `${slug}-${discriminator}`); - }); - } else { - console.log("Using tutorial bot suffix to discriminate"); - - const canonical = requesters.find(({ key }) => !key.endsWith("TUR")); - - if (canonical === undefined) { - throw new Error( - "Two tutorial bots share the same slug? The world is truly broken.", - ); - } - - console.log(`Solution: ${canonical.key} -> ${slug}`); - slugs.set(canonical.id, slug); - console.log(`Solution: ${nonCanonical.key} -> ${slug}-tur`); - slugs.set(nonCanonical.id, `${slug}-tur`); - } - }); - for (const nation of nationsDir) { const tankList = await vfs.xml<{ root: VehicleDefinitionList }>( `Data/XML/item_defs/vehicles/${nation}/list.xml`, @@ -742,7 +562,7 @@ export async function definitions() { } }); - const camouflages = camouflagesXmlEntries + const camouflages = Object.entries(camouflagesXml.root.camouflages) .filter(([, camo]) => { if (!camo.vehicleFilter.include) return false; if (camo.unlockCostCategory !== "legendary-skins-gold") return false; @@ -761,7 +581,7 @@ export async function definitions() { }) .map(([, camo]) => camo.id); - let equalizerEntry = equalizerDefinitions.find( + let equalizerEntry = tierEqualizer.find( (line) => line[0] === `${nation}:${tankKey}`, ); @@ -1317,8 +1137,6 @@ export async function definitions() { ); Object.entries(consumablesCommon).forEach(([key, consumable]) => { - consumableNativeNames[key] = consumable.id; - const entry: Consumable = { id: consumable.id, game_mode_exclusive: "gameModeFilter" in consumable, @@ -1417,8 +1235,6 @@ export async function definitions() { }); Object.entries(provisionsCommon).forEach(([key, provision]) => { - provisionNativeNames[key] = provision.id; - const entry: Provision = { id: provision.id, exclude: [],