Initial commit

This commit is contained in:
Brian Beck 2025-09-11 16:48:23 -07:00
commit 2211ed7650
10117 changed files with 735995 additions and 0 deletions

47
scripts/extract.ts Normal file
View file

@ -0,0 +1,47 @@
import fs from "node:fs/promises";
import unzipper from "unzipper";
import { normalize } from "@/src/stringUtils";
import manifest from "@/public/manifest.json";
import path from "node:path";
const inputBaseDir = "rawGameData/base";
const outputBaseDir = "public/base";
const archives = new Map<string, unzipper.CentralDirectory>();
async function buildExtractedGameDataFolder() {
await fs.mkdir(outputBaseDir, { recursive: true });
const filePaths = Object.keys(manifest).sort();
for (const filePath of filePaths) {
const sources = manifest[filePath];
for (const source of sources) {
if (source) {
let archive = archives.get(source);
if (!archive) {
const archivePath = `${inputBaseDir}/${source}`;
archive = await unzipper.Open.file(archivePath);
archives.set(source, archive);
}
const entry = archive.files.find(
(entry) => normalize(entry.path) === filePath
);
const inFile = `${inputBaseDir}/${source}:${filePath}`;
if (!entry) {
throw new Error(`File not found in archive: ${inFile}`);
}
const outFile = `${outputBaseDir}/@vl2/${source}/${filePath}`;
const outDir = path.dirname(outFile);
console.log(`${inFile} -> ${outFile}`);
await fs.mkdir(outDir, { recursive: true });
await fs.writeFile(outFile, entry.stream());
} else {
const inFile = `${inputBaseDir}/${filePath}`;
const outFile = `${outputBaseDir}/${filePath}`;
console.log(`${inFile} -> ${outFile}`);
await fs.cp(inFile, outFile);
}
}
}
}
buildExtractedGameDataFolder();

17
scripts/interior.ts Normal file
View file

@ -0,0 +1,17 @@
import fs from "node:fs";
import { inspect } from "node:util";
import { parseInteriorBuffer } from "@/src/interior";
const interiorFile = process.argv[2];
const interiorBuffer = fs.readFileSync(interiorFile);
const interiorArrayBuffer = interiorBuffer.buffer.slice(
interiorBuffer.byteOffset,
interiorBuffer.byteOffset + interiorBuffer.byteLength
);
console.log(
inspect(parseInteriorBuffer(interiorArrayBuffer), {
colors: true,
depth: Infinity,
})
);

86
scripts/manifest.ts Normal file
View file

@ -0,0 +1,86 @@
import fs from "node:fs/promises";
import path from "node:path";
import { parseArgs } from "node:util";
import unzipper from "unzipper";
import { normalize } from "@/src/stringUtils";
const archiveFilePattern = /\.vl2$/i;
const baseDir = "rawGameData/base";
function isArchive(name: string) {
return archiveFilePattern.test(name);
}
async function buildManifest() {
const fileSources = new Map<string, string[]>();
const looseFiles: string[] = [];
const archiveFiles: string[] = [];
for await (const entry of fs.glob(`${baseDir}/**/*`, {
withFileTypes: true,
})) {
if (entry.isFile()) {
const fullPath = normalize(`${entry.parentPath}/${entry.name}`);
if (isArchive(entry.name)) {
archiveFiles.push(fullPath);
} else {
looseFiles.push(fullPath);
}
}
}
for (const filePath of looseFiles) {
const relativePath = normalize(path.relative(baseDir, filePath));
fileSources.set(relativePath, [""]);
}
archiveFiles.sort();
for (const archivePath of archiveFiles) {
const relativePath = normalize(path.relative(baseDir, archivePath));
const archive = await unzipper.Open.file(archivePath);
for (const archiveEntry of archive.files) {
if (archiveEntry.type === "File") {
const filePath = normalize(archiveEntry.path);
const sources = fileSources.get(filePath) ?? [];
sources.push(relativePath);
fileSources.set(filePath, sources);
}
}
}
const manifest: Record<string, string[]> = {};
const orderedFiles = Array.from(fileSources.keys()).sort();
for (const filePath of orderedFiles) {
const sources = fileSources.get(filePath);
manifest[filePath] = sources;
console.log(
`${filePath}${sources[0] ? ` 📦 ${sources[0]}` : ""}${
sources.length > 1
? sources
.slice(1)
.map((source) => ` ❗️ ${source}`)
.join("")
: ""
}`
);
}
return manifest;
}
const { values } = parseArgs({
options: {
output: {
type: "string",
short: "o",
},
},
});
const manifest = await buildManifest();
if (values.output) {
await fs.writeFile(values.output, JSON.stringify(manifest), "utf8");
}

61
scripts/mission.ts Normal file
View file

@ -0,0 +1,61 @@
import fs from "node:fs";
import { inspect, parseArgs } from "node:util";
import { parseMissionScript } from "@/src/mission";
import { getFilePath } from "@/src/manifest";
async function run() {
const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
name: {
type: "string",
short: "n",
},
list: {
type: "boolean",
short: "l",
},
},
});
if (values.list) {
if (values.name || positionals[0]) {
console.error("Cannot specify --list (-l) with other options.");
return 1;
}
const manifest = (await import("../public/manifest.json")).default;
const fileNames = Object.keys(manifest);
console.log(
fileNames
.map((f) => f.match(/^missions\/(.+)\.mis$/))
.filter(Boolean)
.map((match) => match[1])
.join("\n")
);
return;
} else if (
(values.name && positionals[0]) ||
(!values.name && !positionals[0])
) {
console.error(
"Must specify exactly one of --name (-n) or a positional filename."
);
return 1;
}
let missionFile = positionals[0];
if (values.name) {
const resourcePath = `missions/${values.name}.mis`;
missionFile = getFilePath(resourcePath);
}
const missionScript = fs.readFileSync(missionFile, "utf8");
console.log(
inspect(parseMissionScript(missionScript), {
colors: false,
depth: Infinity,
})
);
}
const code = await run();
process.exit(code ?? 0);

66
scripts/terrain.ts Normal file
View file

@ -0,0 +1,66 @@
import fs from "node:fs";
import { inspect, parseArgs } from "node:util";
import { parseTerrainBuffer } from "@/src/terrain";
import { getFilePath } from "@/src/manifest";
async function run() {
const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
name: {
type: "string",
short: "n",
},
list: {
type: "boolean",
short: "l",
},
},
});
if (values.list) {
if (values.name || positionals[0]) {
console.error("Cannot specify --list (-l) with other options.");
return 1;
}
const manifest = (await import("../public/manifest.json")).default;
const fileNames = Object.keys(manifest);
console.log(
fileNames
.map((f) => f.match(/^terrains\/(.+)\.ter$/))
.filter(Boolean)
.map((match) => match[1])
.join("\n")
);
return;
} else if (
(values.name && positionals[0]) ||
(!values.name && !positionals[0])
) {
console.error(
"Must specify exactly one of --name (-n) or a positional filename."
);
return 1;
}
let terrainFile = positionals[0];
if (values.name) {
const resourcePath = `terrains/${values.name}.ter`;
terrainFile = getFilePath(resourcePath);
}
const terrainBuffer = fs.readFileSync(terrainFile);
const terrainArrayBuffer = terrainBuffer.buffer.slice(
terrainBuffer.byteOffset,
terrainBuffer.byteOffset + terrainBuffer.byteLength
);
console.log(
inspect(parseTerrainBuffer(terrainArrayBuffer), {
colors: true,
depth: Infinity,
})
);
}
const code = await run();
process.exit(code ?? 0);