Skip to content

Commit

Permalink
Merge pull request #1355 from dvsouto/feat/launch-options
Browse files Browse the repository at this point in the history
feat: add custom launch options to game
  • Loading branch information
zamitto authored Dec 28, 2024
2 parents 3bef263 + d8e7fca commit 9060d43
Show file tree
Hide file tree
Showing 15 changed files with 153 additions and 10 deletions.
3 changes: 3 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@
"loading_save_preview": "Searching for save games…",
"wine_prefix": "Wine Prefix",
"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",
"backup_deletion_failed": "Failed to delete backup",
"max_number_of_artifacts_reached": "Maximum number of backups reached for this game",
Expand Down
3 changes: 3 additions & 0 deletions src/locales/pt-BR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@
"loading_save_preview": "Buscando por arquivos de salvamento…",
"wine_prefix": "Prefixo Wine",
"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",
"backup_deletion_failed": "Falha ao apagar backup",
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
Expand Down
3 changes: 3 additions & 0 deletions src/main/entity/game.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export class Game {
@Column("text", { nullable: true })
executablePath: string | null;

@Column("text", { nullable: true })
launchOptions: string | null;

@Column("text", { nullable: true })
winePrefixPath: string | null;

Expand Down
9 changes: 9 additions & 0 deletions src/main/events/helpers/parse-launch-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const parseLaunchOptions = (params: string | null): string[] => {
if (params == null || params == "") {
return [];
}

const paramsSplit = params.split(" ");

return paramsSplit;
};
1 change: 1 addition & 0 deletions src/main/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import "./library/open-game-executable-path";
import "./library/open-game-installer";
import "./library/open-game-installer-path";
import "./library/update-executable-path";
import "./library/update-launch-options";
import "./library/verify-executable-path";
import "./library/remove-game";
import "./library/remove-game-from-library";
Expand Down
19 changes: 16 additions & 3 deletions src/main/events/library/open-game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,31 @@ import { gameRepository } from "@main/repository";

import { registerEvent } from "../register-event";
import { shell } from "electron";
import { spawn } from "child_process";
import { parseExecutablePath } from "../helpers/parse-executable-path";
import { parseLaunchOptions } from "../helpers/parse-launch-options";

const openGame = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number,
executablePath: string
executablePath: string,
launchOptions: string | null
) => {
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);
19 changes: 19 additions & 0 deletions src/main/events/library/update-launch-options.ts
Original file line number Diff line number Diff line change
@@ -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);
2 changes: 2 additions & 0 deletions src/main/knex-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { AddDisableNsfwAlertColumn } from "./migrations/20241106053733_add_disab
import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed_colum";
import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download";
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 };

Expand All @@ -37,6 +38,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
AddShouldSeedColumn,
AddSeedAfterDownloadColumn,
AddHiddenAchievementDescriptionColumn,
AddLaunchOptionsColumnToGame,
]);
}
getMigrationName(migration: HydraMigration): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
},
};
9 changes: 7 additions & 2 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("createGameShortcut", id),
updateExecutablePath: (id: number, executablePath: string | null) =>
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
updateLaunchOptions: (id: number, launchOptions: string | null) =>
ipcRenderer.invoke("updateLaunchOptions", id, launchOptions),
selectGameWinePrefix: (id: number, winePrefixPath: string | null) =>
ipcRenderer.invoke("selectGameWinePrefix", id, winePrefixPath),
verifyExecutablePathInUse: (executablePath: string) =>
Expand All @@ -115,8 +117,11 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("openGameInstallerPath", gameId),
openGameExecutablePath: (gameId: number) =>
ipcRenderer.invoke("openGameExecutablePath", gameId),
openGame: (gameId: number, executablePath: string) =>
ipcRenderer.invoke("openGame", gameId, executablePath),
openGame: (
gameId: number,
executablePath: string,
launchOptions: string | null
) => ipcRenderer.invoke("openGame", gameId, executablePath, launchOptions),
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
removeGameFromLibrary: (gameId: number) =>
ipcRenderer.invoke("removeGameFromLibrary", gameId),
Expand Down
6 changes: 5 additions & 1 deletion src/renderer/src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@ export function Sidebar() {

if (event.detail === 2) {
if (game.executablePath) {
window.electron.openGame(game.id, game.executablePath);
window.electron.openGame(
game.id,
game.executablePath,
game.launchOptions
);
} else {
showWarningToast(t("game_has_no_executable"));
}
Expand Down
10 changes: 9 additions & 1 deletion src/renderer/src/declaration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ declare global {
id: number,
executablePath: string | null
) => Promise<void>;
updateLaunchOptions: (
id: number,
launchOptions: string | null
) => Promise<void>;
selectGameWinePrefix: (
id: number,
winePrefixPath: string | null
Expand All @@ -102,7 +106,11 @@ declare global {
openGameInstaller: (gameId: number) => Promise<boolean>;
openGameInstallerPath: (gameId: number) => Promise<boolean>;
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>;
removeGameFromLibrary: (gameId: number) => Promise<void>;
removeGame: (gameId: number) => Promise<void>;
Expand Down
12 changes: 10 additions & 2 deletions src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,21 @@ export function HeroPanelActions() {
const openGame = async () => {
if (game) {
if (game.executablePath) {
window.electron.openGame(game.id, game.executablePath);
window.electron.openGame(
game.id,
game.executablePath,
game.launchOptions
);
return;
}

const gameExecutablePath = await selectGameExecutable();
if (gameExecutablePath)
window.electron.openGame(game.id, gameExecutablePath);
window.electron.openGame(
game.id,
gameExecutablePath,
game.launchOptions
);
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useState } from "react";
import { useContext, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components";
import type { Game } from "@types";
Expand All @@ -8,6 +8,7 @@ import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
import { useDownload, useToast } from "@renderer/hooks";
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
import { debounce } from "lodash-es";

export interface GameOptionsModalProps {
visible: boolean;
Expand All @@ -29,6 +30,7 @@ export function GameOptionsModal({

const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
const [launchOptions, setLaunchOptions] = useState(game.launchOptions ?? "");

const {
removeGameInstaller,
Expand All @@ -44,6 +46,13 @@ export function GameOptionsModal({
const isGameDownloading =
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 () => {
if (isGameDownloading) {
await cancelDownload(game.id);
Expand Down Expand Up @@ -116,9 +125,25 @@ export function GameOptionsModal({
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 =
window.electron.platform === "linux";

const shouldShowLaunchOptionsConfiguration =
window.electron.platform === "win32";

return (
<>
<DeleteGameModal
Expand Down Expand Up @@ -226,6 +251,28 @@ export function GameOptionsModal({
</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}>
<h2>{t("downloads_secion_title")}</h2>
<h4 className={styles.gameOptionHeaderDescription}>
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export interface Game {
downloader: Downloader;
winePrefixPath: string | null;
executablePath: string | null;
launchOptions: string | null;
lastTimePlayed: Date | null;
uri: string | null;
fileSize: number;
Expand Down

0 comments on commit 9060d43

Please sign in to comment.