feat: implement all methods

This commit is contained in:
TrèsAbhi committed 2025-11-29 15:16:09 -06:00
1 parent f33a73e1bb
commit 9ffc040723
5 files changed
+134 -102

No files matched your search

+26 -9
View File
@@ -1,12 +1,29 @@
import { assertSecret } from "@blitzkit/core";
import { SteamVFS } from "../core/vfs/vfs";
import { AbstractVFS } from "../core/vfs/abstract";
import { SteamVFS } from "../core/vfs/steam";
import { ZipVFS } from "../core/vfs/zip";
const APP = 444200;
const DEPOT = 444202;
const source = Bun.argv
.find((arg) => arg.startsWith("--source="))
?.split("=")[1];
export const vfs = await new SteamVFS(
assertSecret(import.meta.env.STEAM_USERNAME),
assertSecret(import.meta.env.STEAM_PASSWORD),
APP,
DEPOT
).init();
let resolved: AbstractVFS;
if (source === "steam") {
const APP = 444200;
const DEPOT = 444202;
resolved = await new SteamVFS(
assertSecret(import.meta.env.STEAM_USERNAME),
assertSecret(import.meta.env.STEAM_PASSWORD),
APP,
DEPOT
).init();
} else if (source === "zip") {
const url = assertSecret(import.meta.env.ZIP_URL);
resolved = await new ZipVFS(url).init();
} else {
throw new Error("No valid source specified");
}
export const vfs = resolved;
+84 -1
View File
@@ -1,3 +1,86 @@
import { readDVPL } from "@blitzkit/core";
import { XMLParser } from "fast-xml-parser";
import { normalize } from "path/posix";
import { parse } from "yaml";
export abstract class AbstractVFS {
abstract init(): Promise<AbstractVFS>;
textDecoder = new TextDecoder();
private xmlParser = new XMLParser();
abstract init(): Promise<typeof this>;
abstract dispose(): void;
abstract has(path: string): boolean;
abstract raw(path: string): Promise<Uint8Array>;
abstract paths(): string[];
resolve(path: string) {
const normalized = normalize(path);
if (this.has(normalized)) return normalized;
const dvplPath = `${path}.dvpl`;
if (this.has(dvplPath)) return dvplPath;
return null;
}
assert(path: string) {
const resolved = this.resolve(path);
if (!resolved) throw new Error(`File not found: ${path}`);
return resolved;
}
async file(file: string) {
const resolved = 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;
}
dir(path: string) {
const parentSegments = path.split("/").length;
const children: string[] = [];
for (const [child] of this.paths()) {
const childSegments = child.split("/");
if (
child.startsWith(path) &&
parentSegments + 1 === childSegments.length
) {
children.push(childSegments.at(-1)!);
}
}
return children;
}
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 parse(file) as Type;
}
async xml<Type>(path: string) {
const file = await this.text(path);
return this.xmlParser.parse(file) as Type;
}
[Symbol.dispose]() {
this.dispose();
}
}
+5 -65
View File
@@ -1,8 +1,4 @@
import { readDVPL } from "@blitzkit/core/src/blitz";
import { XMLParser } from "fast-xml-parser";
import { normalize } from "path/posix";
import SteamUser, { EConnectionProtocol } from "steam-user";
import { parse } from "yaml";
import { AbstractVFS } from "./abstract";
interface SteamManifestFile {
@@ -36,9 +32,6 @@ interface SteamManifest {
export class SteamVFS extends AbstractVFS {
private steam = new SteamUser({ protocol: EConnectionProtocol.TCP });
private textDecoder = new TextDecoder();
private xmlParser = new XMLParser();
private manifest: Map<string, SteamManifestFile> = new Map();
constructor(
@@ -74,10 +67,8 @@ export class SteamVFS extends AbstractVFS {
);
});
const paths: string[] = [];
for (const file of manifest.files) {
const path = file.filename.replaceAll("\\", "/");
paths.push(path);
this.manifest.set(path, file);
}
@@ -85,74 +76,23 @@ export class SteamVFS extends AbstractVFS {
}
has(path: string) {
const normalized = normalize(path);
const dvplPath = `${normalized}.dvpl`;
return (
(this.manifest.has(dvplPath) && dvplPath) ||
(this.manifest.has(normalized) && normalized)
);
return this.manifest.has(path);
}
assert(requested: string) {
const path = this.has(requested);
if (!path) throw new Error(`File not found: ${requested}`);
return path;
}
dir(path: string) {
const parentSegments = path.split("/").length;
const children: string[] = [];
for (const child of this.manifest.keys()) {
const childSegments = child.split("/");
if (
child.startsWith(path) &&
parentSegments + 1 === childSegments.length
) {
children.push(childSegments.at(-1)!);
}
}
return children;
}
async file(requested: string) {
const path = this.assert(requested);
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);
let buffer = downloaded.file;
if (path.endsWith(".dvpl")) buffer = readDVPL(downloaded.file);
return new Uint8Array(buffer);
return new Uint8Array(downloaded.file);
}
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 parse(file) as Type;
}
async xml<Type>(path: string) {
const file = await this.text(path);
return this.xmlParser.parse(file) as Type;
paths() {
return Array.from(this.manifest.keys());
}
dispose() {
this.steam.logOff();
}
[Symbol.dispose]() {
this.dispose();
}
}
+19 -2
View File
@@ -1,8 +1,9 @@
import { normalize } from "path/posix";
import { HTTPRangeReader, unzip, ZipEntry } from "unzipit";
import { AbstractVFS } from "./abstract";
export class ZipVFS extends AbstractVFS {
entries: Record<string, ZipEntry> = {};
entries = new Map<string, ZipEntry>();
constructor(private url: string, private root = "app/") {
super();
@@ -13,9 +14,25 @@ export class ZipVFS extends AbstractVFS {
const { entries } = await unzip(reader);
for (const entry of Object.values(entries)) {
this.entries[entry.name.slice(this.root.length)] = entry;
const normalized = normalize(entry.name.slice(this.root.length));
this.entries.set(normalized, entry);
}
return this;
}
has(path: string) {
return path in this.entries;
}
async raw(path: string) {
const arrayBuffer = await this.entries.get(path)!.arrayBuffer();
return new Uint8Array(arrayBuffer);
}
paths() {
return Array.from(this.entries.keys());
}
dispose() {}
}