Feature: Legendary camouflages in the Tankopedia viewer

This commit is contained in:
Mark Greene committed 2026-10-22 16:12:03 +13:00
1 parent 007d4e9540
commit a495a5f208
17 files changed
+915 -172

No files matched your search

@@ -1,11 +1,52 @@
import { CamouflageDefinitions } from "@blitzkit/core";
import {
CamouflageDefinitions,
Sc2ReadStream,
type TankParameters,
} from "@blitzkit/core";
import { isEqual, times } from "lodash-es";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI1 } from "./1_tankDefinitions";
export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
private async skinPresets(vehicle: string) {
const [nation, tankKey] = vehicle.split(":");
const parameters = await this.vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
);
const sc2 = new Sc2ReadStream(
(
await this.vfs.file(`Data/3d/${parameters.resourcesPath.blitzModelPath}`)
).buffer as ArrayBuffer,
).sc2();
const presets = new Set<string>();
for (const node of sc2["#dataNodes"]) {
if (typeof node.configCount !== "number") continue;
times(node.configCount, (index) => node[`configArchive_${index}`])
.filter(
(archive) =>
!isEqual(archive.textures, node.configArchive_0.textures),
)
.forEach((archive) => presets.add(archive.configName));
}
return presets;
}
@Cache()
async camouflageDefinitions() {
const camouflageDefinitions = CamouflageDefinitions.create();
const customization = await this.vfs.yaml<
Record<string, { Name: string; Path: string }>
>("Data/3d/Customization.yaml");
const items = new Map(
Object.values(customization).map(({ Name, Path }) => [
Name,
Path.replace("~res:/", "Data/").replace(/\.sc2$/, ""),
]),
);
const skinPresets = new Map<string, Promise<Set<string>>>();
for (const camoKey in this.camouflagesXml!.root.camouflages) {
const camo = this.camouflagesXml!.root.camouflages[camoKey];
@@ -22,12 +63,49 @@ export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
const resolvedTankName = shortName ?? fullName;
const resolvedTankNameFull =
resolvedTankName === fullName ? undefined : fullName;
const include = camo.vehicleFilter.include;
const vehicle =
include && !Array.isArray(include) && "vehicle" in include
? include.vehicle?.name
: undefined;
let tankId: number | undefined = undefined;
if (vehicle && vehicle in this.tankStringIdMap) {
const resolvedParts = await Promise.all(
Object.values(yamlEntry.customEntities ?? {}).map(
async ({ item }) => {
const path = items.get(item);
return (
path !== undefined &&
(await this.vfs.resolve(`${path}.sc2`)) &&
(await this.vfs.resolve(`${path}.scg`))
);
},
),
);
let hasPreset = false;
if (camo.group === "legendary" && yamlEntry.preset !== undefined) {
if (!skinPresets.has(vehicle)) {
skinPresets.set(vehicle, this.skinPresets(vehicle));
}
hasPreset = (await skinPresets.get(vehicle)!).has(yamlEntry.preset);
}
if (resolvedParts.some(Boolean) || hasPreset) {
tankId = this.tankStringIdMap[vehicle];
}
}
camouflageDefinitions.camouflages[camo.id] = {
id: camo.id,
name: this.getString(camo.userString),
tank_name: resolvedTankName,
tank_name_full: resolvedTankNameFull,
tank_id: tankId,
};
}
@@ -13,4 +13,5 @@ message Camouflage {
required I18nString name = 2;
optional I18nString tank_name = 3;
optional I18nString tank_name_full = 4;
optional uint32 tank_id = 5;
}
@@ -24,6 +24,7 @@ export interface Camouflage {
name: I18nString | undefined;
tank_name?: I18nString | undefined;
tank_name_full?: I18nString | undefined;
tank_id?: number | undefined;
}
function createBaseCamouflageDefinitions(): CamouflageDefinitions {
@@ -191,7 +192,7 @@ export const CamouflageDefinitions_CamouflagesEntry: MessageFns<CamouflageDefini
};
function createBaseCamouflage(): Camouflage {
return { id: 0, name: undefined, tank_name: undefined, tank_name_full: undefined };
return { id: 0, name: undefined, tank_name: undefined, tank_name_full: undefined, tank_id: 0 };
}
export const Camouflage: MessageFns<Camouflage> = {
@@ -208,6 +209,9 @@ export const Camouflage: MessageFns<Camouflage> = {
if (message.tank_name_full !== undefined) {
I18nString.encode(message.tank_name_full, writer.uint32(34).fork()).join();
}
if (message.tank_id !== undefined && message.tank_id !== 0) {
writer.uint32(40).uint32(message.tank_id);
}
return writer;
},
@@ -250,6 +254,14 @@ export const Camouflage: MessageFns<Camouflage> = {
message.tank_name_full = I18nString.decode(reader, reader.uint32());
continue;
}
case 5: {
if (tag !== 40) {
break;
}
message.tank_id = reader.uint32();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@@ -265,6 +277,7 @@ export const Camouflage: MessageFns<Camouflage> = {
name: isSet(object.name) ? I18nString.fromJSON(object.name) : undefined,
tank_name: isSet(object.tank_name) ? I18nString.fromJSON(object.tank_name) : undefined,
tank_name_full: isSet(object.tank_name_full) ? I18nString.fromJSON(object.tank_name_full) : undefined,
tank_id: isSet(object.tank_id) ? globalThis.Number(object.tank_id) : 0,
};
},
@@ -282,6 +295,9 @@ export const Camouflage: MessageFns<Camouflage> = {
if (message.tank_name_full !== undefined) {
obj.tank_name_full = I18nString.toJSON(message.tank_name_full);
}
if (message.tank_id !== undefined && message.tank_id !== 0) {
obj.tank_id = Math.round(message.tank_id);
}
return obj;
},
@@ -300,6 +316,7 @@ export const Camouflage: MessageFns<Camouflage> = {
message.tank_name_full = (object.tank_name_full !== undefined && object.tank_name_full !== null)
? I18nString.fromPartial(object.tank_name_full)
: undefined;
message.tank_id = object.tank_id ?? 0;
return message;
},
};
@@ -3,5 +3,7 @@ export type CamouflagesYaml = Record<
{
userString?: string;
shortUserString?: string;
preset?: string;
customEntities?: Record<string, { item: string }>;
}
>;