@bscotch/stitch-launcher
Version:
Manage GameMaker IDE and runtime installations for fast switching between versions.
281 lines • 13.2 kB
JavaScript
var GameMakerIde_1;
import { __decorate, __metadata } from "tslib";
import { Pathy } from '@bscotch/pathy';
import { ok } from 'assert';
import { exec } from 'child_process';
import { GameMakerComponent } from './GameMakerComponent.js';
import { cleanVersionString, createStaticTracer, download, listDefaultMacrosPaths, listInstalledIdes, listInstalledRuntimes, runIdeInstaller, setActiveRuntime, trace, } from './utility.js';
import { logger } from './log.js';
export class GameMakerRunningIde {
exePath;
projectYypPath;
runtimeVersion;
process;
constructor(exePath, projectYypPath, runtimeVersion) {
this.exePath = exePath;
this.projectYypPath = projectYypPath;
this.runtimeVersion = runtimeVersion;
this.process = exec(`"${exePath}" "${projectYypPath}"`);
}
waitForClose(cb) {
return new Promise((resolve) => {
const events = ['exit', 'close', 'disconnect', 'error'];
const callback = () => {
events.forEach((event) => this.process.off(event, callback));
return resolve(cb?.());
};
events.forEach((event) => this.process.on(event, callback));
});
}
close() {
this.process.kill();
}
}
const ideClassStaticTracer = (methodName) => createStaticTracer('GameMakerIde', methodName);
export class GameMakerIdeError extends Error {
code;
constructor(message, code) {
super(message);
this.code = code;
this.name = 'GameMakerIdeError';
Error.captureStackTrace(this, this.constructor);
}
}
export function assert(claim, message = 'Assertion failed', code = 'UNKNOWN') {
if (!claim) {
throw new GameMakerIdeError(message, code);
}
}
let GameMakerIde = class GameMakerIde extends GameMakerComponent {
static { GameMakerIde_1 = this; }
static error = GameMakerIdeError;
constructor(info) {
super(info);
}
async openProject(projectYypPath, runtimeVersion, options) {
const projectPath = Pathy.asInstance(projectYypPath);
const user = await this.activeUser();
assert(user, 'No active user', 'LOGIN_REQUIRED');
const pairedRuntimeVersion = await this.pairedRuntimeVersion();
runtimeVersion ||= pairedRuntimeVersion;
assert(runtimeVersion, `Could not find a runtime version for IDE v${this.version}, and none was specified.`, 'RUNTIME_NOT_FOUND');
// Assert that this runtime version is in the feed
const release = await GameMakerComponent.findRelease({ runtimeVersion });
assert(release, `Could not find runtime version ${runtimeVersion}`, 'RUNTIME_NOT_FOUND');
// If the runtime version is the paired one,
// and it is not already installed, then the IDE
// will install on boot and set it to active.
// Therefore we can either do NOTHING, or just
// need to unset the runtime.json>active field.
const installedRuntime = (await listInstalledRuntimes()).find((r) => r.version === runtimeVersion);
if (installedRuntime) {
await setActiveRuntime(installedRuntime);
}
assert(installedRuntime || runtimeVersion === pairedRuntimeVersion, `The specified Runtime was neither already installed nor a match for the specified IDE.`, 'RUNTIME_NOT_FOUND');
// Open the project with the given IDE
console.log(`Opening project "${projectYypPath}" using IDE v${this.version} with Runtime v${runtimeVersion}.\n\nThe window might take a few seconds to appear, and might not steal focus!\n\nThis command will exit when the IDE is closed.`);
// Prevent the IDE from annoying the user with
// suggestions to update.
if (options?.disableUpdatePrompt) {
await GameMakerIde_1.disableUpdatePrompt();
}
await GameMakerComponent.ensureOfficialRuntimeFeeds();
return new GameMakerRunningIde(this.executablePath.absolute, projectPath.toString({ format: 'win32' }), runtimeVersion);
}
/**
* Each GameMaker IDE lists the runtime version
* that it is paired with. In theory this is the
* most-compatible runtime version.
*/
async pairedRuntimeVersion() {
const runtimeVersionFile = this.directory.join('matching.runtime');
// Get the value specified in the actual install (this is the most-correct one)
if (await runtimeVersionFile.exists()) {
const version = (await runtimeVersionFile.read()).trim();
ok(version.match(/^\d+\.\d+\.\d+\.\d+$/), `Invalid runtime version: ${version}`);
return version;
}
// If that fails, fall back on the version in the feed
const release = await GameMakerComponent.findRelease({
ideVersion: this.version,
});
assert(release, `Could not find release for IDE v${this.version}`);
return release?.runtime.version;
}
/**
* Install the specified IDE version. Only
* one IDE version can be installed at a time
* (per stable and beta channels), so this
* may clobber the currently-installed IDE.
*
* If this version is already installed, no action
* is taken. If this version's installer is already
* downloaded, it will not be re-downloaded. If it
* is *not* downloaded, then it will be downloaded.
*/
static async install(version, options) {
version = cleanVersionString(version);
const release = await GameMakerComponent.findRelease({
ideVersion: version,
});
ok(release, `Could not find version ${version} in the IDE feed`);
ok(release.ide.link, `Could not find a download link for version ${version}`);
let installedVersion = await GameMakerIde_1.findInstalled(version);
if (!installedVersion || options?.force) {
// See if it's installed to PROGRAMFILES,
// just not yet to Stitch.
let directlyInstalled = await GameMakerIde_1.findDirectlyInstalled(version, options?.programFiles);
if (!directlyInstalled || options?.force) {
// Download & install!
const installerPath = GameMakerIde_1.cachedIdeInstallerPath(version);
await download(release.ide.link, installerPath);
await runIdeInstaller(installerPath);
// Make sure this version is now installed
directlyInstalled = await GameMakerIde_1.findDirectlyInstalled(version, options?.programFiles);
ok(directlyInstalled, `Could not find version ${version} after installation. Installation might have gone to an unexpected location or the installer might have failed.`);
await installerPath.delete();
}
// Copy over to Stitch
console.log("Copying installed files to Stitch's cache...");
await directlyInstalled.directory.copy(GameMakerIde_1.cachedIdeDirectory(version));
installedVersion = await GameMakerIde_1.findInstalled(version);
}
ok(installedVersion, `Could not find version ${version} after installation.`);
return installedVersion;
}
static async findInstalled(version) {
const installedIdeVersions = await GameMakerIde_1.listInstalled();
return installedIdeVersions.find((v) => v.version === version);
}
static async listInstalled() {
await GameMakerIde_1.defaultCachedIdeParentDirectory.ensureDirectory();
return await GameMakerIde_1.listInstalledInDir(GameMakerIde_1.defaultCachedIdeParentDirectory);
}
/**
* Prevent the IDE from showing an update prompt on boot.
*/
static async disableUpdatePrompt() {
const macroFilePaths = await listDefaultMacrosPaths();
for (const path of macroFilePaths) {
const macros = (await path.exists())
? await path.read()
: {};
const backupPath = path.changeExtension('.bk.json');
if (!(await backupPath.exists())) {
console.log('COPYING TO', backupPath.absolute);
await path.copy(backupPath);
}
// Set it to a syntactically correct, but
// non-existent RSS feed.
macros.updateURI =
'http://gms.yoyogames.com/update-win-NuBeta-TOTALLY-FAKE.rss';
await path.write(macros);
}
}
static async findDirectlyInstalled(version, programFiles) {
const installedIdeVersions = await GameMakerIde_1.listDirectlyInstalled(programFiles);
return installedIdeVersions.find((v) => v.version === version);
}
/**
* Check PROGRAMFILES for installed IDE versions. These are the result of running
* the GameMaker IDE installers. The installed
* content end up being separately cached by
* Stitch to allow parallel installs
* (see {@link listInstalled}).
*/
static async listDirectlyInstalled(programFiles = process.env.PROGRAMFILES) {
return await GameMakerIde_1.listInstalledInDir(programFiles);
}
static async listInstalledInDir(parentDir) {
const tracer = ideClassStaticTracer('listInstalledInDir');
const releases = await GameMakerIde_1.listReleases();
tracer(`Searching for folders with GameMaker .exe files in "${parentDir}"`);
const ideExecutables = await listInstalledIdes(parentDir);
tracer(`Found ${ideExecutables.length} GameMaker .exe files`);
const ideVersions = await Promise.all(ideExecutables.map(async (executablePath) => {
const possibleFileNames = /^(IDE|GameMaker(Studio2?)?(-(Beta|LTS))?)\.dll$/;
const parentDir = executablePath.up();
tracer(`Parsing version information from GameMaker installation in "${parentDir}"`);
const dllFile = (await parentDir.listChildren()).filter((p) => {
const match = p.basename.match(possibleFileNames);
return !!match;
})[0];
ok(await dllFile?.exists(), `Could not find DLL file for ${executablePath}`);
// The main DLL file is binary, but it contains
// plaintext version strings for the IDE version.
const version = (await dllFile.read()).match(/\b((23|2|20\d{2})\.\d{1,4}\.\d{1,4}\.\d{1,4})\b/u)?.[1];
ok(version, `Could not find a version string in ${dllFile.absolute}`);
const matchingFeedVersion = releases.find((v) => v.ide.version === version);
if (!matchingFeedVersion) {
logger.warn(`Found local install of GameMaker ${version}, but that version is not in the feed.`);
return;
}
return new GameMakerIde_1({
version,
channel: matchingFeedVersion.channel,
executablePath,
directory: executablePath.up(),
publishedAt: new Date(matchingFeedVersion.publishedAt),
feedUrl: matchingFeedVersion.ide.feedUrl,
});
}));
return ideVersions.filter((v) => v);
}
/**
* The parent folder inside of which all IDE
* installs are cached in their own separate
* folders. For the path to a specific IDE
* version, use {@link cachedIdeDirectory}.
*/
static get defaultCachedIdeParentDirectory() {
return GameMakerIde_1.cacheDir.join('ide');
}
static cachedIdeInstallerPath(version) {
return GameMakerIde_1.defaultCachedIdeParentDirectory.join(`gamemaker-${version}.exe`);
}
static cachedIdeDirectory(version) {
return GameMakerIde_1.defaultCachedIdeParentDirectory.join(`gamemaker-${version}`);
}
};
__decorate([
trace,
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:returntype", Promise)
], GameMakerIde, "install", null);
__decorate([
trace,
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], GameMakerIde, "findInstalled", null);
__decorate([
trace,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], GameMakerIde, "listInstalled", null);
__decorate([
trace,
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", Promise)
], GameMakerIde, "findDirectlyInstalled", null);
__decorate([
trace,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], GameMakerIde, "listDirectlyInstalled", null);
__decorate([
trace,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], GameMakerIde, "listInstalledInDir", null);
GameMakerIde = GameMakerIde_1 = __decorate([
trace,
__metadata("design:paramtypes", [Object])
], GameMakerIde);
export { GameMakerIde };
//# sourceMappingURL=GameMakerIde.js.map