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} +
+ ); + })} +
+ +
+ +