Merge branch 'main' into data-driven

This commit is contained in:
tresabhi committed 2026-11-09 21:28:46 -06:00
commit ffa24a9bc9
44 files changed
+1337 -358

No files matched your search

-9
View File
@@ -1,16 +1,7 @@
{
"name": "@blitzkit/scripts",
"private": true,
"scripts": {
"discover-ids": "bun src/discoverIds/index.ts",
"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": {
"@blitzkit/closed": "workspace:*",
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@gltf-transform/core": "^4.2.1",
+4
View File
@@ -0,0 +1,4 @@
export const WGC_CLIENT_GUID = "WOTB.WW.PRODUCTION";
export const WGC_CLIENT_URL = "https://wgus-eu.wargaming.net";
export const WGC_METADATA_PROTOCOL_VERSION = "7.10";
export const WGC_PATCHES_CHAIN_PROTOCOL_VERSION = "1.11";
+26 -105
View File
@@ -1,117 +1,38 @@
import { assertSecret } from "@blitzkit/core";
import { $ } from "bun";
import { mkdir, rm } from "node:fs/promises";
import {
DEPOT_DOWNLOADER_EXECUTABLE,
DEPOT_DOWNLOADER_MAX_DOWNLOADS,
DEPOT_DOWNLOADER_OUTPUT,
WGPKG_DIR,
} from "./constants";
import { QuickGitHubRelease } from "./types";
import { downloadSteamClient } from "./steam";
import { downloadWGCClient } from "./wgc";
import { downloadWGPKGClient } from "./wgpkg";
import { downloadZipClient } from "./zip";
const DEPOT_DOWNLOADER_RETRY_TIME = 30_000;
export 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);
const provider = import.meta.env.CLIENT_PROVIDER;
export const source = import.meta.env.CLIENT_SOURCE;
export const dir = assertSecret(import.meta.env.CLIENT_DIR);
await rm(dir, { recursive: true, force: true });
await mkdir(dir, { recursive: true });
if (provider === "steam") {
const [id, platform] = source.split("/");
switch (provider) {
// steam, identical to wgc
case "steam":
await downloadSteamClient();
break;
console.log("Downloading Depot Downloader...");
// from zip, usually has a root directory for opentest
case "zip":
await downloadZipClient();
break;
const releases = await fetch(
"https://api.github.com/repos/SteamRE/DepotDownloader/releases/latest",
).then((response) => response.json() as Promise<QuickGitHubRelease>);
const asset = releases.assets.find((asset) =>
asset.name.includes("linux-x64"),
);
// effectively the same as wgc, but direct for preview
case "wgpkg":
await downloadWGPKGClient();
break;
if (!asset) {
throw new Error("No linux-x64 asset found for Steam DepotDownloader");
}
await rm(DEPOT_DOWNLOADER_OUTPUT, { recursive: true, force: true });
await $`curl -L "${asset.browser_download_url}" -o "${DEPOT_DOWNLOADER_OUTPUT}.zip"`;
await $`unzip "${DEPOT_DOWNLOADER_OUTPUT}.zip" -d "${DEPOT_DOWNLOADER_OUTPUT}"`;
await $`chmod +x "${DEPOT_DOWNLOADER_EXECUTABLE}"`;
console.log("Downloading client...");
const username = assertSecret(import.meta.env.STEAM_USERNAME);
const password = assertSecret(import.meta.env.STEAM_PASSWORD);
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);
console.log("Downloading and extracting zipped client...");
await mkdir(dir, { recursive: true });
await $`curl -L "${url}" | bsdtar -xf- --strip-components=1 -C ${dir}`;
} else if (provider === "wgpkg") {
let index = 1;
const urls: string[] = [];
console.log("Discovering packages...");
while (true) {
const url = `${source}.${index.toString().padStart(3, "0")}`;
const response = await fetch(url);
if (response.status === 200) {
urls.push(url);
} else if (response.status === 404) {
break;
} else {
throw new Error(`Wargaming response error status: ${response.status}`);
}
index++;
}
console.log(`Downloading ${urls.length} packages...`);
await rm(WGPKG_DIR, { recursive: true, force: true });
await mkdir(WGPKG_DIR, { recursive: true });
const inputContent = urls
.map((url) => {
const name = url.split("/").pop()!;
return `${url}\n out=${name}\n dir=${WGPKG_DIR}`;
})
.join("\n");
await $`echo ${inputContent} | aria2c -j ${urls.length} -x 2 -s 2 -i -`;
const firstChunk = `${WGPKG_DIR}/${urls[0].split("/").pop()!}`;
await $`7z x "${firstChunk}" -o${dir} -y`;
await rm(WGPKG_DIR, { recursive: true });
} else {
throw new Error(`Unknown provider: ${provider}`);
default:
console.log("Unknown client provider specified, using latest wgc client");
case "wgc":
await downloadWGCClient();
break;
}
@@ -0,0 +1,57 @@
import { assertSecret } from "@blitzkit/core";
import { $ } from "bun";
import { rm } from "node:fs/promises";
import { DEPOT_DOWNLOADER_RETRY_TIME, dir, source } from ".";
import {
DEPOT_DOWNLOADER_EXECUTABLE,
DEPOT_DOWNLOADER_MAX_DOWNLOADS,
DEPOT_DOWNLOADER_OUTPUT,
} from "./constants";
import { QuickGitHubRelease } from "./types";
export async function downloadSteamClient() {
const [id, platform] = source.split("/");
console.log("Downloading Depot Downloader...");
const releases = await fetch(
"https://api.github.com/repos/SteamRE/DepotDownloader/releases/latest",
).then((response) => response.json() as Promise<QuickGitHubRelease>);
const asset = releases.assets.find((asset) =>
asset.name.includes("linux-x64"),
);
if (!asset) {
throw new Error("No linux-x64 asset found for Steam DepotDownloader");
}
await rm(DEPOT_DOWNLOADER_OUTPUT, { recursive: true, force: true });
await $`curl -L "${asset.browser_download_url}" -o "${DEPOT_DOWNLOADER_OUTPUT}.zip"`;
await $`unzip "${DEPOT_DOWNLOADER_OUTPUT}.zip" -d "${DEPOT_DOWNLOADER_OUTPUT}"`;
await $`chmod +x "${DEPOT_DOWNLOADER_EXECUTABLE}"`;
console.log("Downloading client...");
const username = assertSecret(import.meta.env.STEAM_USERNAME);
const password = assertSecret(import.meta.env.STEAM_PASSWORD);
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),
);
}
}
@@ -0,0 +1,62 @@
import locales from "@blitzkit/i18n/locales.json";
import { XMLParser } from "fast-xml-parser";
import {
WGC_CLIENT_GUID,
WGC_CLIENT_URL,
WGC_METADATA_PROTOCOL_VERSION,
WGC_PATCHES_CHAIN_PROTOCOL_VERSION,
} from "../../config/wgcClient";
import { downloadWgParts } from "../wgpkg";
import { ClientMetadata, PatchesChain } from "./types";
const parser = new XMLParser();
export async function downloadWGCClient() {
const metadataUrl = new URL("api/v1/metadata", WGC_CLIENT_URL);
metadataUrl.searchParams.set("guid", WGC_CLIENT_GUID);
metadataUrl.searchParams.set("chain_id", "unknown");
metadataUrl.searchParams.set(
"protocol_version",
WGC_METADATA_PROTOCOL_VERSION,
);
const metadata = await fetch(`${metadataUrl}`)
.then((response) => response.text())
.then((text) => parser.parse(text) as Promise<ClientMetadata>);
const patchesChainUrl = new URL("api/v1/patches_chain", WGC_CLIENT_URL);
patchesChainUrl.searchParams.set(
"protocol_version",
WGC_PATCHES_CHAIN_PROTOCOL_VERSION,
);
patchesChainUrl.searchParams.set("client_type", "full");
patchesChainUrl.searchParams.set("lang", locales.default);
patchesChainUrl.searchParams.set(
"metadata_version",
`${metadata.protocol.version}`,
);
patchesChainUrl.searchParams.set(
"metadata_protocol_version",
WGC_METADATA_PROTOCOL_VERSION,
);
patchesChainUrl.searchParams.set(
"chain_id",
metadata.protocol.predefined_section.chain_id,
);
patchesChainUrl.searchParams.set("game_installation", "true");
patchesChainUrl.searchParams.set("game_id", WGC_CLIENT_GUID);
patchesChainUrl.searchParams.set("client_current_version", "0");
const patchesChain = await fetch(`${patchesChainUrl}`)
.then((response) => response.text())
.then((text) => parser.parse(text) as Promise<PatchesChain>);
const urls = patchesChain.protocol.patches_chain.patch.files.file.map(
(file) =>
new URL(file.name, patchesChain.protocol.web_seeds.url).toString(),
);
await downloadWgParts(urls);
}
@@ -0,0 +1,87 @@
export interface ClientMetadata {
protocol: {
version: number;
predefined_section: PredefinedSection;
generated_section: GeneratedSection;
};
}
interface GeneratedSection {
launch_forbidden: string;
stop_supporting: string;
}
interface PredefinedSection {
app_id: string;
chain_id: string;
supported_languages: string;
default_language: string;
name: string;
fs_name: string;
shortcut_name: string;
executables: { executable: string };
keep_patches_interval: number;
client_types: { client_type: ClientType };
login_enabled: boolean;
realms_settings: RealmsSettings;
localization: Localization;
}
interface ClientType {
initial_app_type: string;
final_app_type: string;
min_supported_os: string;
client_parts: { client_part: string };
}
interface Localization {
item: { text: string[] };
}
interface RealmsSettings {
instances: { instance: Instance[] };
}
interface Instance {
game_id: string;
title_id: string;
platform_realm_id: string;
options: { option: string };
content_service_url: string;
}
export interface PatchesChain {
protocol: {
parts_info: { part: Part };
patches_chain: { patch: Patch };
delay_preload: string;
web_seeds: { url: string };
meta_need_update: string;
version_name: string;
};
}
interface Part {
name: string;
version: number;
status: string;
}
interface Patch {
files: { file: File[] };
torrent: Torrent;
part: string;
version_from: number;
version_to: string;
}
interface File {
name: string;
size: number;
unpacked_size: number;
}
interface Torrent {
hash: string;
urls: { url: string };
}
@@ -0,0 +1,49 @@
import { $ } from "bun";
import { mkdir, rm } from "node:fs/promises";
import { dir, source } from ".";
import { WGPKG_DIR } from "./constants";
export async function downloadWGPKGClient() {
let index = 1;
const urls: string[] = [];
console.log("Discovering packages...");
while (true) {
const url = `${source}.${index.toString().padStart(3, "0")}`;
const response = await fetch(url);
if (response.status === 200) {
urls.push(url);
} else if (response.status === 404) {
break;
} else {
throw new Error(`Wargaming response error status: ${response.status}`);
}
index++;
}
console.log(`Downloading ${urls.length} packages...`);
await downloadWgParts(urls);
}
export async function downloadWgParts(urls: string[]) {
await rm(WGPKG_DIR, { recursive: true, force: true });
await mkdir(WGPKG_DIR, { recursive: true });
const inputContent = urls
.map((url) => {
const name = url.split("/").pop()!;
return `${url}\n out=${name}\n dir=${WGPKG_DIR}`;
})
.join("\n");
await $`echo ${inputContent} | aria2c -j ${urls.length} -x 2 -s 2 -i -`;
const firstChunk = `${WGPKG_DIR}/${urls[0].split("/").pop()!}`;
await $`7z x "${firstChunk}" -o${dir} -y`;
await rm(WGPKG_DIR, { recursive: true });
}
@@ -0,0 +1,13 @@
import { assertSecret } from "@blitzkit/core";
import { $ } from "bun";
import { mkdir } from "node:fs/promises";
import { dir } from ".";
export async function downloadZipClient() {
const url = assertSecret(import.meta.env.CLIENT_SOURCE);
console.log("Downloading and extracting zipped client...");
await mkdir(dir, { recursive: true });
await $`curl -L "${url}" | bsdtar -xf- --strip-components=1 -C ${dir}`;
}
@@ -1,90 +0,0 @@
import { dvp } from "@blitzkit/closed";
import { assertSecret } from "@blitzkit/core";
import { mkdir, writeFile } from "fs/promises";
import { parse as parsePath } from "path";
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
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
.split(" ")[0]
.split(".")
.slice(0, 3)
.join(".");
console.log(`Installing patches for ${currentVersion}...`);
let patchIndex = 1;
while (true) {
const response = await fetch(
`${smartDlcUrl}/s${currentVersion}_${patchIndex}.yaml`,
);
console.log(patchIndex, response.status);
if (!response.ok) break;
console.log(`Applying patch ${patchIndex}...`);
const data = parseYaml(await response.text());
const dvpm = await fetch(`${smartDlcUrl}/${data.dx11}`).then((response) =>
response.arrayBuffer(),
);
const dvpd = await fetch(
`${smartDlcUrl}/${data.dx11.replace(".dvpm", ".dvpd")}`,
).then((response) => response.arrayBuffer());
const files = await dvp(dvpm, dvpd);
for (const { path, data } of files) {
console.log(`Patching "${path}"...`);
const { dir } = parsePath(path);
await mkdir(`${CLIENT_DIR}/Data/${dir}`, { recursive: true });
const buffer = Buffer.from(data);
await writeFile(`${CLIENT_DIR}/Data/${path}`, new Uint8Array(buffer));
}
console.log("patches done for patch");
if ("dynamicContentLocalizationsDir" in data) {
console.log(
`${smartDlcUrl}/${data.dynamicContentLocalizationsDir}/en.yaml`,
);
const localizationsResponse = await fetch(
`${smartDlcUrl}/${data.dynamicContentLocalizationsDir}/en.yaml`,
);
console.log("fetched");
const newStrings = parseYaml(await localizationsResponse.text());
console.log("got new strings");
const oldStrings =
await vfs.yaml<Record<string, string>>(`Data/Strings/en.yaml`);
console.log("got old strings");
const patchedStrings = { ...oldStrings, ...newStrings };
const patchedContent = stringifyYaml(patchedStrings);
console.log("stringified");
await mkdir(`${CLIENT_DIR}/Data/Strings`, { recursive: true });
console.log("mkdir");
await writeFile(
`${CLIENT_DIR}/Data/Strings/en.yaml`,
new Uint8Array(Buffer.from(patchedContent)),
);
console.log("file");
}
patchIndex++;
}
vfs.dispose();
process.exit(0);