infrastructure for inline vfs and clients

This commit is contained in:
tresabhi committed 2026-07-31 20:48:33 -05:00
1 parent d81d885767
commit 39745dd475
19 files changed
+133 -64

No files matched your search

@@ -11,7 +11,6 @@ import {
Equalizer,
EquipmentDefinitions,
EquipmentSlot,
GameDefinitions,
GunDefinition,
I18nString,
MapDefinitions,
@@ -491,20 +490,6 @@ export interface MapAlly {
points: Array<number[]>;
}
interface AvailableNationsYaml {
available_nations: string[];
}
export interface SquadBattleTypeStyles {
Prototypes: {
components: {
UIDataLocalBindingsComponent: {
data: [string, string, string][];
};
};
}[];
}
type CombatRolesYaml = Record<
string,
{
@@ -548,14 +533,7 @@ export async function definitions() {
console.log("Building definitions...");
const uploader = new AssetUploader("definitions");
const gameDefinitions: GameDefinitions = {
version: (await vfs.text("Data/version.txt")).split(" ")[0],
nations: (
await vfs.yaml<AvailableNationsYaml>("Data/available_nations.yaml")
).available_nations,
gameModes: {},
roles: {},
};
const tankDefinitions: TankDefinitions = { tanks: {} };
const camouflageDefinitions: CamouflageDefinitions = { camouflages: {} };
const modelDefinitions: ModelDefinitions = { models: {} };
@@ -673,10 +651,10 @@ export async function definitions() {
`Data/camouflages.yaml`,
);
const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages);
const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStyles>(
const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStylesYaml>(
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`,
);
const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStyles>(
const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStylesYaml>(
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`,
);
const gameModeNativeNames: Record<string, number> = {};
@@ -708,12 +686,8 @@ export async function definitions() {
/"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g,
)) {
const id = Number(match[1]);
const name = getString(match[2]);
gameModeNativeNames[match[3]] = id;
gameDefinitions.gameModes[id] = {
name,
};
}
for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
@@ -1767,22 +1741,6 @@ export async function definitions() {
}
});
Object.entries(combatRoles).forEach(([, value]) => {
gameDefinitions.roles[value.id] = { provisions: [], consumables: [] };
value.default_abilities.forEach((ability) => {
if (ability in consumableNativeNames) {
gameDefinitions.roles[value.id].consumables.push(
consumableNativeNames[ability],
);
} else if (ability in provisionNativeNames) {
gameDefinitions.roles[value.id].provisions.push(
provisionNativeNames[ability],
);
} else throw new Error(`Unknown ability ${ability}`);
});
});
Object.entries(avatar.root.skillsByClasses).forEach(([tankClass, skills]) => {
skillDefinitions.classes[
blitzTankClassToBlitzkit[tankClass as BlitzTankClass]
@@ -1798,10 +1756,6 @@ export async function definitions() {
};
});
await uploader.add({
content: GameDefinitions.encode(gameDefinitions).finish(),
path: "definitions/game.pb",
});
await uploader.add({
content: TankDefinitions.encode(tankDefinitions).finish(),
path: "definitions/tanks.pb",
@@ -1,16 +1,16 @@
import sharp from "sharp";
import { AssetUploader } from "../core/github/assetUploader";
import { vfs } from "./constants";
import { SquadBattleTypeStyles } from "./definitions";
import { SquadBattleTypeStylesYaml } from "./definitions";
export async function gameModeBanners() {
console.log("Building game mode banners...");
using uploader = new AssetUploader("game mode banners");
const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStyles>(
const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStylesYaml>(
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`,
);
const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStyles>(
const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStylesYaml>(
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`,
);
const bannerMatches: { name: string; path: string }[] = [];
-79
View File
@@ -1,79 +0,0 @@
import { readDVPL } from "@blitzkit/core";
import { Options, parse as parseCSV } from "csv-parse/sync";
import { XMLParser } from "fast-xml-parser";
import { normalize } from "path/posix";
import { parse as parseYaml } from "yaml";
export abstract class AbstractVFS {
textDecoder = new TextDecoder();
private xmlParser = new XMLParser();
abstract init(): Promise<typeof this>;
abstract dispose(): void;
abstract has(path: string): Promise<boolean>;
abstract raw(path: string): Promise<Uint8Array>;
async resolve(path: string) {
const normalized = normalize(path);
if (await this.has(normalized)) return normalized;
const dvplPath = `${normalized}.dvpl`;
if (await this.has(dvplPath)) return dvplPath;
return null;
}
async assert(path: string) {
const resolved = await this.resolve(path);
if (resolved === null) throw new Error(`File not found: ${path}`);
return resolved;
}
async file(file: string) {
const resolved = await this.assert(file);
const raw = await this.raw(resolved);
let buffer = raw;
if (resolved.endsWith(".dvpl")) {
buffer = new Uint8Array(readDVPL(Buffer.from(raw)));
}
return buffer;
}
abstract dir(path: string): Promise<string[]>;
async text(path: string) {
const file = await this.file(path);
return this.textDecoder.decode(file);
}
async yaml<Type>(path: string) {
const file = await this.text(path);
return parseYaml(file) as Type;
}
async xml<Type>(path: string) {
const file = await this.text(path);
return this.xmlParser.parse(file) as Type;
}
async _csv(text: string, options: Options = {}) {
const parsed = parseCSV(text, options);
return parsed;
}
async csv(path: string, options: Options = {}) {
const text = await this.text(path);
return await this._csv(text, options);
}
[Symbol.dispose]() {
this.dispose();
}
}
-35
View File
@@ -1,35 +0,0 @@
import { readdir } from "node:fs/promises";
import { AbstractVFS } from "./abstract";
export class LocalVFS extends AbstractVFS {
constructor(private base: string) {
super();
}
async init() {
return this;
}
async has(path: string) {
const file = Bun.file(`${this.base}/${path}`);
return await file.exists();
}
async raw(path: string) {
const file = Bun.file(`${this.base}/${path}`);
const buffer = await file.arrayBuffer();
const array = new Uint8Array(buffer);
return array;
}
async dir(path: string) {
try {
return await readdir(`${this.base}/${path}`);
} catch (_) {
return [];
}
}
dispose() {}
}
-49
View File
@@ -1,49 +0,0 @@
import { AbstractVFS } from "./abstract";
export class MixedVFS extends AbstractVFS {
constructor(private vfses: AbstractVFS[]) {
super();
}
async init() {
for (const vfs of this.vfses) {
await vfs.init();
}
return this;
}
dispose() {
for (const vfs of this.vfses) {
vfs.dispose();
}
}
async has(path: string) {
for (const vfs of this.vfses) {
if (await vfs.has(path)) return true;
}
return false;
}
async raw(path: string) {
for (const vfs of this.vfses) {
if (await vfs.has(path)) return await vfs.raw(path);
}
throw new Error(`File not found: ${path}`);
}
async dir(path: string) {
const files = new Set<string>();
for (const vfs of this.vfses) {
for (const file of await vfs.dir(path)) {
files.add(file);
}
}
return Array.from(files);
}
}
-111
View File
@@ -1,111 +0,0 @@
import SteamUser, { EConnectionProtocol } from "steam-user";
import { AbstractVFS } from "./abstract";
interface SteamManifestFile {
chunks: {
sha: string;
crc: number;
offset: string;
cb_original: number;
cb_compressed: number;
}[];
filename: string;
size: string;
flags: number;
sha_filename: string;
sha_content: string;
linktarget: unknown;
}
interface SteamManifest {
files: SteamManifestFile[];
depot_id: number;
gid_manifest: string;
creation_time: number;
filenames_encrypted: boolean;
cb_disk_original: string;
cb_disk_compressed: string;
unique_chunks: number;
crc_encrypted: number;
crc_clear: number;
}
export class SteamVFS extends AbstractVFS {
private steam = new SteamUser({ protocol: EConnectionProtocol.TCP });
private manifest: Map<string, SteamManifestFile> = new Map();
constructor(
private username: string,
private password: string,
private app: number,
private depot: number,
) {
super();
}
async init() {
this.steam.logOn({
accountName: this.username,
password: this.password,
});
await new Promise((resolve) => this.steam.once("loggedOn", resolve));
const productInfo = await this.steam.getProductInfo([this.app], []);
const manifest: SteamManifest = await new Promise((resolve) => {
// @ts-expect-error
this.steam.getManifest(
this.app,
this.depot,
// @ts-expect-error
productInfo.apps[this.app].appinfo.depots[this.depot].manifests.public
.gid,
"public",
// @ts-expect-error
(_, response) => {
resolve(response);
},
);
});
for (const file of manifest.files) {
const path = file.filename.replaceAll("\\", "/");
this.manifest.set(path, file);
}
return this;
}
async has(path: string) {
return this.manifest.has(path);
}
async raw(path: string) {
const fileManifest = this.manifest.get(path)!;
const downloaded: { type: "complete"; file: Buffer } =
// @ts-expect-error
await this.steam.downloadFile(this.app, this.depot, fileManifest);
return new Uint8Array(downloaded.file);
}
async dir(path: string) {
const parentSegments = path.split("/").length;
const children = new Set<string>();
for (const child of this.manifest.keys()) {
if (!child.startsWith(path) || child === path) continue;
const childSegments = child.split("/");
const nextSegment = childSegments[parentSegments];
children.add(nextSegment);
}
return Array.from(children);
}
dispose() {
this.steam.logOff();
}
}
-53
View File
@@ -1,53 +0,0 @@
import { normalize } from "path/posix";
import { HTTPRangeReader, unzip, ZipEntry } from "unzipit";
import { AbstractVFS } from "./abstract";
export class ZipVFS extends AbstractVFS {
entries = new Map<string, ZipEntry>();
constructor(
private url: string,
private root = "app/",
) {
super();
}
async init() {
const reader = new HTTPRangeReader(this.url);
const { entries } = await unzip(reader);
for (const entry of Object.values(entries)) {
const normalized = normalize(entry.name.slice(this.root.length));
this.entries.set(normalized, entry);
}
return this;
}
async has(path: string) {
return this.entries.has(path);
}
async raw(path: string) {
const arrayBuffer = await this.entries.get(path)!.arrayBuffer();
return new Uint8Array(arrayBuffer);
}
async dir(path: string) {
const parentSegments = path.split("/").length;
const children = new Set<string>();
for (const child of this.entries.keys()) {
if (!child.startsWith(path) || child === path) continue;
const childSegments = child.split("/");
const nextSegment = childSegments[parentSegments];
children.add(nextSegment);
}
return Array.from(children);
}
dispose() {}
}