init
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
// About me: Bundles the extension into dist/ as the folder layout Vortex loads:
|
||||
// a single CommonJS index.js plus info.json and the game artwork. `vortex-api` is
|
||||
// left external because Vortex provides it to extensions at runtime.
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import * as esbuild from 'esbuild';
|
||||
|
||||
import { encodePng, renderGameArt } from '../src/gameart.ts';
|
||||
|
||||
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const distDir = resolve(projectRoot, 'dist');
|
||||
const pkg = JSON.parse(readFileSync(resolve(projectRoot, 'package.json'), 'utf8'));
|
||||
|
||||
mkdirSync(distDir, { recursive: true });
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: [resolve(projectRoot, 'src/index.ts')],
|
||||
outfile: resolve(distDir, 'index.js'),
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
target: 'node18',
|
||||
external: ['vortex-api'],
|
||||
legalComments: 'none',
|
||||
});
|
||||
|
||||
// The bundle is CommonJS, but this repo is an ESM package; without this marker
|
||||
// node would read dist/index.js as ESM and fail on `module.exports`.
|
||||
writeFileSync(resolve(distDir, 'package.json'), `${JSON.stringify({ type: 'commonjs' }, null, 2)}\n`);
|
||||
|
||||
const info = {
|
||||
name: 'Game: Cyberpunk 2077 (Linux/Proton)',
|
||||
author: 'local',
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
gameId: 'cyberpunk2077linux',
|
||||
};
|
||||
writeFileSync(resolve(distDir, 'info.json'), `${JSON.stringify(info, null, 2)}\n`);
|
||||
|
||||
// The tile artwork is generated, never copied from Vortex or Nexus, so the build
|
||||
// renders it straight from source. It is deterministic, so this is reproducible.
|
||||
writeFileSync(resolve(distDir, 'gameart.png'), encodePng(renderGameArt()));
|
||||
|
||||
console.log(`built ${resolve(distDir, 'index.js')}`);
|
||||
@@ -0,0 +1,19 @@
|
||||
// About me: Writes assets/gameart.png from the generated artwork in src/gameart.ts.
|
||||
// Kept as a separate step from the build so the committed image only changes when
|
||||
// someone deliberately regenerates it.
|
||||
//
|
||||
// Usage: node scripts/make-gameart.mjs [outputPath]
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { encodePng, renderGameArt } from '../src/gameart.ts';
|
||||
|
||||
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const output = resolve(projectRoot, process.argv[2] ?? 'assets/gameart.png');
|
||||
|
||||
mkdirSync(dirname(output), { recursive: true });
|
||||
const png = encodePng(renderGameArt());
|
||||
writeFileSync(output, png);
|
||||
|
||||
console.log(`wrote ${output} (${png.length} bytes)`);
|
||||
@@ -0,0 +1,118 @@
|
||||
// About me: Writes the missing Steam libraryfolders.vdf into a Wine prefix from
|
||||
// the Linux side, before Vortex is ever started. Vortex core scans Steam at
|
||||
// startup and reads C:\Program Files (x86)\Steam\config\libraryfolders.vdf; when
|
||||
// SteamTinkerLaunch has not created it, the unhandled ENOENT shows up as
|
||||
// "unrecoverable error" and Vortex dies. Running this once repairs the prefix.
|
||||
//
|
||||
// Usage: node scripts/repair-prefix.mjs [--prefix <pfx>] [--force] [--dry-run]
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { findSteamLibraries, findSteamRoots } from '../src/discovery.ts';
|
||||
import {
|
||||
PREFIX_STEAM_DIR,
|
||||
buildShimVdf,
|
||||
dedupeByCanonicalPath,
|
||||
ensureSteamShim,
|
||||
} from '../src/steamShim.ts';
|
||||
|
||||
const DEFAULT_PREFIX = resolve(homedir(), '.config/steamtinkerlaunch/vortex/compatdata/pfx');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { prefix: DEFAULT_PREFIX, force: false, dryRun: false };
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
if (argv[i] === '--prefix') {
|
||||
args.prefix = resolve(argv[i + 1] ?? '');
|
||||
i += 1;
|
||||
} else if (argv[i] === '--force') {
|
||||
args.force = true;
|
||||
} else if (argv[i] === '--dry-run') {
|
||||
args.dryRun = true;
|
||||
} else {
|
||||
throw new Error(`unknown argument: ${argv[i]}`);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the paths Vortex would see inside the prefix to real Linux paths:
|
||||
* Z: is the Linux root, C: is the prefix's drive_c.
|
||||
*/
|
||||
function toRealPath(prefix, winPath) {
|
||||
const normalised = winPath.replace(/\\/g, '/');
|
||||
if (/^[zZ]:/.test(normalised)) {
|
||||
return normalised.slice(2) || '/';
|
||||
}
|
||||
if (/^[cC]:/.test(normalised)) {
|
||||
return resolve(prefix, 'drive_c', normalised.slice(3));
|
||||
}
|
||||
return normalised;
|
||||
}
|
||||
|
||||
function prefixFileAccess(prefix) {
|
||||
return {
|
||||
exists: (p) => existsSync(toRealPath(prefix, p)),
|
||||
readFile: (p) => readFileSync(toRealPath(prefix, p), 'utf8'),
|
||||
readDir: (p) => {
|
||||
try {
|
||||
return readdirSync(toRealPath(prefix, p));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!existsSync(args.prefix)) {
|
||||
console.error(`prefix not found: ${args.prefix}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = prefixFileAccess(args.prefix);
|
||||
|
||||
// Only the Wine-side (Z:) view is useful here: the shim has to contain paths
|
||||
// Vortex can open from inside the prefix.
|
||||
const libraries = dedupeByCanonicalPath(
|
||||
[
|
||||
...new Set(
|
||||
findSteamRoots(files)
|
||||
.filter((root) => root.startsWith('Z:'))
|
||||
.flatMap((root) => findSteamLibraries(files, root)),
|
||||
),
|
||||
],
|
||||
(candidate) => realpathSync(toRealPath(args.prefix, candidate)),
|
||||
);
|
||||
|
||||
console.log(`prefix: ${args.prefix}`);
|
||||
console.log(`steam dir: ${toRealPath(args.prefix, PREFIX_STEAM_DIR)}`);
|
||||
console.log(`libraries: ${libraries.length === 0 ? '(none found)' : ''}`);
|
||||
for (const library of libraries) {
|
||||
console.log(` ${library}`);
|
||||
}
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log('--- shim that would be written ---');
|
||||
console.log(buildShimVdf(libraries));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const writer = {
|
||||
makeDir: (p) => mkdirSync(toRealPath(args.prefix, p), { recursive: true }),
|
||||
writeFile: (p, contents) => writeFileSync(toRealPath(args.prefix, p), contents, 'utf8'),
|
||||
};
|
||||
|
||||
const result = ensureSteamShim(files, writer, libraries, { overwrite: args.force });
|
||||
console.log(`result: ${result.action} -> ${toRealPath(args.prefix, result.path)}`);
|
||||
|
||||
if (result.action === 'skipped') {
|
||||
console.error(
|
||||
libraries.length === 0
|
||||
? 'no Steam libraries found under /home; pass --prefix for the right prefix or check your Steam install'
|
||||
: `no Steam directory at ${toRealPath(args.prefix, PREFIX_STEAM_DIR)}; is this the Vortex prefix?`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// About me: Loads the built bundle the way Vortex does - require() with a stubbed
|
||||
// `vortex-api` - and drives registerGame/registerInstaller against a fake context.
|
||||
// This catches packaging mistakes (bad export shape, missing external) that unit
|
||||
// tests on the source cannot see. Run with: node scripts/smoke-load.cjs
|
||||
const Module = require('node:module');
|
||||
const path = require('node:path');
|
||||
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function patchedLoad(request, parent, isMain) {
|
||||
if (request === 'vortex-api') {
|
||||
return { log: (level, message) => console.log(` [vortex-api ${level}] ${message}`) };
|
||||
}
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
|
||||
const extension = require(path.resolve(__dirname, '../dist/index.js'));
|
||||
const main = extension.default ?? extension;
|
||||
|
||||
const registered = { games: [], installers: [] };
|
||||
const context = {
|
||||
registerGame: (game) => registered.games.push(game),
|
||||
registerInstaller: (id, priority, testSupported, install) =>
|
||||
registered.installers.push({ id, priority, testSupported, install }),
|
||||
once: (cb) => cb(),
|
||||
api: {},
|
||||
};
|
||||
|
||||
const result = main(context);
|
||||
console.log('main() returned:', result);
|
||||
|
||||
const game = registered.games[0];
|
||||
console.log('game id:', game.id);
|
||||
console.log('executable:', game.executable());
|
||||
console.log('queryModPath:', JSON.stringify(game.queryModPath('')));
|
||||
console.log('queryPath():', game.queryPath());
|
||||
|
||||
const installer = registered.installers[0];
|
||||
installer
|
||||
.testSupported(['a.archive'], game.id)
|
||||
.then((supported) => {
|
||||
console.log('testSupported:', JSON.stringify(supported));
|
||||
return installer.install(
|
||||
['Cool Mod v1\\archive\\pc\\mod\\cool.archive', 'Cool Mod v1\\readme.txt'],
|
||||
'Z:\\staging\\Cool Mod-1234.installing',
|
||||
game.id,
|
||||
);
|
||||
})
|
||||
.then((res) => {
|
||||
console.log('install instructions:', JSON.stringify(res.instructions, null, 2));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('smoke failed:', err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user