fix: logins now refresh on plugins load

feat: Added tar archive support
fix: Downloaded games and emulator execute permission now updated
fix: Fixed rclone for linux
fix: on screen keyaboard only now shows up when using a gamepad or touch
This commit is contained in:
Simeon Radivoev 2026-04-21 23:21:50 +03:00
parent 6aacec2c0d
commit 7bd0ebdcca
Signed by: simeonradivoev
GPG key ID: C16C2132A7660C8E
39 changed files with 523 additions and 275 deletions

View file

@ -11,6 +11,7 @@ import { ensureDir, move } from "fs-extra";
import { simulateProgress } from "@/bun/utils";
import { path7za } from "7zip-bin";
import { getEmulatorDownload, getEmulatorPath } from "../store/services/emulatorsService";
import { $ } from "bun";
type EmulatorDownloadStates = "download" | "extract";
@ -61,7 +62,7 @@ export class EmulatorDownloadJob implements IJob<z.infer<typeof EmulatorDownload
const destinationPaths = await downloader.start();
if (destinationPaths)
{
const isArchive = destinationPaths[0].endsWith('.7z') || destinationPaths[0].endsWith('.zip');
const isArchive = destinationPaths[0].endsWith('.7z') || destinationPaths[0].endsWith('.zip') || destinationPaths[0].endsWith('.tar');
const isAppImage = destinationPaths[0].endsWith(".AppImage");
if (!isArchive && !isAppImage)
@ -74,14 +75,23 @@ export class EmulatorDownloadJob implements IJob<z.infer<typeof EmulatorDownload
if (destinationPaths[0])
{
let destinationPath = destinationPaths[0];
await new Promise((resolve, reject) =>
if (destinationPath.endsWith('.tar'))
{
const seven = Seven.extractFull(destinationPath, emulatorsFolder, { $bin: process.env.ZIP7_PATH ?? path7za, $progress: true, noRootDuplication: true });
seven.on('progress', p => context.setProgress(p.percent, "extract"));
seven.on('error', e => reject(e));
seven.on('end', () => resolve(true));
});
await fs.rm(destinationPath, { recursive: true });
context.setProgress(0, "extract");
await ensureDir(emulatorsFolder);
await $`tar -xf ${destinationPath} -C ${emulatorsFolder}`;
await fs.rm(destinationPath, { recursive: true });
} else
{
await new Promise((resolve, reject) =>
{
const seven = Seven.extractFull(destinationPath, emulatorsFolder, { $bin: process.env.ZIP7_PATH ?? path7za, $progress: true, noRootDuplication: true });
seven.on('progress', p => context.setProgress(p.percent, "extract"));
seven.on('error', e => reject(e));
seven.on('end', () => resolve(true));
});
await fs.rm(destinationPath, { recursive: true });
}
// check if 1 root folder we need to get rid of
const contents = await fs.readdir(emulatorsFolder);
@ -106,15 +116,18 @@ export class EmulatorDownloadJob implements IJob<z.infer<typeof EmulatorDownload
}
}
await Bun.write(`${emulatorsFolder}.json`, JSON.stringify(info, null, 3));
const execs: EmulatorSourceEntryType[] = [];
await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: this.emulator, sources: execs });
await plugins.hooks.emulators.emulatorPostInstall.promise({
emulator: this.emulator,
emulatorPackage: this.emulatorPackage,
path: emulatorsFolder,
path: execs.find(e => e.type === 'store')?.binPath ?? emulatorsFolder,
info,
update: this.isUpdate
});
await Bun.write(`${emulatorsFolder}.json`, JSON.stringify(info, null, 3));
}
}

View file

@ -59,6 +59,7 @@ export class InstallJob implements IJob<never, InstallJobStates>
if (!info) throw new Error(`Could not find downloader for source ${this.source}`);
const files = await checkFiles(info.files, !!info.extract_path);
const finalFiles: string[] = [];
if (this.config?.dryRun !== true)
{
@ -84,6 +85,7 @@ export class InstallJob implements IJob<never, InstallJobStates>
{
return;
}
if (info.extract_path && downloadedFiles)
{
let progress = 0;
@ -139,6 +141,7 @@ export class InstallJob implements IJob<never, InstallJobStates>
if (filePath.endsWith('.zip'))
{
cx.setProgress(0, "extract");
console.error(e);
console.warn("Could not extract", filePath, "with 7zip trying zip extractor");
await ensureDir(extractPath);
const zip = new StreamZip.async({ file: filePath });
@ -175,6 +178,12 @@ export class InstallJob implements IJob<never, InstallJobStates>
await move(tmpGameFolder, extractPath, { overwrite: true });
}
}
finalFiles.push(extractPath);
} else
{
finalFiles.push(...downloadedFiles);
}
}
@ -323,6 +332,7 @@ export class InstallJob implements IJob<never, InstallJobStates>
await simulateProgress(p => cx.setProgress(p, "download"), cx.abortSignal);
}
await plugins.hooks.games.postInstall.promise({ source: this.source, id: this.gameId, files: finalFiles, info });
events.emit('notification', { message: `${info.name}: Installed`, type: 'success', duration: 8000 });
}

