mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-10 03:06:11 +01:00
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:
commit
8566b2cc2f
94 files changed
+2037
-1861
No files matched your search
@@ -246,6 +246,27 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected getTankFilterIds(
|
||||
name: string,
|
||||
definitionKey: string,
|
||||
filter: "include" | "exclude",
|
||||
) {
|
||||
const keys = name.trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
return keys.flatMap((key) => {
|
||||
const id = this.tankStringIdMap[key];
|
||||
|
||||
if (id === undefined) {
|
||||
console.warn(
|
||||
`Skipping unavailable tank "${key}" in ${filter} vehicle filter for "${definitionKey}"`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return [id];
|
||||
});
|
||||
}
|
||||
|
||||
protected getString(name: string) {
|
||||
const collection: Record<string, string> = {
|
||||
[locales.default]: this.stringsI18n[locales.default][name],
|
||||
@@ -264,6 +285,8 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
|
||||
collection[locale] = localizedString;
|
||||
}
|
||||
|
||||
collection[locales.default] = collection[locales.default] ?? name;
|
||||
|
||||
return { locales: collection } satisfies I18nString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,7 +387,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
||||
: TankType.TANK_TYPE_RESEARCHABLE,
|
||||
tier: tank.level,
|
||||
class: tankClass,
|
||||
testing: tankTags.includes("testTank"),
|
||||
testing: tankTags.includes("testTank") || tank.level === 11,
|
||||
deprecated: tankTags.includes("deprecated"),
|
||||
price: tankPrice,
|
||||
camouflage_still: tankDefinition.root.invisibility.still,
|
||||
@@ -595,9 +595,25 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
||||
for (const shellKey in gunListEntry.shots) {
|
||||
const gunShellEntry = gunListEntry.shots[shellKey];
|
||||
const shell = shellList.root[shellKey];
|
||||
|
||||
// Newer client data stores shot properties on the shell rather
|
||||
// than repeating them under every gun's <shots> entry.
|
||||
const speed =
|
||||
typeof gunShellEntry === "string"
|
||||
? shell.speed
|
||||
: gunShellEntry.speed;
|
||||
const maxDistance =
|
||||
typeof gunShellEntry === "string"
|
||||
? shell.maxDistance
|
||||
: gunShellEntry.maxDistance;
|
||||
const piercingPower =
|
||||
typeof gunShellEntry === "string"
|
||||
? shell.piercingPower
|
||||
: gunShellEntry.piercingPower;
|
||||
|
||||
const shellId = toUniqueId(nation, shell.id);
|
||||
const shellName = this.getString(shell.userString);
|
||||
const penetrationRaw = gunShellEntry.piercingPower
|
||||
const penetrationRaw = piercingPower
|
||||
.split(" ")
|
||||
.filter((penetrationString) => penetrationString !== "")
|
||||
.map(Number);
|
||||
@@ -607,7 +623,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
||||
].shells.push({
|
||||
id: shellId,
|
||||
name: shellName,
|
||||
velocity: gunShellEntry.speed,
|
||||
velocity: speed,
|
||||
armor_damage: shell.damage.armor,
|
||||
module_damage: shell.damage.devices,
|
||||
caliber: shell.caliber,
|
||||
@@ -623,7 +639,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
||||
near: penetrationRaw[0],
|
||||
far: penetrationRaw[1],
|
||||
},
|
||||
range: gunShellEntry.maxDistance,
|
||||
range: maxDistance,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
|
||||
);
|
||||
const sc2 = new Sc2ReadStream(
|
||||
(
|
||||
await this.vfs.file(`Data/3d/${parameters.resourcesPath.blitzModelPath}`)
|
||||
await this.vfs.file(
|
||||
`Data/3d/${parameters.resourcesPath.blitzModelPath}`,
|
||||
)
|
||||
).buffer as ArrayBuffer,
|
||||
).sc2();
|
||||
const presets = new Set<string>();
|
||||
@@ -51,6 +53,8 @@ export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
|
||||
for (const camoKey in this.camouflagesXml!.root.camouflages) {
|
||||
const camo = this.camouflagesXml!.root.camouflages[camoKey];
|
||||
|
||||
if (!(camoKey in this.camouflagesYaml!)) continue;
|
||||
|
||||
const yamlEntry = this.camouflagesYaml![camoKey];
|
||||
const fullName = yamlEntry.userString
|
||||
? this.getString(yamlEntry.userString)
|
||||
@@ -83,14 +87,19 @@ export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
|
||||
),
|
||||
);
|
||||
|
||||
const [nation, tankKey] = vehicle.split(":");
|
||||
// some unique camos only set the preset for their one tank
|
||||
const preset =
|
||||
yamlEntry.preset ??
|
||||
yamlEntry.override?.[nation]?.override?.[tankKey]?.preset;
|
||||
let hasPreset = false;
|
||||
|
||||
if (camo.group === "legendary" && yamlEntry.preset !== undefined) {
|
||||
if (preset !== undefined) {
|
||||
if (!skinPresets.has(vehicle)) {
|
||||
skinPresets.set(vehicle, this.skinPresets(vehicle));
|
||||
}
|
||||
|
||||
hasPreset = (await skinPresets.get(vehicle)!).has(yamlEntry.preset);
|
||||
hasPreset = (await skinPresets.get(vehicle)!).has(preset);
|
||||
}
|
||||
|
||||
if (resolvedParts.some(Boolean) || hasPreset) {
|
||||
@@ -98,12 +107,15 @@ export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
|
||||
}
|
||||
}
|
||||
|
||||
const name = this.getString(camo.userString);
|
||||
|
||||
camouflageDefinitions.camouflages[camo.id] = {
|
||||
id: camo.id,
|
||||
name: this.getString(camo.userString),
|
||||
name,
|
||||
tank_name: resolvedTankName,
|
||||
tank_name_full: resolvedTankNameFull,
|
||||
tank_id: tankId,
|
||||
attachment: camo.kind === "customization",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -44,9 +44,7 @@ export abstract class ServerBlitzKitAPI6 extends ServerBlitzKitAPI5 {
|
||||
filter_type: {
|
||||
$case: "ids",
|
||||
value: {
|
||||
ids: includeRaw.name.split(/ +/).map((key) => {
|
||||
return this.tankStringIdMap[key];
|
||||
}),
|
||||
ids: this.getTankFilterIds(includeRaw.name, key, "include"),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -72,9 +70,7 @@ export abstract class ServerBlitzKitAPI6 extends ServerBlitzKitAPI5 {
|
||||
filter_type: {
|
||||
$case: "ids",
|
||||
value: {
|
||||
ids: excludeRaw.name.split(/ +/).map((key) => {
|
||||
return this.tankStringIdMap[key];
|
||||
}),
|
||||
ids: this.getTankFilterIds(excludeRaw.name, key, "exclude"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
|
||||
async provisionDefinitions() {
|
||||
const provisionDefinitions = ProvisionDefinitions.create();
|
||||
|
||||
Object.entries(this.provisionsCommon).forEach(([, provision]) => {
|
||||
Object.entries(this.provisionsCommon).forEach(([key, provision]) => {
|
||||
const entry: Provision = {
|
||||
id: provision.id,
|
||||
exclude: [],
|
||||
@@ -42,9 +42,7 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
|
||||
filter_type: {
|
||||
$case: "ids",
|
||||
value: {
|
||||
ids: includeRaw.name.split(/ +/).map((key) => {
|
||||
return this.tankStringIdMap[key];
|
||||
}),
|
||||
ids: this.getTankFilterIds(includeRaw.name, key, "include"),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -70,9 +68,7 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
|
||||
filter_type: {
|
||||
$case: "ids",
|
||||
value: {
|
||||
ids: excludeRaw.name
|
||||
.split(/ +/)
|
||||
.map((key) => this.tankStringIdMap[key]),
|
||||
ids: this.getTankFilterIds(excludeRaw.name, key, "exclude"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,4 +14,5 @@ message Camouflage {
|
||||
optional I18nString tank_name = 3;
|
||||
optional I18nString tank_name_full = 4;
|
||||
optional uint32 tank_id = 5;
|
||||
optional bool attachment = 6;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export interface Camouflage {
|
||||
tank_name?: I18nString | undefined;
|
||||
tank_name_full?: I18nString | undefined;
|
||||
tank_id?: number | undefined;
|
||||
attachment?: boolean | undefined;
|
||||
}
|
||||
|
||||
function createBaseCamouflageDefinitions(): CamouflageDefinitions {
|
||||
@@ -192,7 +193,7 @@ export const CamouflageDefinitions_CamouflagesEntry: MessageFns<CamouflageDefini
|
||||
};
|
||||
|
||||
function createBaseCamouflage(): Camouflage {
|
||||
return { id: 0, name: undefined, tank_name: undefined, tank_name_full: undefined, tank_id: 0 };
|
||||
return { id: 0, name: undefined, tank_name: undefined, tank_name_full: undefined, tank_id: 0, attachment: false };
|
||||
}
|
||||
|
||||
export const Camouflage: MessageFns<Camouflage> = {
|
||||
@@ -212,6 +213,9 @@ export const Camouflage: MessageFns<Camouflage> = {
|
||||
if (message.tank_id !== undefined && message.tank_id !== 0) {
|
||||
writer.uint32(40).uint32(message.tank_id);
|
||||
}
|
||||
if (message.attachment !== undefined && message.attachment !== false) {
|
||||
writer.uint32(48).bool(message.attachment);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
@@ -262,6 +266,14 @@ export const Camouflage: MessageFns<Camouflage> = {
|
||||
message.tank_id = reader.uint32();
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 48) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.attachment = reader.bool();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
@@ -278,6 +290,7 @@ export const Camouflage: MessageFns<Camouflage> = {
|
||||
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,
|
||||
attachment: isSet(object.attachment) ? globalThis.Boolean(object.attachment) : false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -298,6 +311,9 @@ export const Camouflage: MessageFns<Camouflage> = {
|
||||
if (message.tank_id !== undefined && message.tank_id !== 0) {
|
||||
obj.tank_id = Math.round(message.tank_id);
|
||||
}
|
||||
if (message.attachment !== undefined && message.attachment !== false) {
|
||||
obj.attachment = message.attachment;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
@@ -317,6 +333,7 @@ export const Camouflage: MessageFns<Camouflage> = {
|
||||
? I18nString.fromPartial(object.tank_name_full)
|
||||
: undefined;
|
||||
message.tank_id = object.tank_id ?? 0;
|
||||
message.attachment = object.attachment ?? false;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -5,5 +5,10 @@ export type CamouflagesYaml = Record<
|
||||
shortUserString?: string;
|
||||
preset?: string;
|
||||
customEntities?: Record<string, { item: string }>;
|
||||
// nation -> tank
|
||||
override?: Record<
|
||||
string,
|
||||
{ override?: Record<string, { preset?: string }> }
|
||||
>;
|
||||
}
|
||||
>;
|
||||
@@ -20,11 +20,13 @@ export interface GunDefinitionsList {
|
||||
burst?: { count: number; rate: number };
|
||||
clip?: { count: number; rate: number };
|
||||
shots: {
|
||||
[key: string]: {
|
||||
speed: number;
|
||||
piercingPower: string;
|
||||
maxDistance: number;
|
||||
};
|
||||
[key: string]:
|
||||
| ""
|
||||
| {
|
||||
speed: number;
|
||||
piercingPower: string;
|
||||
maxDistance: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,6 +10,9 @@ export type ShellDefinitionsList = Record<
|
||||
normalizationAngle?: number;
|
||||
ricochetAngle?: number;
|
||||
explosionRadius?: number;
|
||||
speed: number;
|
||||
maxDistance?: number;
|
||||
piercingPower: string;
|
||||
}
|
||||
> & {
|
||||
icons: Record<string, string>;
|
||||
|
||||
Reference in new issue
Block a user