From 1e7234d12d2e918d9106d580d8b152855bf5f5da Mon Sep 17 00:00:00 2001
From: Jydet
Date: Sat, 11 Jul 2026 10:20:20 +0200
Subject: [PATCH] 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";
}