View file

@ -5,9 +5,9 @@ import { db, events, plugins } from "../app";
import * as appSchema from "@schema/app";
import { eq } from "drizzle-orm";
import { spawn } from 'node:child_process';
import { watch } from "node:fs";
import fs from "node:fs/promises";
import { updateLocalLastPlayed } from "../games/services/statusService";
import { getErrorMessage } from "@/bun/utils";
export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSchema>, string>
{
@ -42,15 +42,24 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
const source = this.gameSource ?? this.gameId.source;
const id = this.gameSourceId ?? this.gameId.id;
await plugins.hooks.games.postPlay.promise(
{
source,
id,
command: this.validCommand,
changedSaveFiles: Array.from(this.changedSaveFiles.values()),
validChangedSaveFiles: {},
gameInfo
}).catch(e => console.error(e));
await new Promise(async (resolve) =>
{
await plugins.hooks.games.postPlay.promise(
{
source,
id,
command: this.validCommand,
changedSaveFiles: Array.from(this.changedSaveFiles.values()),
validChangedSaveFiles: {},
gameInfo
}).catch(e =>
{
console.error(e);
events.emit('notification', { message: getErrorMessage(e), type: 'error' });
}).then(() => resolve(false));
const timeoutHandler = () => resolve(false);
setTimeout(timeoutHandler, 5000);
});
}
prePlay (setProgress: (progress: number, state: string) => void, gameInfo: { platformSlug?: string; })
@ -118,31 +127,58 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
{
await this.prePlay(context.setProgress.bind(context), { platformSlug: gameInfo?.platformSlug }).catch(e => reject(e));
// ES-DE commands require shell execution. Some emulators fail otherwise.
const spawnGame = spawn(this.validCommand.command, {
shell: this.validCommand.shell ?? true,
cwd: this.validCommand.startDir,
signal: context.abortSignal,
env: {
...process.env,
...this.validCommand.env
},
});
context.setProgress(0, "playing");
spawnGame.stdout.on('data', data => console.log(data));
spawnGame.on('close', (code) =>
if (Array.isArray(this.validCommand.command))
{
resolve(code);
});
spawnGame.on('error', e =>
{
console.error(e);
resolve(1);
});
const bunGame = Bun.spawn(this.validCommand.command, {
cwd: this.validCommand.startDir,
signal: context.abortSignal,
env: {
...process.env,
...this.validCommand.env
}
});
context.setProgress(0, "playing");
bunGame.exited.then(e =>
{
resolve(true);
}).catch(e =>
{
console.error(e);
reject(e);
});
game = bunGame;
} else
{
// ES-DE commands require shell execution. Some emulators fail otherwise.
const spawnGame = spawn(this.validCommand.command, {
shell: this.validCommand.shell ?? true,
cwd: this.validCommand.startDir,
signal: context.abortSignal,
env: {
...process.env,
...this.validCommand.env
},
});
context.setProgress(0, "playing");
spawnGame.stdout.on('data', data => console.log(data));
spawnGame.on('close', (code) =>
{
resolve(code);
});
spawnGame.on('error', e =>
{
console.error(e);
resolve(1);
});
game = spawnGame;
}
game = spawnGame;
}
else if (this.validCommand.metadata.emulatorBin)
{
@ -151,7 +187,6 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
await this.prePlay(context.setProgress.bind(context), { platformSlug: gameInfo?.platformSlug });
// We have full control over launching integrated emulators better to use bun spawn
await fs.chmod(this.validCommand.metadata.emulatorBin, 0o755);
const bunGame = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs.args], {
cwd: this.validCommand.startDir,
signal: context.abortSignal,
@ -212,7 +247,7 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
} catch (e)
{
context.abort(e);
reject(e);
resolve(e);
}
});