From 1e7234d12d2e918d9106d580d8b152855bf5f5da Mon Sep 17 00:00:00 2001
From: Jydet
Date: Sat, 11 Jul 2026 10:20:20 +0200
Subject: [PATCH 1/4] feat: edit the settings of the game in the dev tab
---
arguments_example.json | 57 +++++
package-lock.json | 4 +-
packages/main/src/modules/GameClient.ts | 59 +++--
packages/main/src/modules/GameUpdater.ts | 1 +
packages/main/src/services/SettingsManager.ts | 1 +
packages/preload/src/index.ts | 2 +
.../renderer/src/components/SettingsMenu.tsx | 238 +++++++++++++++++-
packages/renderer/src/types/index.ts | 1 +
packages/renderer/src/vite-env.d.ts | 1 +
9 files changed, 339 insertions(+), 25 deletions(-)
create mode 100644 arguments_example.json
diff --git a/arguments_example.json b/arguments_example.json
new file mode 100644
index 0000000..e68bc54
--- /dev/null
+++ b/arguments_example.json
@@ -0,0 +1,57 @@
+[
+ {
+ "key": "DEV_MODE",
+ "description": "Activates the developer mode, enabling additional debug tools and shortcuts.",
+ "type": "boolean"
+ },
+ {
+ "key": "CONFIGURATION_FILE",
+ "description": "Path to the configuration file to use.",
+ "type": "string"
+ },
+ {
+ "key": "NO_CAMERA_MIN_ZOOM",
+ "description": "Disables the minimum zoom restriction on the camera.",
+ "type": "boolean"
+ },
+ {
+ "key": "ONLY_ALLOWED_TEAM_TAB",
+ "description": "The tab number of the only tab allowed in the Fighter Management UI.",
+ "type": "number"
+ },
+ {
+ "key": "ONLY_ALLOWED_LADDER_TAB",
+ "description": "The tab number of the only tab allowed in the Ladder UI.",
+ "type": "number"
+ },
+ {
+ "key": "SKIP_TURN_NO_DELAY",
+ "description": "Bypass the delay before ending a turn.",
+ "type": "boolean"
+ },
+ {
+ "key": "REPLAY_FILE_PATH",
+ "description": "Path to a replay file to load and play.",
+ "type": "string"
+ },
+ {
+ "key": "WORLD_FADE",
+ "description": "Activates the world fade effect during transitions.",
+ "type": "boolean"
+ },
+ {
+ "key": "HOT_RELOAD_EFFECT",
+ "description": "Enables hot reloading of visual effects.",
+ "type": "boolean"
+ },
+ {
+ "key": "NO_CLASS_RESTRICTION",
+ "description": "Disables class restrictions for character creation or editing.",
+ "type": "boolean"
+ },
+ {
+ "key": "RESTORE_CONSOLE_PATH",
+ "description": "Restores the last used console path on startup instead of defaulting to admin.",
+ "type": "boolean"
+ }
+]
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index e0cc8d0..1565856 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "arena-returns-launcher",
- "version": "3.4.2",
+ "version": "3.5.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "arena-returns-launcher",
- "version": "3.4.2",
+ "version": "3.5.1",
"workspaces": [
"packages/*"
],
diff --git a/packages/main/src/modules/GameClient.ts b/packages/main/src/modules/GameClient.ts
index 35a86fe..c5a9519 100644
--- a/packages/main/src/modules/GameClient.ts
+++ b/packages/main/src/modules/GameClient.ts
@@ -1,13 +1,12 @@
-import { ipcMain, app } from "electron";
-import { join } from "path";
-import { existsSync, mkdirSync } from "fs";
-import { chmodSync } from "fs";
-import { stat, chmod, readdir, readFile, appendFile } from "fs/promises";
-import { exec } from "child_process";
+import {app, ipcMain} from "electron";
+import {join} from "path";
+import {chmodSync, existsSync, mkdirSync} from "fs";
+import {appendFile, chmod, readdir, readFile, stat} from "fs/promises";
+import {exec} from "child_process";
import log from "electron-log";
-import { GameUpdater, GameSettings, ReplayFile } from "./GameUpdater.js";
-import type { AppModule } from "../AppModule.js";
-import type { ModuleContext } from "../ModuleContext.js";
+import {GameSettings, GameUpdater, ReplayFile} from "./GameUpdater.js";
+import type {AppModule} from "../AppModule.js";
+import type {ModuleContext} from "../ModuleContext.js";
export class GameClient implements AppModule {
private gameUpdater: GameUpdater | null = null;
@@ -37,6 +36,9 @@ export class GameClient implements AppModule {
ipcMain.handle("gameClient:launchReplayOffline", (_e, path) =>
this.launchReplayOffline(path)
);
+ ipcMain.handle("gameClient:getGameArgumentsDescriptor", () =>
+ this.getGameArgumentsDescriptor()
+ );
}
onSettingsUpdate(settings: GameSettings): void {
@@ -75,18 +77,15 @@ export class GameClient implements AppModule {
await this.startJavaProcess({
mainClass: "com.ankamagames.dofusarena.client.DofusArenaClient",
settings: this.currentSettings || undefined,
- extraArgs: [
- "-ONLY_ALLOWED_TEAM_TAB=1",
- "-ONLY_ALLOWED_LADDER_TAB=ONE_VS_ONE",
- ],
+ extraArgs: this.currentSettings?.devExtraJavaArgs.split(" ").map(arg => `-${arg}`) ?? [],
});
}
async openReplaysFolder(): Promise {
- const { shell } = await import("electron");
+ const {shell} = await import("electron");
const replaysPath = join(this.gameClientPath, "game", "replays");
- mkdirSync(replaysPath, { recursive: true });
+ mkdirSync(replaysPath, {recursive: true});
try {
await shell.openPath(replaysPath);
@@ -101,8 +100,8 @@ export class GameClient implements AppModule {
async listReplays(): Promise {
const replaysPath = join(this.gameClientPath, "game", "replays");
- const { readdir } = await import("fs/promises");
- mkdirSync(replaysPath, { recursive: true });
+ const {readdir} = await import("fs/promises");
+ mkdirSync(replaysPath, {recursive: true});
try {
const files = await readdir(replaysPath);
@@ -191,7 +190,7 @@ export class GameClient implements AppModule {
settings?: GameSettings;
extraArgs?: string[];
}): Promise {
- const { mainClass, settings, extraArgs = [] } = options;
+ const {mainClass, settings, extraArgs = []} = options;
const gameDir = join(this.gameClientPath, "game");
const libDir = join(this.gameClientPath, "lib");
const jreDir = join(this.gameClientPath, "jre");
@@ -349,7 +348,7 @@ export class GameClient implements AppModule {
return new Promise((resolve, reject) => {
const child = exec(
`"${javaExecutable}" ${args.join(" ")}`,
- { cwd },
+ {cwd},
(error) => {
if (error && !error.killed) {
log.error("Java process error:", error);
@@ -380,7 +379,7 @@ export class GameClient implements AppModule {
}
const child = exec(
`"${javaExecutable}" ${args.join(" ")}`,
- { cwd },
+ {cwd},
(error) => {
if (error && !error.killed) {
log.error("Java process error:", error);
@@ -411,7 +410,7 @@ export class GameClient implements AppModule {
}
// Ensure we have the full system PATH for finding wine
- const env = { ...process.env };
+ const env = {...process.env};
if (!env.PATH?.includes("/opt/homebrew/bin")) {
env.PATH = `${
env.PATH || ""
@@ -420,7 +419,7 @@ export class GameClient implements AppModule {
const child = exec(
`wine "${javaExecutable}" ${args.join(" ")}`,
- { cwd, env },
+ {cwd, env},
(error) => {
if (error && !error.killed) {
log.error("Java process error:", error);
@@ -435,6 +434,22 @@ export class GameClient implements AppModule {
});
}
+ async getGameArgumentsDescriptor(): Promise {
+ try {
+ let schemaPath = join(this.gameClientPath, "game", "arguments.json");
+ if (existsSync(schemaPath)) {
+ log.info("Loading arguments descriptor from ", schemaPath)
+ const content = await readFile(schemaPath, "utf-8");
+ return JSON.parse(content);
+ }
+ log.info("Arguments descriptor not found")
+ return null;
+ } catch (error) {
+ log.error("Failed to load arguments.json:", error);
+ return null;
+ }
+ }
+
private parseReplayFilename(filename: string, fullPath: string): ReplayFile {
const replayFile: ReplayFile = {
filename,
diff --git a/packages/main/src/modules/GameUpdater.ts b/packages/main/src/modules/GameUpdater.ts
index 7042480..cd601ec 100644
--- a/packages/main/src/modules/GameUpdater.ts
+++ b/packages/main/src/modules/GameUpdater.ts
@@ -62,6 +62,7 @@ export interface GameSettings {
gameRamAllocation: number;
devModeEnabled: boolean;
devExtraJavaArgs: string;
+ devGameArgs: string;
devForceVersion: string;
devCdnEnvironment: "production" | "staging";
}
diff --git a/packages/main/src/services/SettingsManager.ts b/packages/main/src/services/SettingsManager.ts
index 1bd6191..d89bd1a 100644
--- a/packages/main/src/services/SettingsManager.ts
+++ b/packages/main/src/services/SettingsManager.ts
@@ -21,6 +21,7 @@ export class SettingsManager {
return {
gameRamAllocation: 2,
devModeEnabled: false,
+ devGameArgs: "ONLY_ALLOWED_TEAM_TAB=1 ONLY_ALLOWED_LADDER_TAB=ONE_VS_ONE",
devExtraJavaArgs: "",
devForceVersion: "",
devCdnEnvironment: "production",
diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts
index 8777d36..9d9af93 100644
--- a/packages/preload/src/index.ts
+++ b/packages/preload/src/index.ts
@@ -58,6 +58,8 @@ const gameClient = {
listReplays: () => ipcRenderer.invoke("gameClient:listReplays"),
launchReplayOffline: (replayPath: string) =>
ipcRenderer.invoke("gameClient:launchReplayOffline", replayPath),
+ getGameArgumentsDescriptor: () =>
+ ipcRenderer.invoke("gameClient:getGameArgumentsDescriptor"),
};
// News functions
diff --git a/packages/renderer/src/components/SettingsMenu.tsx b/packages/renderer/src/components/SettingsMenu.tsx
index 255ccbb..c53c33b 100644
--- a/packages/renderer/src/components/SettingsMenu.tsx
+++ b/packages/renderer/src/components/SettingsMenu.tsx
@@ -16,12 +16,120 @@ import {
Code,
AlertTriangle,
} from "lucide-react";
-import { SimpleSlider, SimpleSelect } from "./common/FormControls";
+import { SimpleSlider, SimpleSelect, SimpleSwitch } from "./common/FormControls";
import type { SettingsState } from "@/types";
import { gameClient, gameUpdater, system } from "@app/preload";
import { useGameStateContext } from "@/contexts/GameStateContext";
import log from "@/utils/logger";
+interface ArgumentDescriptorItem {
+ key: string;
+ description: string;
+ type: "boolean" | "string" | "number";
+}
+
+const parseGameArgs = (argsString: string): Record => {
+ const parsed: Record = {};
+ if (!argsString) return parsed;
+ argsString.split(/\s+/).forEach((part) => {
+ if (!part) return;
+ const [key, ...valParts] = part.split("=");
+ if (valParts.length > 0) {
+ parsed[key] = valParts.join("=");
+ } else {
+ parsed[key] = true;
+ }
+ });
+ return parsed;
+};
+
+const updateDescriptorArg = (
+ currentArgsStr: string,
+ descriptor: ArgumentDescriptorItem[],
+ keyToUpdate: string,
+ newValue: string | boolean
+): string => {
+ const parsed = parseGameArgs(currentArgsStr);
+ const descriptorKeys = new Set(descriptor.map((s) => s.key));
+
+ if (newValue === false || newValue === "") {
+ delete parsed[keyToUpdate];
+ } else {
+ parsed[keyToUpdate] = newValue;
+ }
+
+ const descriptorParts: string[] = [];
+ const customParts: string[] = [];
+
+ Object.entries(parsed).forEach(([key, val]) => {
+ if (descriptorKeys.has(key)) {
+ if (val === true) {
+ descriptorParts.push(key);
+ } else if (val !== false && val !== "" && val !== undefined) {
+ descriptorParts.push(`${key}=${val}`);
+ }
+ } else {
+ if (val === true) {
+ customParts.push(key);
+ } else {
+ customParts.push(`${key}=${val}`);
+ }
+ }
+ });
+
+ const finalParts = [...descriptorParts];
+ if (customParts.length > 0) {
+ finalParts.push(customParts.join(" "));
+ }
+
+ return finalParts.join(" ");
+};
+
+const updateCustomArgs = (
+ currentArgsStr: string,
+ descriptor: ArgumentDescriptorItem[],
+ newCustomStr: string
+): string => {
+ const parsed = parseGameArgs(currentArgsStr);
+ const descriptorKeys = new Set(descriptor.map((s) => s.key));
+
+ const descriptorParts: string[] = [];
+ Object.entries(parsed).forEach(([key, val]) => {
+ if (descriptorKeys.has(key)) {
+ if (val === true) {
+ descriptorParts.push(key);
+ } else if (val !== false && val !== "" && val !== undefined) {
+ descriptorParts.push(`${key}=${val}`);
+ }
+ }
+ });
+
+ const finalParts = [...descriptorParts];
+ if (newCustomStr.trim()) {
+ finalParts.push(newCustomStr.trim());
+ }
+
+ return finalParts.join(" ");
+};
+
+const getCustomArgsString = (currentArgsStr: string, descriptor: ArgumentDescriptorItem[]): string => {
+ const parsed = parseGameArgs(currentArgsStr);
+ const descriptorKeys = new Set(descriptor.map((s) => s.key));
+ const customParts: string[] = [];
+
+ Object.entries(parsed).forEach(([key, val]) => {
+ if (!descriptorKeys.has(key)) {
+ if (val === true) {
+ customParts.push(key);
+ } else {
+ customParts.push(`${key}=${val}`);
+ }
+ }
+ });
+
+ return customParts.join(" ");
+};
+
interface SettingsMenuProps {
isOpen: boolean;
onClose: () => void;
@@ -44,6 +152,36 @@ export const SettingsMenu: React.FC = ({
// Local settings state for editing (doesn't affect main UI)
const [localSettings, setLocalSettings] = useState(settings);
+ const [ArgumentsDescriptor, setArgumentsDescriptor] = useState(null);
+ const [descriptorLoading, setDescriptorLoading] = useState(false);
+
+ // Fetch arguments.json descriptor when developer mode is enabled and settings menu is open
+ useEffect(() => {
+ if (!isOpen || !localSettings.devModeEnabled) {
+ setArgumentsDescriptor(null);
+ return;
+ }
+
+ const fetchDescriptor = async () => {
+ setDescriptorLoading(true);
+ try {
+ const descriptor = await gameClient.getGameArgumentsDescriptor();
+ if (Array.isArray(descriptor) && descriptor.every((item: any) => item && typeof item.key === "string" && typeof item.type === "string")) {
+ setArgumentsDescriptor(descriptor as ArgumentDescriptorItem[]);
+ } else {
+ setArgumentsDescriptor(null);
+ }
+ } catch (error) {
+ log.error("Failed to fetch arguments descriptor:", error);
+ setArgumentsDescriptor(null);
+ } finally {
+ setDescriptorLoading(false);
+ }
+ };
+
+ fetchDescriptor();
+ }, [isOpen, localSettings.devModeEnabled]);
+
// Update local settings when menu opens or settings prop changes
useEffect(() => {
if (isOpen) {
@@ -309,6 +447,104 @@ export const SettingsMenu: React.FC = ({
jeu.
+
+
+
+ {descriptorLoading ? (
+
+ Chargement des arguments...
+
+ ) : ArgumentsDescriptor ? (
+
+
+ {ArgumentsDescriptor.map((item) => {
+ const parsedArgs = parseGameArgs(localSettings.devGameArgs);
+ const currentValue = parsedArgs[item.key];
+
+ return (
+
+
+ {item.key}
+ {item.type === "boolean" ? (
+ {
+ const updated = updateDescriptorArg(
+ localSettings.devGameArgs,
+ ArgumentsDescriptor,
+ item.key,
+ checked
+ );
+ updateSetting("devGameArgs", updated);
+ }}
+ />
+ ) : (
+ {
+ const updated = updateDescriptorArg(
+ localSettings.devGameArgs,
+ ArgumentsDescriptor,
+ item.key,
+ e.target.value
+ );
+ updateSetting("devGameArgs", updated);
+ }}
+ className="w-1/2 px-2 py-1 bg-black/40 border border-white/20 rounded text-white text-sm"
+ />
+ )}
+
+
{item.description}
+
+ );
+ })}
+
+
+
+
+
+
+ ) : (
+ <>
+
)}
diff --git a/packages/renderer/src/types/index.ts b/packages/renderer/src/types/index.ts
index 1e9cae3..5c7045a 100644
--- a/packages/renderer/src/types/index.ts
+++ b/packages/renderer/src/types/index.ts
@@ -35,6 +35,7 @@ export interface SettingsState {
// Developer mode
devModeEnabled: boolean;
devExtraJavaArgs: string;
+ devGameArgs: string;
devForceVersion: string;
devCdnEnvironment: "production" | "staging";
}
diff --git a/packages/renderer/src/vite-env.d.ts b/packages/renderer/src/vite-env.d.ts
index 75a2d5a..10b3c17 100644
--- a/packages/renderer/src/vite-env.d.ts
+++ b/packages/renderer/src/vite-env.d.ts
@@ -21,6 +21,7 @@ interface GameSettings {
gameRamAllocation: number;
devModeEnabled: boolean;
devExtraJavaArgs: string;
+ devGameArgs: string;
devForceVersion: string;
devCdnEnvironment: "production" | "staging";
}
From 0dee71622353c0d45a76e895f8af3d6b48f2622d Mon Sep 17 00:00:00 2001
From: Jordan Rey
Date: Thu, 27 Aug 2026 12:21:06 +0200
Subject: [PATCH 2/4] chore: lint and allow git in npmrc
---
.npmrc | 1 +
packages/main/src/index.ts | 14 ++--
packages/main/src/modules/GameClient.ts | 77 +++++++++---------
packages/main/src/modules/GameUpdater.ts | 32 ++++----
packages/main/src/services/SettingsManager.ts | 6 +-
.../renderer/src/components/SettingsMenu.tsx | 78 ++++++++++++++-----
6 files changed, 126 insertions(+), 82 deletions(-)
create mode 100644 .npmrc
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..7b1e8b3
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+allow-git=all
\ No newline at end of file
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts
index 9e309a2..e06f0dd 100644
--- a/packages/main/src/index.ts
+++ b/packages/main/src/index.ts
@@ -44,7 +44,7 @@ export async function initApp(initConfig: AppInitConfig) {
createWindowManagerModule({
initConfig,
openDevTools: import.meta.env.DEV,
- })
+ }),
)
.init(disallowMultipleAppInstance())
.init(terminateAppOnLastWindowClose())
@@ -59,16 +59,18 @@ export async function initApp(initConfig: AppInitConfig) {
.init(
allowInternalOrigins(
new Set(
- initConfig.renderer instanceof URL ? [initConfig.renderer.origin] : []
- )
- )
+ initConfig.renderer instanceof URL
+ ? [initConfig.renderer.origin]
+ : [],
+ ),
+ ),
)
.init(
allowExternalUrls(
initConfig.renderer instanceof URL
? ALLOWED_EXTERNAL_ORIGINS
- : new Set()
- )
+ : new Set(),
+ ),
);
await moduleRunner;
diff --git a/packages/main/src/modules/GameClient.ts b/packages/main/src/modules/GameClient.ts
index c5a9519..c17eb1a 100644
--- a/packages/main/src/modules/GameClient.ts
+++ b/packages/main/src/modules/GameClient.ts
@@ -1,12 +1,12 @@
-import {app, ipcMain} from "electron";
-import {join} from "path";
-import {chmodSync, existsSync, mkdirSync} from "fs";
-import {appendFile, chmod, readdir, readFile, stat} from "fs/promises";
-import {exec} from "child_process";
+import { app, ipcMain } from "electron";
+import { join } from "path";
+import { chmodSync, existsSync, mkdirSync } from "fs";
+import { appendFile, chmod, readdir, readFile, stat } from "fs/promises";
+import { exec } from "child_process";
import log from "electron-log";
-import {GameSettings, GameUpdater, ReplayFile} from "./GameUpdater.js";
-import type {AppModule} from "../AppModule.js";
-import type {ModuleContext} from "../ModuleContext.js";
+import { GameSettings, GameUpdater, ReplayFile } from "./GameUpdater.js";
+import type { AppModule } from "../AppModule.js";
+import type { ModuleContext } from "../ModuleContext.js";
export class GameClient implements AppModule {
private gameUpdater: GameUpdater | null = null;
@@ -30,14 +30,14 @@ export class GameClient implements AppModule {
// Register GameClient-specific IPC handlers
ipcMain.handle("gameClient:launchGame", () => this.launchGame());
ipcMain.handle("gameClient:openReplaysFolder", () =>
- this.openReplaysFolder()
+ this.openReplaysFolder(),
);
ipcMain.handle("gameClient:listReplays", () => this.listReplays());
ipcMain.handle("gameClient:launchReplayOffline", (_e, path) =>
- this.launchReplayOffline(path)
+ this.launchReplayOffline(path),
);
ipcMain.handle("gameClient:getGameArgumentsDescriptor", () =>
- this.getGameArgumentsDescriptor()
+ this.getGameArgumentsDescriptor(),
);
}
@@ -77,15 +77,18 @@ export class GameClient implements AppModule {
await this.startJavaProcess({
mainClass: "com.ankamagames.dofusarena.client.DofusArenaClient",
settings: this.currentSettings || undefined,
- extraArgs: this.currentSettings?.devExtraJavaArgs.split(" ").map(arg => `-${arg}`) ?? [],
+ extraArgs:
+ this.currentSettings?.devExtraJavaArgs
+ .split(" ")
+ .map((arg) => `-${arg}`) ?? [],
});
}
async openReplaysFolder(): Promise {
- const {shell} = await import("electron");
+ const { shell } = await import("electron");
const replaysPath = join(this.gameClientPath, "game", "replays");
- mkdirSync(replaysPath, {recursive: true});
+ mkdirSync(replaysPath, { recursive: true });
try {
await shell.openPath(replaysPath);
@@ -93,15 +96,15 @@ export class GameClient implements AppModule {
throw new Error(
`Failed to open replays folder: ${
error instanceof Error ? error.message : "Unknown error"
- }`
+ }`,
);
}
}
async listReplays(): Promise {
const replaysPath = join(this.gameClientPath, "game", "replays");
- const {readdir} = await import("fs/promises");
- mkdirSync(replaysPath, {recursive: true});
+ const { readdir } = await import("fs/promises");
+ mkdirSync(replaysPath, { recursive: true });
try {
const files = await readdir(replaysPath);
@@ -152,7 +155,7 @@ export class GameClient implements AppModule {
const gameConfigPath = join(
this.gameClientPath,
"game",
- "config.properties"
+ "config.properties",
);
try {
@@ -173,11 +176,11 @@ export class GameClient implements AppModule {
log.info("Adding dev mode proxy settings to config.properties");
await appendFile(
gameConfigPath,
- "\nproxyGroup_2=Localhost\nproxyAddresses_2=localhost:5555\n"
+ "\nproxyGroup_2=Localhost\nproxyAddresses_2=localhost:5555\n",
);
await appendFile(
gameConfigPath,
- "\nproxyGroup_3=Staging\nproxyAddresses_3=minuit-staging.arenareturns.com:6666\n"
+ "\nproxyGroup_3=Staging\nproxyAddresses_3=minuit-staging.arenareturns.com:6666\n",
);
} catch (error) {
log.error("Failed to update config.properties for dev mode:", error);
@@ -190,7 +193,7 @@ export class GameClient implements AppModule {
settings?: GameSettings;
extraArgs?: string[];
}): Promise {
- const {mainClass, settings, extraArgs = []} = options;
+ const { mainClass, settings, extraArgs = [] } = options;
const gameDir = join(this.gameClientPath, "game");
const libDir = join(this.gameClientPath, "lib");
const jreDir = join(this.gameClientPath, "jre");
@@ -208,7 +211,7 @@ export class GameClient implements AppModule {
.join(
process.platform === "win32" || process.platform === "darwin"
? ";"
- : ":"
+ : ":",
);
const coreJarPath = join(gameDir, "core.jar");
// FIXME: Gigahack since darwin relies on wine
@@ -286,7 +289,7 @@ export class GameClient implements AppModule {
...settings.devExtraJavaArgs
.split(" ")
.map((arg) => arg.trim())
- .filter((arg) => arg.length > 0)
+ .filter((arg) => arg.length > 0),
);
}
@@ -343,17 +346,17 @@ export class GameClient implements AppModule {
private async launchJavaProcessWindows(
javaExecutable: string,
args: string[],
- cwd: string
+ cwd: string,
): Promise {
return new Promise((resolve, reject) => {
const child = exec(
`"${javaExecutable}" ${args.join(" ")}`,
- {cwd},
+ { cwd },
(error) => {
if (error && !error.killed) {
log.error("Java process error:", error);
}
- }
+ },
);
if (child.pid) {
resolve();
@@ -366,7 +369,7 @@ export class GameClient implements AppModule {
private async launchJavaProcessLinux(
javaExecutable: string,
args: string[],
- cwd: string
+ cwd: string,
): Promise {
return new Promise((resolve, reject) => {
try {
@@ -374,17 +377,17 @@ export class GameClient implements AppModule {
} catch (error) {
log.warn(
`Failed to set permissions on Java executable ${javaExecutable}:`,
- error
+ error,
);
}
const child = exec(
`"${javaExecutable}" ${args.join(" ")}`,
- {cwd},
+ { cwd },
(error) => {
if (error && !error.killed) {
log.error("Java process error:", error);
}
- }
+ },
);
if (child.pid) {
resolve();
@@ -397,7 +400,7 @@ export class GameClient implements AppModule {
private async launchJavaProcessDarwin(
javaExecutable: string,
args: string[],
- cwd: string
+ cwd: string,
): Promise {
return new Promise((resolve, reject) => {
try {
@@ -405,12 +408,12 @@ export class GameClient implements AppModule {
} catch (error) {
log.warn(
`Failed to set permissions on Java executable ${javaExecutable}:`,
- error
+ error,
);
}
// Ensure we have the full system PATH for finding wine
- const env = {...process.env};
+ const env = { ...process.env };
if (!env.PATH?.includes("/opt/homebrew/bin")) {
env.PATH = `${
env.PATH || ""
@@ -419,12 +422,12 @@ export class GameClient implements AppModule {
const child = exec(
`wine "${javaExecutable}" ${args.join(" ")}`,
- {cwd, env},
+ { cwd, env },
(error) => {
if (error && !error.killed) {
log.error("Java process error:", error);
}
- }
+ },
);
if (child.pid) {
resolve();
@@ -438,11 +441,11 @@ export class GameClient implements AppModule {
try {
let schemaPath = join(this.gameClientPath, "game", "arguments.json");
if (existsSync(schemaPath)) {
- log.info("Loading arguments descriptor from ", schemaPath)
+ log.info("Loading arguments descriptor from ", schemaPath);
const content = await readFile(schemaPath, "utf-8");
return JSON.parse(content);
}
- log.info("Arguments descriptor not found")
+ log.info("Arguments descriptor not found");
return null;
} catch (error) {
log.error("Failed to load arguments.json:", error);
diff --git a/packages/main/src/modules/GameUpdater.ts b/packages/main/src/modules/GameUpdater.ts
index cd601ec..d2c4b7f 100644
--- a/packages/main/src/modules/GameUpdater.ts
+++ b/packages/main/src/modules/GameUpdater.ts
@@ -119,12 +119,12 @@ export class GameUpdater implements AppModule {
ipcMain.handle("gameUpdater:checkForUpdates", () => this.checkForUpdates());
ipcMain.handle("gameUpdater:startDownload", () => this.startDownload());
ipcMain.handle("gameUpdater:getDownloadProgress", () =>
- this.getDownloadProgress()
+ this.getDownloadProgress(),
);
ipcMain.handle("gameUpdater:cancelDownload", () => this.cancelDownload());
ipcMain.handle("gameUpdater:repairClient", () => this.repairClient());
ipcMain.handle("gameUpdater:openGameDirectory", () =>
- this.openGameDirectory()
+ this.openGameDirectory(),
);
}
@@ -173,7 +173,7 @@ export class GameUpdater implements AppModule {
if (previousSettings) {
if (previousSettings.devCdnEnvironment !== settings.devCdnEnvironment) {
log.debug(
- `CDN environment changed from ${previousSettings.devCdnEnvironment} to ${settings.devCdnEnvironment}, notifying UI to refresh status`
+ `CDN environment changed from ${previousSettings.devCdnEnvironment} to ${settings.devCdnEnvironment}, notifying UI to refresh status`,
);
this.notifyRenderer("status-changed", {});
@@ -226,7 +226,7 @@ export class GameUpdater implements AppModule {
if (!response.ok) {
throw new Error(
- `Failed to fetch version info: ${response.status} ${response.statusText}`
+ `Failed to fetch version info: ${response.status} ${response.statusText}`,
);
}
@@ -251,7 +251,7 @@ export class GameUpdater implements AppModule {
throw new Error(
`Failed to update local version: ${
error instanceof Error ? error.message : "Unknown error"
- }`
+ }`,
);
}
}
@@ -277,12 +277,12 @@ export class GameUpdater implements AppModule {
const remoteVersion = await this.getRemoteVersion();
const manifestResponse = await fetch(
- `${this.cdnUrl}/versions/${remoteVersion}.json`
+ `${this.cdnUrl}/versions/${remoteVersion}.json`,
);
if (!manifestResponse.ok) {
throw new Error(
- `Failed to fetch version manifest: ${manifestResponse.status}`
+ `Failed to fetch version manifest: ${manifestResponse.status}`,
);
}
@@ -335,12 +335,12 @@ export class GameUpdater implements AppModule {
const remoteVersion = await this.getRemoteVersion();
const manifestResponse = await fetch(
- `${this.cdnUrl}/versions/${remoteVersion}.json`
+ `${this.cdnUrl}/versions/${remoteVersion}.json`,
);
if (!manifestResponse.ok) {
throw new Error(
- `Failed to fetch version manifest: ${manifestResponse.status}`
+ `Failed to fetch version manifest: ${manifestResponse.status}`,
);
}
@@ -373,7 +373,7 @@ export class GameUpdater implements AppModule {
throw new Error(
`Failed to open game directory: ${
error instanceof Error ? error.message : "Unknown error"
- }`
+ }`,
);
}
}
@@ -406,7 +406,7 @@ export class GameUpdater implements AppModule {
private async checkFiles(
files: FileManifest[],
- forceCheck = false
+ forceCheck = false,
): Promise {
if (!existsSync(this.gameClientPath)) {
mkdirSync(this.gameClientPath, { recursive: true });
@@ -492,7 +492,7 @@ export class GameUpdater implements AppModule {
const downloadPromises = files.map((file) =>
queue.add(() => this.downloadAndSaveFile(file), {
priority: 1,
- })
+ }),
);
try {
@@ -536,7 +536,7 @@ export class GameUpdater implements AppModule {
const response = await fetch(fileUrl);
if (!response.ok) {
throw new Error(
- `Failed to download file: ${response.status} ${response.statusText}`
+ `Failed to download file: ${response.status} ${response.statusText}`,
);
}
@@ -549,7 +549,7 @@ export class GameUpdater implements AppModule {
const dir = join(
this.gameClientPath,
- file.path.split("/").slice(0, -1).join("/")
+ file.path.split("/").slice(0, -1).join("/"),
);
if (dir && !existsSync(dir)) {
mkdirSync(dir, { recursive: true });
@@ -570,7 +570,7 @@ export class GameUpdater implements AppModule {
throw new Error(
`Failed to download file ${file.path}: ${
error instanceof Error ? error.message : "Unknown error"
- }`
+ }`,
);
}
}
@@ -653,7 +653,7 @@ export class GameUpdater implements AppModule {
private async getAllLocalFiles(
dirPath: string = this.gameClientPath,
relativeTo: string = this.gameClientPath,
- files: string[] = []
+ files: string[] = [],
): Promise {
try {
const entries = await readdir(dirPath);
diff --git a/packages/main/src/services/SettingsManager.ts b/packages/main/src/services/SettingsManager.ts
index d89bd1a..f91036b 100644
--- a/packages/main/src/services/SettingsManager.ts
+++ b/packages/main/src/services/SettingsManager.ts
@@ -67,7 +67,7 @@ export class SettingsManager {
await writeFile(
this.settingsPath,
JSON.stringify(settings, null, 2),
- "utf-8"
+ "utf-8",
);
log.info("Settings saved successfully:", settings);
@@ -82,7 +82,7 @@ export class SettingsManager {
throw new Error(
`Failed to save settings: ${
error instanceof Error ? error.message : "Unknown error"
- }`
+ }`,
);
}
}
@@ -120,7 +120,7 @@ export class SettingsManager {
*/
private notifySettingsChange(settings: GameSettings): void {
log.info(
- `Notifying ${this.changeCallbacks.size} modules of settings change`
+ `Notifying ${this.changeCallbacks.size} modules of settings change`,
);
for (const callback of this.changeCallbacks) {
diff --git a/packages/renderer/src/components/SettingsMenu.tsx b/packages/renderer/src/components/SettingsMenu.tsx
index c53c33b..482ad48 100644
--- a/packages/renderer/src/components/SettingsMenu.tsx
+++ b/packages/renderer/src/components/SettingsMenu.tsx
@@ -16,7 +16,11 @@ import {
Code,
AlertTriangle,
} from "lucide-react";
-import { SimpleSlider, SimpleSelect, SimpleSwitch } from "./common/FormControls";
+import {
+ SimpleSlider,
+ SimpleSelect,
+ SimpleSwitch,
+} from "./common/FormControls";
import type { SettingsState } from "@/types";
import { gameClient, gameUpdater, system } from "@app/preload";
import { useGameStateContext } from "@/contexts/GameStateContext";
@@ -28,7 +32,9 @@ interface ArgumentDescriptorItem {
type: "boolean" | "string" | "number";
}
-const parseGameArgs = (argsString: string): Record => {
+const parseGameArgs = (
+ argsString: string,
+): Record => {
const parsed: Record = {};
if (!argsString) return parsed;
argsString.split(/\s+/).forEach((part) => {
@@ -47,7 +53,7 @@ const updateDescriptorArg = (
currentArgsStr: string,
descriptor: ArgumentDescriptorItem[],
keyToUpdate: string,
- newValue: string | boolean
+ newValue: string | boolean,
): string => {
const parsed = parseGameArgs(currentArgsStr);
const descriptorKeys = new Set(descriptor.map((s) => s.key));
@@ -88,7 +94,7 @@ const updateDescriptorArg = (
const updateCustomArgs = (
currentArgsStr: string,
descriptor: ArgumentDescriptorItem[],
- newCustomStr: string
+ newCustomStr: string,
): string => {
const parsed = parseGameArgs(currentArgsStr);
const descriptorKeys = new Set(descriptor.map((s) => s.key));
@@ -112,7 +118,10 @@ const updateCustomArgs = (
return finalParts.join(" ");
};
-const getCustomArgsString = (currentArgsStr: string, descriptor: ArgumentDescriptorItem[]): string => {
+const getCustomArgsString = (
+ currentArgsStr: string,
+ descriptor: ArgumentDescriptorItem[],
+): string => {
const parsed = parseGameArgs(currentArgsStr);
const descriptorKeys = new Set(descriptor.map((s) => s.key));
const customParts: string[] = [];
@@ -152,7 +161,9 @@ export const SettingsMenu: React.FC = ({
// Local settings state for editing (doesn't affect main UI)
const [localSettings, setLocalSettings] = useState(settings);
- const [ArgumentsDescriptor, setArgumentsDescriptor] = useState(null);
+ const [ArgumentsDescriptor, setArgumentsDescriptor] = useState<
+ ArgumentDescriptorItem[] | null
+ >(null);
const [descriptorLoading, setDescriptorLoading] = useState(false);
// Fetch arguments.json descriptor when developer mode is enabled and settings menu is open
@@ -166,7 +177,15 @@ export const SettingsMenu: React.FC = ({
setDescriptorLoading(true);
try {
const descriptor = await gameClient.getGameArgumentsDescriptor();
- if (Array.isArray(descriptor) && descriptor.every((item: any) => item && typeof item.key === "string" && typeof item.type === "string")) {
+ if (
+ Array.isArray(descriptor) &&
+ descriptor.every(
+ (item: any) =>
+ item &&
+ typeof item.key === "string" &&
+ typeof item.type === "string",
+ )
+ ) {
setArgumentsDescriptor(descriptor as ArgumentDescriptorItem[]);
} else {
setArgumentsDescriptor(null);
@@ -405,7 +424,7 @@ export const SettingsMenu: React.FC = ({
onChange={(value) =>
updateSetting(
"devCdnEnvironment",
- value as "production" | "staging"
+ value as "production" | "staging",
)
}
options={[
@@ -460,31 +479,44 @@ export const SettingsMenu: React.FC = ({
{ArgumentsDescriptor.map((item) => {
- const parsedArgs = parseGameArgs(localSettings.devGameArgs);
+ const parsedArgs = parseGameArgs(
+ localSettings.devGameArgs,
+ );
const currentValue = parsedArgs[item.key];
return (
-
+
- {item.key}
+
+ {item.key}
+
{item.type === "boolean" ? (
{
const updated = updateDescriptorArg(
localSettings.devGameArgs,
ArgumentsDescriptor,
item.key,
- checked
+ checked,
);
updateSetting("devGameArgs", updated);
}}
/>
) : (
= ({
localSettings.devGameArgs,
ArgumentsDescriptor,
item.key,
- e.target.value
+ e.target.value,
);
updateSetting("devGameArgs", updated);
}}
@@ -501,7 +533,9 @@ export const SettingsMenu: React.FC = ({
/>
)}
-
{item.description}
+
+ {item.description}
+
);
})}
@@ -512,12 +546,15 @@ export const SettingsMenu: React.FC
= ({
Autres arguments du jeu
From 45d8734265fd0fa74680a9af558e53d4ebb29b15 Mon Sep 17 00:00:00 2001
From: Jydet
Date: Fri, 28 Aug 2026 00:39:52 +0200
Subject: [PATCH 3/4] feat: edit the settings of the game in the dev tab
---
packages/main/src/modules/GameClient.ts | 203 ++++++++++--------
.../renderer/src/components/SettingsMenu.tsx | 176 ++++++++++-----
2 files changed, 239 insertions(+), 140 deletions(-)
diff --git a/packages/main/src/modules/GameClient.ts b/packages/main/src/modules/GameClient.ts
index c17eb1a..fee3ba1 100644
--- a/packages/main/src/modules/GameClient.ts
+++ b/packages/main/src/modules/GameClient.ts
@@ -2,12 +2,56 @@ import { app, ipcMain } from "electron";
import { join } from "path";
import { chmodSync, existsSync, mkdirSync } from "fs";
import { appendFile, chmod, readdir, readFile, stat } from "fs/promises";
-import { exec } from "child_process";
+import { spawn } from "child_process";
import log from "electron-log";
import { GameSettings, GameUpdater, ReplayFile } from "./GameUpdater.js";
import type { AppModule } from "../AppModule.js";
import type { ModuleContext } from "../ModuleContext.js";
+const splitCommandLineArgs = (argsString: string): string[] => {
+ const args: string[] = [];
+ let current = "";
+ let quote: '"' | "'" | null = null;
+ let hasContent = false;
+
+ for (let index = 0; index < argsString.length; index += 1) {
+ const character = argsString[index];
+
+ if (quote) {
+ if (character === quote) {
+ quote = null;
+ } else if (
+ character === "\\" &&
+ argsString[index + 1] !== undefined &&
+ (argsString[index + 1] === quote || argsString[index + 1] === "\\")
+ ) {
+ current += argsString[index + 1];
+ index += 1;
+ } else {
+ current += character;
+ }
+ continue;
+ }
+
+ if (character === '"' || character === "'") {
+ quote = character;
+ hasContent = true;
+ } else if (/\s/.test(character)) {
+ if (hasContent) {
+ args.push(current);
+ current = "";
+ hasContent = false;
+ }
+ } else {
+ current += character;
+ hasContent = true;
+ }
+ }
+
+ if (hasContent) args.push(current);
+ return args;
+};
+
export class GameClient implements AppModule {
private gameUpdater: GameUpdater | null = null;
private gameClientPath: string;
@@ -77,10 +121,6 @@ export class GameClient implements AppModule {
await this.startJavaProcess({
mainClass: "com.ankamagames.dofusarena.client.DofusArenaClient",
settings: this.currentSettings || undefined,
- extraArgs:
- this.currentSettings?.devExtraJavaArgs
- .split(" ")
- .map((arg) => `-${arg}`) ?? [],
});
}
@@ -194,6 +234,14 @@ export class GameClient implements AppModule {
extraArgs?: string[];
}): Promise {
const { mainClass, settings, extraArgs = [] } = options;
+ const fullGameArgs = [
+ ...(this.currentSettings?.devGameArgs
+ ? splitCommandLineArgs(this.currentSettings.devGameArgs).map(
+ (arg) => `-${arg}`,
+ )
+ : []),
+ ...extraArgs,
+ ];
const gameDir = join(this.gameClientPath, "game");
const libDir = join(this.gameClientPath, "lib");
const jreDir = join(this.gameClientPath, "jre");
@@ -273,27 +321,26 @@ export class GameClient implements AppModule {
"-XX:+UseG1GC",
"-XX:G1NewSizePercent=20",
"-XX:G1ReservePercent=20",
+ "-XX:ReservedCodeCacheSize=256m",
"--add-exports",
"java.desktop/sun.awt=ALL-UNNAMED",
+ "--enable-native-access=ALL-UNNAMED",
"-Djava.net.preferIPv4Stack=true",
- "-Dsun.awt.noerasebackground=true",
- "-Dsun.java2d.noddraw=true",
- "-Dsun.java2d.dpiaware=false",
- "-Dsun.java2d.uiScale=1.0",
- "-Djogl.disable.openglarbcontext",
- `-Djava.library.path="${nativesPath}"`,
+ "-Djogl.disable.openglarbcontext=1",
+ `-Djava.library.path=${nativesPath}`,
];
+ if (process.platform === "darwin") {
+ javaArgs.push("-XstartOnFirstThread");
+ }
+
if (settings?.devModeEnabled && settings?.devExtraJavaArgs) {
javaArgs.push(
- ...settings.devExtraJavaArgs
- .split(" ")
- .map((arg) => arg.trim())
- .filter((arg) => arg.length > 0),
+ ...splitCommandLineArgs(settings.devExtraJavaArgs),
);
}
- javaArgs.push("-cp", `"${fullClasspath}"`, mainClass, ...extraArgs);
+ javaArgs.push("-cp", fullClasspath, mainClass, ...fullGameArgs);
switch (process.platform) {
case "win32":
@@ -348,22 +395,7 @@ export class GameClient implements AppModule {
args: string[],
cwd: string,
): Promise {
- return new Promise((resolve, reject) => {
- const child = exec(
- `"${javaExecutable}" ${args.join(" ")}`,
- { cwd },
- (error) => {
- if (error && !error.killed) {
- log.error("Java process error:", error);
- }
- },
- );
- if (child.pid) {
- resolve();
- } else {
- reject(new Error("Failed to start Java process"));
- }
- });
+ return this.spawnJavaProcess(javaExecutable, args, cwd);
}
private async launchJavaProcessLinux(
@@ -371,30 +403,16 @@ export class GameClient implements AppModule {
args: string[],
cwd: string,
): Promise {
- return new Promise((resolve, reject) => {
- try {
- chmodSync(javaExecutable, 0o755);
- } catch (error) {
- log.warn(
- `Failed to set permissions on Java executable ${javaExecutable}:`,
- error,
- );
- }
- const child = exec(
- `"${javaExecutable}" ${args.join(" ")}`,
- { cwd },
- (error) => {
- if (error && !error.killed) {
- log.error("Java process error:", error);
- }
- },
+ try {
+ chmodSync(javaExecutable, 0o755);
+ } catch (error) {
+ log.warn(
+ `Failed to set permissions on Java executable ${javaExecutable}:`,
+ error,
);
- if (child.pid) {
- resolve();
- } else {
- reject(new Error("Failed to start Java process"));
- }
- });
+ }
+
+ return this.spawnJavaProcess(javaExecutable, args, cwd);
}
private async launchJavaProcessDarwin(
@@ -402,53 +420,60 @@ export class GameClient implements AppModule {
args: string[],
cwd: string,
): Promise {
- return new Promise((resolve, reject) => {
- try {
- chmodSync(javaExecutable, 0o755);
- } catch (error) {
- log.warn(
- `Failed to set permissions on Java executable ${javaExecutable}:`,
- error,
- );
- }
+ try {
+ chmodSync(javaExecutable, 0o755);
+ } catch (error) {
+ log.warn(
+ `Failed to set permissions on Java executable ${javaExecutable}:`,
+ error,
+ );
+ }
- // Ensure we have the full system PATH for finding wine
- const env = { ...process.env };
- if (!env.PATH?.includes("/opt/homebrew/bin")) {
- env.PATH = `${
- env.PATH || ""
- }:/opt/homebrew/bin:/usr/local/bin:/opt/local/bin`;
- }
+ // Ensure we have the full system PATH for finding wine
+ const env = { ...process.env };
+ if (!env.PATH?.includes("/opt/homebrew/bin")) {
+ env.PATH = `${
+ env.PATH || ""
+ }:/opt/homebrew/bin:/usr/local/bin:/opt/local/bin`;
+ }
- const child = exec(
- `wine "${javaExecutable}" ${args.join(" ")}`,
- { cwd, env },
- (error) => {
- if (error && !error.killed) {
- log.error("Java process error:", error);
- }
- },
- );
- if (child.pid) {
- resolve();
- } else {
- reject(new Error("Failed to start Java process"));
- }
+ return this.spawnJavaProcess("wine", [javaExecutable, ...args], cwd, env);
+ }
+
+ private spawnJavaProcess(
+ executable: string,
+ args: string[],
+ cwd: string,
+ env: NodeJS.ProcessEnv = process.env,
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ const child = spawn(executable, args, { cwd, env, stdio: "ignore" });
+
+ child.once("error", reject);
+ child.once("spawn", resolve);
+ child.once("exit", (code, signal) => {
+ if (code !== 0) {
+ log.error(
+ `Java process exited with code ${code ?? "unknown"}` +
+ (signal ? ` (signal: ${signal})` : ""),
+ );
+ }
+ });
});
}
async getGameArgumentsDescriptor(): Promise {
try {
- let schemaPath = join(this.gameClientPath, "game", "arguments.json");
+ let schemaPath = join(this.gameClientPath, "game", "args");
if (existsSync(schemaPath)) {
- log.info("Loading arguments descriptor from ", schemaPath);
+ log.info("Loading arguments descriptor from", schemaPath);
const content = await readFile(schemaPath, "utf-8");
return JSON.parse(content);
}
- log.info("Arguments descriptor not found");
+ log.info("Arguments descriptor not found at", schemaPath);
return null;
} catch (error) {
- log.error("Failed to load arguments.json:", error);
+ log.error("Failed to load arguments descriptor:", error);
return null;
}
}
diff --git a/packages/renderer/src/components/SettingsMenu.tsx b/packages/renderer/src/components/SettingsMenu.tsx
index 482ad48..f79d678 100644
--- a/packages/renderer/src/components/SettingsMenu.tsx
+++ b/packages/renderer/src/components/SettingsMenu.tsx
@@ -32,12 +32,82 @@ interface ArgumentDescriptorItem {
type: "boolean" | "string" | "number";
}
+type GameArgValue = string | true;
+
+const isArgumentDescriptorItem = (
+ item: unknown,
+): item is ArgumentDescriptorItem => {
+ if (!item || typeof item !== "object") return false;
+
+ const candidate = item as Record;
+ return (
+ typeof candidate.key === "string" &&
+ typeof candidate.description === "string" &&
+ (candidate.type === "boolean" ||
+ candidate.type === "string" ||
+ candidate.type === "number")
+ );
+};
+
+const splitCommandLineArgs = (argsString: string): string[] => {
+ const args: string[] = [];
+ let current = "";
+ let quote: '"' | "'" | null = null;
+ let hasContent = false;
+
+ for (let index = 0; index < argsString.length; index += 1) {
+ const character = argsString[index];
+
+ if (quote) {
+ if (character === quote) {
+ quote = null;
+ } else if (
+ character === "\\" &&
+ argsString[index + 1] !== undefined &&
+ (argsString[index + 1] === quote || argsString[index + 1] === "\\")
+ ) {
+ current += argsString[index + 1];
+ index += 1;
+ } else {
+ current += character;
+ }
+ continue;
+ }
+
+ if (character === '"' || character === "'") {
+ quote = character;
+ hasContent = true;
+ } else if (/\s/.test(character)) {
+ if (hasContent) {
+ args.push(current);
+ current = "";
+ hasContent = false;
+ }
+ } else {
+ current += character;
+ hasContent = true;
+ }
+ }
+
+ if (hasContent) args.push(current);
+ return args;
+};
+
+const serializeGameArg = (key: string, value: GameArgValue): string => {
+ if (value === true) return key;
+
+ const serializedValue = /\s|["']/.test(value)
+ ? JSON.stringify(value)
+ : value;
+ return `${key}=${serializedValue}`;
+};
+
const parseGameArgs = (
argsString: string,
-): Record => {
- const parsed: Record = {};
+): Record => {
+ const parsed: Record = {};
if (!argsString) return parsed;
- argsString.split(/\s+/).forEach((part) => {
+ splitCommandLineArgs(argsString).forEach((part) => {
if (!part) return;
const [key, ...valParts] = part.split("=");
if (valParts.length > 0) {
@@ -69,17 +139,11 @@ const updateDescriptorArg = (
Object.entries(parsed).forEach(([key, val]) => {
if (descriptorKeys.has(key)) {
- if (val === true) {
- descriptorParts.push(key);
- } else if (val !== false && val !== "" && val !== undefined) {
- descriptorParts.push(`${key}=${val}`);
+ if (val !== "") {
+ descriptorParts.push(serializeGameArg(key, val));
}
} else {
- if (val === true) {
- customParts.push(key);
- } else {
- customParts.push(`${key}=${val}`);
- }
+ customParts.push(serializeGameArg(key, val));
}
});
@@ -102,20 +166,16 @@ const updateCustomArgs = (
const descriptorParts: string[] = [];
Object.entries(parsed).forEach(([key, val]) => {
if (descriptorKeys.has(key)) {
- if (val === true) {
- descriptorParts.push(key);
- } else if (val !== false && val !== "" && val !== undefined) {
- descriptorParts.push(`${key}=${val}`);
+ if (val !== "") {
+ descriptorParts.push(serializeGameArg(key, val));
}
}
});
- const finalParts = [...descriptorParts];
- if (newCustomStr.trim()) {
- finalParts.push(newCustomStr.trim());
- }
-
- return finalParts.join(" ");
+ const customArgs = newCustomStr.trim();
+ return customArgs
+ ? [...descriptorParts, customArgs].join(" ")
+ : descriptorParts.join(" ");
};
const getCustomArgsString = (
@@ -128,11 +188,7 @@ const getCustomArgsString = (
Object.entries(parsed).forEach(([key, val]) => {
if (!descriptorKeys.has(key)) {
- if (val === true) {
- customParts.push(key);
- } else {
- customParts.push(`${key}=${val}`);
- }
+ customParts.push(serializeGameArg(key, val));
}
});
@@ -165,6 +221,8 @@ export const SettingsMenu: React.FC = ({
ArgumentDescriptorItem[] | null
>(null);
const [descriptorLoading, setDescriptorLoading] = useState(false);
+ const [customGameArgsInput, setCustomGameArgsInput] = useState("");
+ const [rawGameArgsInput, setRawGameArgsInput] = useState("");
// Fetch arguments.json descriptor when developer mode is enabled and settings menu is open
useEffect(() => {
@@ -179,14 +237,9 @@ export const SettingsMenu: React.FC = ({
const descriptor = await gameClient.getGameArgumentsDescriptor();
if (
Array.isArray(descriptor) &&
- descriptor.every(
- (item: any) =>
- item &&
- typeof item.key === "string" &&
- typeof item.type === "string",
- )
+ descriptor.every(isArgumentDescriptorItem)
) {
- setArgumentsDescriptor(descriptor as ArgumentDescriptorItem[]);
+ setArgumentsDescriptor(descriptor);
} else {
setArgumentsDescriptor(null);
}
@@ -209,6 +262,20 @@ export const SettingsMenu: React.FC = ({
}
}, [isOpen, settings]);
+ useEffect(() => {
+ if (!ArgumentsDescriptor) return;
+
+ setCustomGameArgsInput(
+ getCustomArgsString(localSettings.devGameArgs, ArgumentsDescriptor),
+ );
+ }, [ArgumentsDescriptor, localSettings.devGameArgs]);
+
+ useEffect(() => {
+ if (ArgumentsDescriptor) return;
+
+ setRawGameArgsInput(localSettings.devGameArgs);
+ }, [ArgumentsDescriptor, localSettings.devGameArgs]);
+
// Fetch app version and log directory when settings menu opens
useEffect(() => {
if (!isOpen) return;
@@ -238,6 +305,19 @@ export const SettingsMenu: React.FC = ({
});
};
+ const handleCustomGameArgsBlur = () => {
+ if (!ArgumentsDescriptor) return;
+
+ setLocalSettings((currentSettings) => ({
+ ...currentSettings,
+ devGameArgs: updateCustomArgs(
+ currentSettings.devGameArgs,
+ ArgumentsDescriptor,
+ customGameArgsInput,
+ ),
+ }));
+ };
+
const handleSave = async () => {
setIsSaving(true);
setSaveError(null);
@@ -546,20 +626,13 @@ export const SettingsMenu: React.FC = ({
Autres arguments du jeu