Merge branch 'main' into beg

This commit is contained in:
TrèsAbhi authored and GitHub committed 2026-10-25 18:11:37 -05:00
commit bb64d718c2
37 files changed
+412 -387

No files matched your search

@@ -285,6 +285,8 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
collection[locale] = localizedString;
}
collection[locales.default] = collection[locales.default] ?? name;
return { locales: collection } satisfies I18nString;
}
}
@@ -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>();
@@ -100,9 +102,11 @@ 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,
+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 -7
View File
@@ -1,10 +1,4 @@
{
"test": {
"default": "test",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"fox": "The quick brown fox jumps over the lazy dog."
},
"common": {
"name": "BlitzKit",
@@ -338,6 +332,13 @@
},
"tools": {
"credits": {
"name": "Credits",
"button": "View people",
"description": "Contributors of BlitzKit",
"seo_description": "BlitzKit is the work of many"
},
"playlist": {
"reset": "Reset",
"shuffle": "Shuffle",
@@ -996,7 +997,6 @@
"about": {
"title": "About",
"credits": "Credits",
"discord": "Discord",
"github": "GitHub",
"patches": "Patches"
+1
View File
@@ -6,6 +6,7 @@
"build-protos": "bun src/protos.ts",
"record-performance": "bun src/recordPerformance/index.ts",
"download-client": "bun src/downloadClient/index.ts",
"download-smart-dlc": "bun src/downloadSmartDLC/index.ts",
"vfs": "bun src/vfs/index.ts"
},
"dependencies": {
+23 -5
View File
@@ -9,6 +9,8 @@ import {
} from "./constants";
import { QuickGitHubRelease } from "./types";
const DEPOT_DOWNLOADER_RETRY_TIME = 30_000;
const provider = assertSecret(import.meta.env.CLIENT_PROVIDER);
const source = assertSecret(import.meta.env.CLIENT_SOURCE);
const dir = assertSecret(import.meta.env.CLIENT_DIR);
@@ -43,11 +45,27 @@ if (provider === "steam") {
const username = assertSecret(import.meta.env.STEAM_USERNAME);
const password = assertSecret(import.meta.env.STEAM_PASSWORD);
await $`${DEPOT_DOWNLOADER_EXECUTABLE} -os ${
platform
} -app ${id} -username ${username} -password ${
password
} -dir ${dir} -max-downloads ${DEPOT_DOWNLOADER_MAX_DOWNLOADS}`;
while (true) {
const result = await $`${
DEPOT_DOWNLOADER_EXECUTABLE
} -os ${platform} -app ${id} -username ${
username
} -password ${password} -dir ${dir} -max-downloads ${
DEPOT_DOWNLOADER_MAX_DOWNLOADS
}`.nothrow();
if (result.exitCode === 0) {
console.log("Client downloaded successfully!");
break;
}
console.error(
`DepotDownloader failed (exit ${result.exitCode}). Retrying in ${DEPOT_DOWNLOADER_RETRY_TIME / 1000} seconds...`,
);
await new Promise((resolve) =>
setTimeout(resolve, DEPOT_DOWNLOADER_RETRY_TIME),
);
}
} else if (provider === "zip") {
const url = assertSecret(import.meta.env.CLIENT_SOURCE);
@@ -2,9 +2,16 @@ import { dvp } from "@blitzkit/closed";
import { assertSecret } from "@blitzkit/core";
import { mkdir, writeFile } from "fs/promises";
import { parse as parsePath } from "path";
import ProgressBar from "progress";
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
import { PATCHES_ROOT, vfs } from "./buildAssets/constants";
import { RegionsPublish } from "../types/regionsPublish";
import { vfs } from "../vfs/vfs";
const CLIENT_DIR = assertSecret(import.meta.env.CLIENT_DIR);
const regionsPublish = await vfs.yaml<RegionsPublish>(
"Data/regions_publish.yaml",
);
const [smartDlcUrl] = regionsPublish.Common.smartDlcUrls;
const versionTextFile = await vfs.text(`Data/version.txt`);
const currentVersion = versionTextFile
@@ -18,9 +25,7 @@ console.log(`Installing patches for ${currentVersion}...`);
let patchIndex = 1;
while (true) {
const response = await fetch(
`${assertSecret(
import.meta.env.WOTB_DLC_CDN,
)}/dlc/s${currentVersion}_${patchIndex}.yaml`,
`${smartDlcUrl}/s${currentVersion}_${patchIndex}.yaml`,
);
console.log(patchIndex, response.status);
@@ -30,51 +35,34 @@ while (true) {
console.log(`Applying patch ${patchIndex}...`);
const data = parseYaml(await response.text());
const dvpm = await fetch(
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11}`,
).then((response) => response.arrayBuffer());
const dvpm = await fetch(`${smartDlcUrl}/${data.dx11}`).then((response) =>
response.arrayBuffer(),
);
const dvpd = await fetch(
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11.replace(
".dvpm",
".dvpd",
)}`,
`${smartDlcUrl}/${data.dx11.replace(".dvpm", ".dvpd")}`,
).then((response) => response.arrayBuffer());
const files = await dvp(dvpm, dvpd);
const bar = new ProgressBar(
`Patching ${files.length} files :bar`,
files.length,
);
for (const { path, data } of files) {
console.log(`Patching "${path}"...`);
const { dir } = parsePath(path);
try {
await mkdir(`${PATCHES_ROOT}/Data/${dir}`, { recursive: true });
} catch (error) {
console.warn(`Failed to make directory "${dir}"`);
}
await mkdir(`${CLIENT_DIR}/Data/${dir}`, { recursive: true });
const buffer = Buffer.from(data);
await writeFile(`${PATCHES_ROOT}/Data/${path}`, new Uint8Array(buffer));
bar.tick();
await writeFile(`${CLIENT_DIR}/Data/${path}`, new Uint8Array(buffer));
}
console.log("patches done for patch");
if ("dynamicContentLocalizationsDir" in data) {
console.log(
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${
data.dynamicContentLocalizationsDir
}/en.yaml`,
`${smartDlcUrl}/${data.dynamicContentLocalizationsDir}/en.yaml`,
);
const localizationsResponse = await fetch(
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${
data.dynamicContentLocalizationsDir
}/en.yaml`,
`${smartDlcUrl}/${data.dynamicContentLocalizationsDir}/en.yaml`,
);
console.log("fetched");
const newStrings = parseYaml(await localizationsResponse.text());
@@ -86,10 +74,10 @@ while (true) {
const patchedContent = stringifyYaml(patchedStrings);
console.log("stringified");
await mkdir(`${PATCHES_ROOT}/Data/Strings`, { recursive: true });
await mkdir(`${CLIENT_DIR}/Data/Strings`, { recursive: true });
console.log("mkdir");
await writeFile(
`${PATCHES_ROOT}/Data/Strings/en.yaml`,
`${CLIENT_DIR}/Data/Strings/en.yaml`,
new Uint8Array(Buffer.from(patchedContent)),
);
console.log("file");
@@ -0,0 +1,7 @@
export interface RegionsPublish {
Common: Common;
}
interface Common {
smartDlcUrls: string[];
}
+4 -4
View File
@@ -1,7 +1,4 @@
import { assertSecret } from "@blitzkit/core";
import { LocalVFS } from "@blitzkit/core/src/blitzkit/vfs/local";
export const vfs = new LocalVFS(assertSecret(import.meta.env.CLIENT_DIR));
import { vfs } from "./vfs";
const command = Bun.argv[2];
const path = Bun.argv[3];
@@ -20,4 +17,7 @@ if (command === "cat") {
for (const file of files) {
console.log(file);
}
} else if (command === "yaml") {
const content = await vfs.yaml(path);
await Bun.write(Bun.stdout, JSON.stringify(content, null, 2));
}
+4
View File
@@ -0,0 +1,4 @@
import { assertSecret } from "@blitzkit/core";
import { LocalVFS } from "@blitzkit/core/src/blitzkit/vfs/local";
export const vfs = new LocalVFS(assertSecret(import.meta.env.CLIENT_DIR));
Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,135 @@
---
import { Grid, Text } from "@radix-ui/themes";
import { credits } from "../config/credits";
import { getStrings } from "../core/i18n/getStrings";
interface Props {
locale: string;
}
const { locale } = Astro.props;
const strings = getStrings(locale);
---
<style>
.credits {
display: flex;
justify-content: center;
padding: var(--space-4);
.inner {
display: flex;
flex-direction: column;
gap: var(--space-2);
width: 100%;
max-width: 80rem;
.list {
gap: var(--space-5);
.link {
display: contents;
.person {
--profile-size: 5rem;
position: relative;
display: flex;
flex-direction: column;
padding: var(--space-4);
padding-top: 0;
margin-top: calc(var(--profile-size) / 2);
gap: var(--space-1);
box-shadow: var(--shadow-3);
border-radius: var(--radius-3);
.contain {
border-radius: inherit;
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
overflow: hidden;
.hint {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
filter: saturate(20) brightness(0.125) blur(4rem);
}
}
.text {
position: relative;
}
img {
border-radius: 50%;
position: relative;
margin-top: calc(var(--profile-size) / -2);
object-fit: cover;
width: var(--profile-size);
height: var(--profile-size);
margin-bottom: var(--space-2);
}
}
}
}
}
}
</style>
<div class="credits">
<div class="inner">
<Grid
className="list"
py="4"
columns="repeat(auto-fill, minmax(16rem, 1fr))"
flow="dense"
gap="4"
gapY="8"
>
{
credits.map((person) => (
<a target="_blank" href={person.url} class="link">
<div class="person">
<div class="contain">
<div
class="hint"
style={`background-image: url(${person.image})`}
/>
</div>
<img src={person.image} />
<Text weight="medium" className="text" color="gray" highContrast>
{person.name}
</Text>
<Text className="text" color="gray" wrap="pretty">
{person.description}
</Text>
</div>
</a>
))
}
</Grid>
</div>
</div>
@@ -84,9 +84,6 @@ const strings = getStrings(locale);
sm: "start",
}}
>
<LinkI18n {locale} href="/docs/about/credits" color="gray">
{strings.website.footer.about.credits}
</LinkI18n>
<Link
href="https://discord.gg/nDt7AjGJQH"
target="_blank"
+102
View File
@@ -0,0 +1,102 @@
interface CreditPeople {
name: string;
description: string;
url?: string;
image: string;
}
export const credits: CreditPeople[] = [
{
name: "TrèsAbhi",
description: "Founder and lead developer",
url: "https://abhi-deep.com/",
image: "https://github.com/tresabhi.png",
},
{
name: "HitJack",
description: "Developer on website",
url: "https://marksstuff.com",
image: "https://github.com/Hitjack007.png",
},
{
name: "aeson000",
description: "Advisor on game and website",
url: "https://discord.gg/WHdER7ZPAD",
image: "/media/users/aeson000.png",
},
{
name: "GonnaHetzMe",
description: "Developer and advisor on back end",
url: "https://github.com/karelpak1",
image: "https://github.com/karelpak1.png",
},
{
name: "ChickenMan7777",
description: "Moderator and former advisor on game mechanics",
url: "https://discord.gg/rEsUbXCqD9",
image: "/media/users/chickenman7777.webp",
},
{
name: "Avenir",
description: "Moderator and elite Advanced Recon Commando",
image: "/media/users/avenir.webp",
},
{
name: "Pyogenics",
description: "Former advisor on game 3D format",
url: "https://github.com/Pyogenics",
image: "https://github.com/Pyogenics.png",
},
{
name: "Prince_NA",
description: "Former advisor on bot and website",
image: "/media/users/prince-na.webp",
},
{
name: "Vovko",
description: "Former advisor on bot",
url: "https://amth.one/",
image: "https://github.com/cufee.png",
},
{
name: "S0AP",
description: "Former advisor on and advertiser for bot",
url: "https://discord.gg/3KfH93um3Y",
image: "/media/users/s0ap.webp",
},
{
name: "Synx",
description: "Former advisor on bot and website",
url: "https://www.youtube.com/@Synx_Blitz",
image: "/media/users/synx.webp",
},
{
name: "34wotb",
description: "Former advisor on bot",
url: "https://www.twitch.tv/34wotb/",
image: "/media/users/34wotb.jpg",
},
{
name: "HisRoyalFatness",
description: "Former advisor on website",
url: "https://www.youtube.com/@hisroyalfatness",
image: "/media/users/hisroyalfatness.jpg",
},
{
name: "Minitelrose",
description: "Former advisor on game mechanics",
url: "https://wotinspector.com/",
image: "/media/users/minitelrose.webp",
},
{
name: "Maddox",
description: "Game asset decompression",
url: "https://github.com/Maddoxkkm",
image: "https://github.com/Maddoxkkm.png",
},
{
name: "TJ Duff",
description: "Chief executive cheer leader",
image: "/media/users/tj-duff.jpg",
},
];
+4
View File
@@ -74,4 +74,8 @@ export const tools: Record<string, Tool> = {
href: "https://discord.gg/nDt7AjGJQH",
button: "plum",
},
credits: {
id: "credits",
button: "bronze",
},
};
@@ -1,237 +0,0 @@
import {
parseHierarchies,
Sc2ReadStream,
type CamouflagesXml,
type CamouflagesYaml,
type TankParameters,
} from "@blitzkit/core";
import {
Document,
Node,
NodeIO,
PropertyType,
Scene,
} from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { dedup, mergeDocuments, unpartition } from "@gltf-transform/functions";
import type { APIContext } from "astro";
import type { Vector3Tuple, Vector4Tuple } from "three";
import { SKIN_NUMBER } from "../../../../../constants/skinNumber";
import { api } from "../../../../../core/blitzkit/api";
import { mixStaticPaths } from "../../../../../core/blitzkit/mixStaticPaths";
import { vfs } from "../../../../../core/blitzkit/vfs";
import {
extractMaterials,
extractModel,
} from "../../../../[...chunk_models_tanks]/[id]/model.glb";
import { getStaticPaths as _getStaticPaths } from "../_index";
const SKIN_NUMBER_SLOTS = 5;
const DIGIT_COLUMNS = 5;
const DIGIT_ROWS = 2;
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
const camouflages = await api.camouflageDefinitions();
return Object.values(camouflages.camouflages)
.filter((camouflage) => camouflage.tank_id)
.map((camouflage) => ({
params: { id: camouflage.tank_id!, skin: camouflage.id },
props: { skin: camouflage.id },
}));
});
const camouflagesXml = await vfs.xml<{ root: CamouflagesXml }>(
"Data/XML/item_defs/vehicles/common/camouflages.xml",
);
const camouflagesYaml = await vfs.yaml<CamouflagesYaml>(
"Data/camouflages.yaml",
);
const customization = await vfs.yaml<
Record<string, { Name: string; Path: string }>
>("Data/3d/Customization.yaml");
export async function GET({ props }: APIContext<{ skin: number }>) {
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const items = new Map(
Object.values(customization).map(({ Name, Path }) => [
Name,
Path.replace("~res:/3d/", "").replace(/\.sc2$/, ""),
]),
);
const key = Object.keys(camouflagesXml.root.camouflages).find(
(key) => camouflagesXml.root.camouflages[key].id === props.skin,
)!;
const include = camouflagesXml.root.camouflages[key].vehicleFilter.include;
const [nation, tankKey] = (
include as { vehicle: { name: string } }
).vehicle.name.split(":");
const { preset, customEntities } = camouflagesYaml[key];
const skinNumber = Number(preset?.match(/(\d+)$/)?.[1] ?? 1);
const parameters = await vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
);
const basePath = parameters.resourcesPath.blitzModelPath.replace(
/\.sc2$/,
"",
);
const sc2 = new Sc2ReadStream(
(await vfs.file(`Data/3d/${basePath}.sc2`)).buffer as ArrayBuffer,
).sc2();
const slots: Record<
string,
{
target: string;
position: Vector3Tuple;
rotation: Vector4Tuple;
scale: Vector3Tuple;
}
> = {};
const states: Record<string, string | null> = {};
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
for (const component of components) {
if (component["comp.typename"] === "NewSlotComponent") {
for (const slot of Object.values(component.slots)) {
slots[slot.name] = {
target: hierarchy.name,
position: slot.transform.position,
rotation: slot.transform.rotation,
scale: slot.transform.scale,
};
}
}
if (component["comp.typename"] === "SlotComponent") {
slots[component["sc.slotName"]] = {
target: hierarchy.name,
position: component["sc.attachmentTranslation"] as Vector3Tuple,
rotation: component["sc.attachmentRotation"] as Vector4Tuple,
scale: component["sc.attachmentScale"] as Vector3Tuple,
};
}
if (component["comp.typename"] !== "StateSwitcherComponent") continue;
const active = component[`ssc.state${component["ssc.activeState"]}`];
// camos without their own variant keep the default piece, but groups
// of elements to hide stay hidden
let state: string | null =
active && !/skin\d*/.test(active) && !active.includes("hide")
? active
: null;
for (let index = 0; index < component["ssc.statesCount"]; index++) {
const name = component[`ssc.state${index}`];
const match = name.match(/skin(\d*)/);
if (
match &&
!name.includes("hide") &&
Number(match[1] || 1) === skinNumber
) {
state = name;
}
}
states[hierarchy.name] = state;
}
return null;
});
const document = new Document();
const scene = document.createScene().setExtras({ states });
const targets = new Map<string, Node>();
document.createBuffer();
if (preset) await extractMaterials(document, sc2, basePath, preset);
for (const name in customEntities) {
const slot = slots[name];
const path = items.get(customEntities[name].item);
if (
!slot ||
!path ||
!(await vfs.resolve(`Data/3d/${path}.sc2`)) ||
!(await vfs.resolve(`Data/3d/${path}.scg`))
) {
continue;
}
const part = await extractModel(vfs, path, true);
const map = mergeDocuments(document, part);
const partScene = map.get(part.getRoot().listScenes()[0]) as Scene;
const node = document
.createNode(name)
.setTranslation(slot.position)
.setRotation(slot.rotation)
.setScale(slot.scale);
let target = targets.get(slot.target);
if (!target) {
target = document.createNode(slot.target);
targets.set(slot.target, target);
scene.addChild(target);
}
for (const child of partScene.listChildren()) {
node.addChild(child as Node);
}
partScene.dispose();
target.addChild(node);
}
applySkinNumber(document, SKIN_NUMBER);
await document.transform(
dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.TEXTURE] }),
unpartition(),
);
return new Response(await nodeIO.writeBinary(document));
}
function applySkinNumber(document: Document, number: string) {
const start = Math.ceil((SKIN_NUMBER_SLOTS - number.length) / 2) + 1;
for (const node of document.getRoot().listNodes()) {
const match = node.getName().match(/_number_\d+_(\d+)$/);
if (!match) continue;
const digit = number[Number(match[1]) - start];
if (digit === undefined) {
node.dispose();
continue;
}
const cell = (Number(digit) + 9) % 10;
const column = cell % DIGIT_COLUMNS;
const row = Math.floor(cell / DIGIT_COLUMNS);
node.traverse((child) => {
for (const primitive of child.getMesh()?.listPrimitives() ?? []) {
const uvs = primitive.getAttribute("TEXCOORD_0")?.clone();
if (!uvs) continue;
for (let index = 0; index < uvs.getCount(); index++) {
const [u, v] = uvs.getElement(index, [0, 0]) as [number, number];
uvs.setElement(index, [
(column + u) / DIGIT_COLUMNS,
(row + v) / DIGIT_ROWS,
]);
}
primitive.setAttribute("TEXCOORD_0", uvs);
}
});
}
}
@@ -0,0 +1,13 @@
---
import CreditsPromo from "../../../../components/CreditsPromo.astro";
import { tools } from "../../../../constants/tools";
import ToolLayout from "../../../../layouts/ToolLayout.astro";
export { getStaticPaths } from "../index.astro";
const { locale } = Astro.props;
---
<ToolLayout tool={tools.credits} {locale}>
<CreditsPromo {locale} />
</ToolLayout>