mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 05:24:55 +03:00
Merge pull request #1355 from dvsouto/feat/launch-options
feat: add custom launch options to game
This commit is contained in:
commit
9060d435cf
@ -167,6 +167,9 @@
|
|||||||
"loading_save_preview": "Searching for save games…",
|
"loading_save_preview": "Searching for save games…",
|
||||||
"wine_prefix": "Wine Prefix",
|
"wine_prefix": "Wine Prefix",
|
||||||
"wine_prefix_description": "The Wine prefix used to run this game",
|
"wine_prefix_description": "The Wine prefix used to run this game",
|
||||||
|
"launch_options": "Launch Options",
|
||||||
|
"launch_options_description": "Advanced users may choose to enter modifications to their launch options",
|
||||||
|
"launch_options_placeholder": "No parameter specified",
|
||||||
"no_download_option_info": "No information available",
|
"no_download_option_info": "No information available",
|
||||||
"backup_deletion_failed": "Failed to delete backup",
|
"backup_deletion_failed": "Failed to delete backup",
|
||||||
"max_number_of_artifacts_reached": "Maximum number of backups reached for this game",
|
"max_number_of_artifacts_reached": "Maximum number of backups reached for this game",
|
||||||
|
@ -155,6 +155,9 @@
|
|||||||
"loading_save_preview": "Buscando por arquivos de salvamento…",
|
"loading_save_preview": "Buscando por arquivos de salvamento…",
|
||||||
"wine_prefix": "Prefixo Wine",
|
"wine_prefix": "Prefixo Wine",
|
||||||
"wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo",
|
"wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo",
|
||||||
|
"launch_options": "Opções de Inicialização",
|
||||||
|
"launch_options_description": "Usuários avançados podem adicionar opções de inicialização no jogo",
|
||||||
|
"launch_options_placeholder": "Nenhum parâmetro informado",
|
||||||
"no_download_option_info": "Sem informações disponíveis",
|
"no_download_option_info": "Sem informações disponíveis",
|
||||||
"backup_deletion_failed": "Falha ao apagar backup",
|
"backup_deletion_failed": "Falha ao apagar backup",
|
||||||
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
||||||
|
@ -37,6 +37,9 @@ export class Game {
|
|||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
executablePath: string | null;
|
executablePath: string | null;
|
||||||
|
|
||||||
|
@Column("text", { nullable: true })
|
||||||
|
launchOptions: string | null;
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
winePrefixPath: string | null;
|
winePrefixPath: string | null;
|
||||||
|
|
||||||
|
9
src/main/events/helpers/parse-launch-options.ts
Normal file
9
src/main/events/helpers/parse-launch-options.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const parseLaunchOptions = (params: string | null): string[] => {
|
||||||
|
if (params == null || params == "") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramsSplit = params.split(" ");
|
||||||
|
|
||||||
|
return paramsSplit;
|
||||||
|
};
|
@ -22,6 +22,7 @@ import "./library/open-game-executable-path";
|
|||||||
import "./library/open-game-installer";
|
import "./library/open-game-installer";
|
||||||
import "./library/open-game-installer-path";
|
import "./library/open-game-installer-path";
|
||||||
import "./library/update-executable-path";
|
import "./library/update-executable-path";
|
||||||
|
import "./library/update-launch-options";
|
||||||
import "./library/verify-executable-path";
|
import "./library/verify-executable-path";
|
||||||
import "./library/remove-game";
|
import "./library/remove-game";
|
||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
|
@ -2,18 +2,31 @@ import { gameRepository } from "@main/repository";
|
|||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
|
import { spawn } from "child_process";
|
||||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||||
|
import { parseLaunchOptions } from "../helpers/parse-launch-options";
|
||||||
|
|
||||||
const openGame = async (
|
const openGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number,
|
gameId: number,
|
||||||
executablePath: string
|
executablePath: string,
|
||||||
|
launchOptions: string | null
|
||||||
) => {
|
) => {
|
||||||
const parsedPath = parseExecutablePath(executablePath);
|
const parsedPath = parseExecutablePath(executablePath);
|
||||||
|
const parsedParams = parseLaunchOptions(launchOptions);
|
||||||
|
|
||||||
await gameRepository.update({ id: gameId }, { executablePath: parsedPath });
|
await gameRepository.update(
|
||||||
|
{ id: gameId },
|
||||||
|
{ executablePath: parsedPath, launchOptions }
|
||||||
|
);
|
||||||
|
|
||||||
shell.openPath(parsedPath);
|
if (process.platform === "linux" || process.platform === "darwin") {
|
||||||
|
shell.openPath(parsedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
spawn(parsedPath, parsedParams, { shell: false, detached: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("openGame", openGame);
|
registerEvent("openGame", openGame);
|
||||||
|
19
src/main/events/library/update-launch-options.ts
Normal file
19
src/main/events/library/update-launch-options.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const updateLaunchOptions = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
id: number,
|
||||||
|
launchOptions: string | null
|
||||||
|
) => {
|
||||||
|
return gameRepository.update(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
launchOptions: launchOptions?.trim() != "" ? launchOptions : null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("updateLaunchOptions", updateLaunchOptions);
|
@ -16,6 +16,7 @@ import { AddDisableNsfwAlertColumn } from "./migrations/20241106053733_add_disab
|
|||||||
import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed_colum";
|
import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed_colum";
|
||||||
import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download";
|
import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download";
|
||||||
import { AddHiddenAchievementDescriptionColumn } from "./migrations/20241216140633_add_hidden_achievement_description_column ";
|
import { AddHiddenAchievementDescriptionColumn } from "./migrations/20241216140633_add_hidden_achievement_description_column ";
|
||||||
|
import { AddLaunchOptionsColumnToGame } from "./migrations/20241226044022_add_launch_options_column_to_game";
|
||||||
|
|
||||||
export type HydraMigration = Knex.Migration & { name: string };
|
export type HydraMigration = Knex.Migration & { name: string };
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
|||||||
AddShouldSeedColumn,
|
AddShouldSeedColumn,
|
||||||
AddSeedAfterDownloadColumn,
|
AddSeedAfterDownloadColumn,
|
||||||
AddHiddenAchievementDescriptionColumn,
|
AddHiddenAchievementDescriptionColumn,
|
||||||
|
AddLaunchOptionsColumnToGame,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
getMigrationName(migration: HydraMigration): string {
|
getMigrationName(migration: HydraMigration): string {
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import type { HydraMigration } from "@main/knex-client";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
export const AddLaunchOptionsColumnToGame: HydraMigration = {
|
||||||
|
name: "AddLaunchOptionsColumnToGame",
|
||||||
|
up: (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("game", (table) => {
|
||||||
|
return table.string("launchOptions").nullable();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("game", (table) => {
|
||||||
|
return table.dropColumn("launchOptions");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
@ -104,6 +104,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("createGameShortcut", id),
|
ipcRenderer.invoke("createGameShortcut", id),
|
||||||
updateExecutablePath: (id: number, executablePath: string | null) =>
|
updateExecutablePath: (id: number, executablePath: string | null) =>
|
||||||
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
|
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
|
||||||
|
updateLaunchOptions: (id: number, launchOptions: string | null) =>
|
||||||
|
ipcRenderer.invoke("updateLaunchOptions", id, launchOptions),
|
||||||
selectGameWinePrefix: (id: number, winePrefixPath: string | null) =>
|
selectGameWinePrefix: (id: number, winePrefixPath: string | null) =>
|
||||||
ipcRenderer.invoke("selectGameWinePrefix", id, winePrefixPath),
|
ipcRenderer.invoke("selectGameWinePrefix", id, winePrefixPath),
|
||||||
verifyExecutablePathInUse: (executablePath: string) =>
|
verifyExecutablePathInUse: (executablePath: string) =>
|
||||||
@ -115,8 +117,11 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("openGameInstallerPath", gameId),
|
ipcRenderer.invoke("openGameInstallerPath", gameId),
|
||||||
openGameExecutablePath: (gameId: number) =>
|
openGameExecutablePath: (gameId: number) =>
|
||||||
ipcRenderer.invoke("openGameExecutablePath", gameId),
|
ipcRenderer.invoke("openGameExecutablePath", gameId),
|
||||||
openGame: (gameId: number, executablePath: string) =>
|
openGame: (
|
||||||
ipcRenderer.invoke("openGame", gameId, executablePath),
|
gameId: number,
|
||||||
|
executablePath: string,
|
||||||
|
launchOptions: string | null
|
||||||
|
) => ipcRenderer.invoke("openGame", gameId, executablePath, launchOptions),
|
||||||
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
||||||
removeGameFromLibrary: (gameId: number) =>
|
removeGameFromLibrary: (gameId: number) =>
|
||||||
ipcRenderer.invoke("removeGameFromLibrary", gameId),
|
ipcRenderer.invoke("removeGameFromLibrary", gameId),
|
||||||
|
@ -154,7 +154,11 @@ export function Sidebar() {
|
|||||||
|
|
||||||
if (event.detail === 2) {
|
if (event.detail === 2) {
|
||||||
if (game.executablePath) {
|
if (game.executablePath) {
|
||||||
window.electron.openGame(game.id, game.executablePath);
|
window.electron.openGame(
|
||||||
|
game.id,
|
||||||
|
game.executablePath,
|
||||||
|
game.launchOptions
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showWarningToast(t("game_has_no_executable"));
|
showWarningToast(t("game_has_no_executable"));
|
||||||
}
|
}
|
||||||
|
10
src/renderer/src/declaration.d.ts
vendored
10
src/renderer/src/declaration.d.ts
vendored
@ -93,6 +93,10 @@ declare global {
|
|||||||
id: number,
|
id: number,
|
||||||
executablePath: string | null
|
executablePath: string | null
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
updateLaunchOptions: (
|
||||||
|
id: number,
|
||||||
|
launchOptions: string | null
|
||||||
|
) => Promise<void>;
|
||||||
selectGameWinePrefix: (
|
selectGameWinePrefix: (
|
||||||
id: number,
|
id: number,
|
||||||
winePrefixPath: string | null
|
winePrefixPath: string | null
|
||||||
@ -102,7 +106,11 @@ declare global {
|
|||||||
openGameInstaller: (gameId: number) => Promise<boolean>;
|
openGameInstaller: (gameId: number) => Promise<boolean>;
|
||||||
openGameInstallerPath: (gameId: number) => Promise<boolean>;
|
openGameInstallerPath: (gameId: number) => Promise<boolean>;
|
||||||
openGameExecutablePath: (gameId: number) => Promise<void>;
|
openGameExecutablePath: (gameId: number) => Promise<void>;
|
||||||
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
openGame: (
|
||||||
|
gameId: number,
|
||||||
|
executablePath: string,
|
||||||
|
launchOptions: string | null
|
||||||
|
) => Promise<void>;
|
||||||
closeGame: (gameId: number) => Promise<boolean>;
|
closeGame: (gameId: number) => Promise<boolean>;
|
||||||
removeGameFromLibrary: (gameId: number) => Promise<void>;
|
removeGameFromLibrary: (gameId: number) => Promise<void>;
|
||||||
removeGame: (gameId: number) => Promise<void>;
|
removeGame: (gameId: number) => Promise<void>;
|
||||||
|
@ -55,13 +55,21 @@ export function HeroPanelActions() {
|
|||||||
const openGame = async () => {
|
const openGame = async () => {
|
||||||
if (game) {
|
if (game) {
|
||||||
if (game.executablePath) {
|
if (game.executablePath) {
|
||||||
window.electron.openGame(game.id, game.executablePath);
|
window.electron.openGame(
|
||||||
|
game.id,
|
||||||
|
game.executablePath,
|
||||||
|
game.launchOptions
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gameExecutablePath = await selectGameExecutable();
|
const gameExecutablePath = await selectGameExecutable();
|
||||||
if (gameExecutablePath)
|
if (gameExecutablePath)
|
||||||
window.electron.openGame(game.id, gameExecutablePath);
|
window.electron.openGame(
|
||||||
|
game.id,
|
||||||
|
gameExecutablePath,
|
||||||
|
game.launchOptions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useContext, useState } from "react";
|
import { useContext, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Modal, TextField } from "@renderer/components";
|
import { Button, Modal, TextField } from "@renderer/components";
|
||||||
import type { Game } from "@types";
|
import type { Game } from "@types";
|
||||||
@ -8,6 +8,7 @@ import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
|||||||
import { useDownload, useToast } from "@renderer/hooks";
|
import { useDownload, useToast } from "@renderer/hooks";
|
||||||
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
||||||
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
||||||
|
import { debounce } from "lodash-es";
|
||||||
|
|
||||||
export interface GameOptionsModalProps {
|
export interface GameOptionsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -29,6 +30,7 @@ export function GameOptionsModal({
|
|||||||
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
|
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
|
||||||
|
const [launchOptions, setLaunchOptions] = useState(game.launchOptions ?? "");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
removeGameInstaller,
|
removeGameInstaller,
|
||||||
@ -44,6 +46,13 @@ export function GameOptionsModal({
|
|||||||
const isGameDownloading =
|
const isGameDownloading =
|
||||||
game.status === "active" && lastPacket?.game.id === game.id;
|
game.status === "active" && lastPacket?.game.id === game.id;
|
||||||
|
|
||||||
|
const debounceUpdateLaunchOptions = useRef(
|
||||||
|
debounce(async (value: string) => {
|
||||||
|
await window.electron.updateLaunchOptions(game.id, value);
|
||||||
|
updateGame();
|
||||||
|
}, 1000)
|
||||||
|
).current;
|
||||||
|
|
||||||
const handleRemoveGameFromLibrary = async () => {
|
const handleRemoveGameFromLibrary = async () => {
|
||||||
if (isGameDownloading) {
|
if (isGameDownloading) {
|
||||||
await cancelDownload(game.id);
|
await cancelDownload(game.id);
|
||||||
@ -116,9 +125,25 @@ export function GameOptionsModal({
|
|||||||
updateGame();
|
updateGame();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeLaunchOptions = async (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
|
||||||
|
setLaunchOptions(value);
|
||||||
|
debounceUpdateLaunchOptions(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearLaunchOptions = async () => {
|
||||||
|
setLaunchOptions("");
|
||||||
|
|
||||||
|
window.electron.updateLaunchOptions(game.id, null).then(updateGame);
|
||||||
|
};
|
||||||
|
|
||||||
const shouldShowWinePrefixConfiguration =
|
const shouldShowWinePrefixConfiguration =
|
||||||
window.electron.platform === "linux";
|
window.electron.platform === "linux";
|
||||||
|
|
||||||
|
const shouldShowLaunchOptionsConfiguration =
|
||||||
|
window.electron.platform === "win32";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteGameModal
|
<DeleteGameModal
|
||||||
@ -226,6 +251,28 @@ export function GameOptionsModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{shouldShowLaunchOptionsConfiguration && (
|
||||||
|
<div className={styles.gameOptionHeader}>
|
||||||
|
<h2>{t("launch_options")}</h2>
|
||||||
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
|
{t("launch_options_description")}
|
||||||
|
</h4>
|
||||||
|
<TextField
|
||||||
|
value={launchOptions}
|
||||||
|
theme="dark"
|
||||||
|
placeholder={t("launch_options_placeholder")}
|
||||||
|
onChange={handleChangeLaunchOptions}
|
||||||
|
rightContent={
|
||||||
|
game.launchOptions && (
|
||||||
|
<Button onClick={handleClearLaunchOptions} theme="outline">
|
||||||
|
{t("clear")}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className={styles.gameOptionHeader}>
|
||||||
<h2>{t("downloads_secion_title")}</h2>
|
<h2>{t("downloads_secion_title")}</h2>
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
|
@ -115,6 +115,7 @@ export interface Game {
|
|||||||
downloader: Downloader;
|
downloader: Downloader;
|
||||||
winePrefixPath: string | null;
|
winePrefixPath: string | null;
|
||||||
executablePath: string | null;
|
executablePath: string | null;
|
||||||
|
launchOptions: string | null;
|
||||||
lastTimePlayed: Date | null;
|
lastTimePlayed: Date | null;
|
||||||
uri: string | null;
|
uri: string | null;
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
|
Loading…
Reference in New Issue
Block a user