Feature: Looping garage animations for legendary camouflages

Legendary camo add-ons now loop their garage animation in the Tankopedia viewer. It's always on, with no settings.

- The garage replay now also works out which motions end up looping, and the export writes only those clips
- Customization parts export skeletons, skin weights and sockets
- Parts render as primitives so their bones can animate, with attachments layered on top
- Base tank models are unchanged
This commit is contained in:
Mark Greene committed 2026-10-31 22:15:57 +13:00
commit 8566b2cc2f
94 files changed
+2037 -1861

No files matched your search

+26 -1
View File
@@ -88,6 +88,7 @@ enum PvrChannelType {
enum ResolvedBitRatePixelFormat {
R4G4B4A4,
R8G8B8A8,
R5G6B5A0,
}
@@ -137,6 +138,27 @@ export class PvrReadStream extends ReadStream {
};
}
case ResolvedBitRatePixelFormat.R8G8B8A8: {
const data = Buffer.alloc(pixelCount * 4);
times(pixelCount, (index) => {
const buffer = this.consumeUint8Array(4);
const bufferIndex = index * 4;
data[bufferIndex] = buffer[0]; // R
data[bufferIndex + 1] = buffer[1]; // G
data[bufferIndex + 2] = buffer[2]; // B
data[bufferIndex + 3] = buffer[3]; // A
});
return {
data,
width: header.width,
height: header.height,
channels: 4 as const,
};
}
case ResolvedBitRatePixelFormat.R5G6B5A0: {
const data = Buffer.alloc(pixelCount * 3);
@@ -208,10 +230,13 @@ export class PvrReadStream extends ReadStream {
if (pixelFormat.order === "rgba") {
if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) {
return ResolvedBitRatePixelFormat.R4G4B4A4;
} else
} else if (isEqual(pixelFormat.bitRates, [8, 8, 8, 8])) {
return ResolvedBitRatePixelFormat.R8G8B8A8;
} else {
throw new TypeError(
`Unhandled rgba bit rate ${pixelFormat.bitRates.join(", ")}`,
);
}
} else if (pixelFormat.order === "rgb\0") {
if (isEqual(pixelFormat.bitRates, [5, 6, 5, 0])) {
return ResolvedBitRatePixelFormat.R5G6B5A0;
+7 -1
View File
@@ -40,6 +40,8 @@ export interface ConfigArchive {
interface EnabledPresets {
AlphaTest: boolean;
AlphaBlend?: boolean;
AlphaBlendAdditive?: boolean;
}
interface Flags {
@@ -156,7 +158,11 @@ export type Component =
"ro.notShadowOnly": boolean;
"ro.sOclCull": boolean;
"ro.sOclIndex": number;
} & Record<`rb${number}.${"lodIndex" | "switchIndex"}`, number>;
} & Record<`rb${number}.${"lodIndex" | "switchIndex"}`, number> &
Record<
`skinnedObject.batch${number}.targetsData`,
ArrayBuffer | undefined
>;
}
| {
"comp.typename": "SlotComponent";