(3) some definitions headers

This commit is contained in:
tresabhi committed 2026-08-01 13:26:45 -05:00
1 parent fd8448b0a6
commit 244968bbbf
4 files changed
+175 -189

No files matched your search

+156 -3
View File
@@ -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<string, Record<string, string>> = {};
private nationSlugDiscriminators = {
china: "cn",
european: "eu",
france: "fr",
germany: "de",
japan: "jp",
other: "ot",
uk: "gb",
usa: "us",
ussr: "ru",
};
private stringsI18n: Record<string, Record<string, string>> = {};
private nationsDir?: string[];
consumablesCommon: ConsumablesCommon = {};
provisionsCommon: ProvisionsCommon = {};
private tankStringIdMap: Record<string, number> = {};
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<CombatRolesYaml>(
`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<string, number> = {};
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<string, { id: number; key: string }[]>();
const idToNation: Record<number, string> = {};
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<number, string>();
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<number, ResearchCost>();
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;
}
+1
View File
@@ -13,3 +13,4 @@ export * from "./provisionsCommon";
export * from "./squadBattleTypeStylesYaml";
export * from "./tankmenAvatar";
export * from "./tanksStats";
export * from "./vehicleDefinitionList";
@@ -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<string, string>;
configurationModes: string;
};
}
+2 -186
View File
@@ -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<string, string>;
configurationModes: string;
};
}
interface TurretDefinitionsList {
nextAvailableId: number;
ids: Record<string, number>;
@@ -406,7 +376,6 @@ const blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = {
HIGH_EXPLOSIVE: ShellType.SHELL_TYPE_HE,
HOLLOW_CHARGE: ShellType.SHELL_TYPE_HEAT,
};
export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU))$/;
const blitzModuleTypeToBlitzkit: Record<keyof BlitzModuleType, ModuleType> = {
chassis: ModuleType.MODULE_TYPE_TRACKS,
engine: ModuleType.MODULE_TYPE_ENGINE,
@@ -432,155 +401,6 @@ function assignArmor(
}
export async function definitions() {
const tankStringIdMap: Record<string, number> = {};
const tankXps = new Map<number, ResearchCost>();
const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages);
const gameModeNativeNames: Record<string, number> = {};
const consumableNativeNames: Record<string, number> = {};
const provisionNativeNames: Record<string, number> = {};
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<string, { id: number; key: string }[]>();
const idToNation: Record<number, string> = {};
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<number, string>();
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: [],