mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-10 11:16:10 +01:00
Merge branch 'main' into data-driven
This commit is contained in:
44 files changed
+1337
-358
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user