diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 2866e46..39c0954 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -84,6 +84,7 @@ export function Toolbar({ showAddPanel, onToggleAddPanel }: ToolbarProps) { language, // 调试设置 tcpCompatMode, + gamePath, } = useAppStore(); const [isStarting, setIsStarting] = useState(false); @@ -283,9 +284,10 @@ export function Toolbar({ showAddPanel, onToggleAddPanel }: ToolbarProps) { schedulePolicyName?: string; /** 自动连接阶段变化回调(用于 UI 状态更新) */ onPhaseChange?: (phase: AutoConnectPhase) => void; + hasRetriedAfterLaunch?: boolean; }, ): Promise => { - const { schedulePolicyName, onPhaseChange } = options || {}; + const { schedulePolicyName, onPhaseChange, hasRetriedAfterLaunch } = options || {}; const targetId = targetInstance.id; const targetTasks = targetInstance.selectedTasks || []; @@ -442,6 +444,61 @@ export function Toolbar({ showAddPanel, onToggleAddPanel }: ToolbarProps) { // 如果未连接,尝试自动连接 if (!isTargetConnected && controller) { const controllerType = controller.type; + const WINDOW_RETRY_DELAY_MS = 60_000; + + const tryLaunchGameAndRetry = async ( + missingWindowName?: string, + ): Promise => { + if (controllerType !== 'Win32' && controllerType !== 'Gamepad') { + return null; + } + if (hasRetriedAfterLaunch) { + return null; + } + + const path = gamePath.trim(); + if (!path) { + log.warn(`实例 ${targetInstance.name}: 未设置游戏路径,跳过自动拉起`); + addLog(targetId, { + type: 'warning', + message: t('action.gamePathNotSet'), + }); + return null; + } + + log.info(`实例 ${targetInstance.name}: 未找到窗口,尝试启动游戏: ${path}`); + addLog(targetId, { + type: 'info', + message: t('action.autoLaunchGameStarting', { path }), + }); + + try { + await maaService.runAction(path, '', basePath, false); + } catch (launchErr) { + log.error(`实例 ${targetInstance.name}: 启动游戏失败:`, launchErr); + addLog(targetId, { + type: 'error', + message: t('action.autoLaunchGameFailed', { error: String(launchErr) }), + }); + return false; + } + + addLog(targetId, { + type: 'info', + message: t('action.autoLaunchGameWaiting', { seconds: 60 }), + }); + await new Promise((resolve) => setTimeout(resolve, WINDOW_RETRY_DELAY_MS)); + + addLog(targetId, { + type: 'info', + message: t('action.autoLaunchGameRetrying', { name: missingWindowName || '' }), + }); + + return await startTasksForInstance(targetInstance, { + ...options, + hasRetriedAfterLaunch: true, + }); + }; await ensureMaaInitialized(); await maaService.createInstance(targetId).catch((err) => { @@ -485,6 +542,10 @@ export function Toolbar({ showAddPanel, onToggleAddPanel }: ToolbarProps) { const matchedWindow = windows.find((w) => w.window_name === savedDevice.windowName); if (!matchedWindow) { log.warn(`实例 ${targetInstance.name}: 未找到窗口 ${savedDevice.windowName}`); + const retryResult = await tryLaunchGameAndRetry(savedDevice.windowName); + if (retryResult !== null) { + return retryResult; + } return false; } if (controllerType === 'Win32') { @@ -545,6 +606,10 @@ export function Toolbar({ showAddPanel, onToggleAddPanel }: ToolbarProps) { const windows = await maaService.findWin32Windows(classRegex, windowRegex); if (windows.length === 0) { log.warn(`实例 ${targetInstance.name}: 未搜索到任何窗口`); + const retryResult = await tryLaunchGameAndRetry(); + if (retryResult !== null) { + return retryResult; + } addLog(targetId, { type: 'error', message: t('taskList.autoConnect.noWindowFound'), @@ -850,6 +915,7 @@ export function Toolbar({ showAddPanel, onToggleAddPanel }: ToolbarProps) { clearScheduleExecution, setShowAddTaskPanel, addLog, + gamePath, t, ], ); diff --git a/src/components/settings/GeneralSection.tsx b/src/components/settings/GeneralSection.tsx index 92239e6..4914133 100644 --- a/src/components/settings/GeneralSection.tsx +++ b/src/components/settings/GeneralSection.tsx @@ -9,6 +9,7 @@ import { Power, Play, Rocket, + Gamepad2, ChevronDown, Check, } from 'lucide-react'; @@ -16,7 +17,7 @@ import { import { useAppStore } from '@/stores/appStore'; import { defaultWindowSize } from '@/types/config'; import { isTauri } from '@/utils/paths'; -import { SwitchButton } from '@/components/FormControls'; +import { FileField, SwitchButton } from '@/components/FormControls'; import { FrameRateSelector } from '../FrameRateSelector'; export function GeneralSection() { @@ -36,6 +37,8 @@ export function GeneralSection() { autoRunOnLaunch, setAutoRunOnLaunch, autoStartRemovedInstanceName, + gamePath, + setGamePath, } = useAppStore(); // 开机自启动状态(直接从 Tauri 插件查询,不走 store) @@ -234,6 +237,27 @@ export function GeneralSection() { + {/* 游戏路径(用于未找到窗口时自动拉起游戏) */} +
+
+ +
+ {t('settings.gamePath')} +

{t('settings.gamePathHint')}

+
+
+ setGamePath(value.trim())} + placeholder={t('settings.gamePathPlaceholder')} + filters={[ + { name: 'Executable', extensions: ['exe', 'bat', 'cmd', 'ps1', 'sh'] }, + { name: 'All Files', extensions: ['*'] }, + ]} + /> +
+ {/* ④ 最小化到托盘 */}
diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 083c25e..4947a56 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -97,6 +97,10 @@ export default { autoRunOnLaunch: 'Also auto-execute on manual launch', autoRunOnLaunchHint: 'Automatically execute the selected configuration when manually opening the app (if disabled, only triggers on system startup)', + gamePath: 'Game Path', + gamePathHint: + 'If no game window is found when starting tasks, launch this path and retry after 60 seconds', + gamePathPlaceholder: 'Select game executable path (e.g. xxx.exe)', confirmBeforeDelete: 'Confirm delete actions', confirmBeforeDeleteHint: 'Show confirmation before delete/clear list/import overwrite, etc.', maxLogsPerInstance: 'Max logs per instance', @@ -286,6 +290,11 @@ export default { preActionFailed: 'Pre-program failed: {{error}}', preActionExitCode: 'Pre-program exit code: {{code}}', preActionConnectDelay: 'Waiting {{seconds}} seconds before connecting...', + gamePathNotSet: 'Game path is not set, cannot auto launch and retry', + autoLaunchGameStarting: 'Game window not found, launching game: {{path}}', + autoLaunchGameWaiting: 'Game launched, waiting {{seconds}} seconds before retry...', + autoLaunchGameRetrying: 'Retrying to find game window {{name}}', + autoLaunchGameFailed: 'Failed to auto launch game: {{error}}', }, // Option Editor diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index bb8e889..d23fb6f 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -96,6 +96,9 @@ export default { autoRunOnLaunch: '手动启动时也自动执行', autoRunOnLaunchHint: '每次手动打开程序时,也自动执行上方选定的配置(关闭则仅在开机自启动时触发)', + gamePath: '游戏路径', + gamePathHint: '开始任务时若未找到游戏窗口,将启动此路径并等待 60 秒后重试', + gamePathPlaceholder: '选择游戏可执行文件路径(如 xxx.exe)', confirmBeforeDelete: '删除操作需要二次确认', confirmBeforeDeleteHint: '删除任务、清空列表、导入覆盖等操作会先弹出确认对话框', maxLogsPerInstance: '每个实例保留的日志上限', @@ -280,6 +283,11 @@ export default { preActionFailed: '前置程序执行失败: {{error}}', preActionExitCode: '前置程序退出码: {{code}}', preActionConnectDelay: '等待 {{seconds}} 秒后连接...', + gamePathNotSet: '未设置游戏路径,无法自动拉起游戏重试', + autoLaunchGameStarting: '未找到游戏窗口,正在启动游戏:{{path}}', + autoLaunchGameWaiting: '游戏已启动,等待 {{seconds}} 秒后重试...', + autoLaunchGameRetrying: '正在重试查找游戏窗口{{name}}', + autoLaunchGameFailed: '自动启动游戏失败:{{error}}', }, // 选项编辑器 diff --git a/src/stores/appStore.ts b/src/stores/appStore.ts index 558fe24..a38fd6d 100644 --- a/src/stores/appStore.ts +++ b/src/stores/appStore.ts @@ -971,6 +971,7 @@ export const useAppStore = create()( autoRunOnLaunch: config.settings.autoRunOnLaunch ?? false, autoStartRemovedInstanceName: config.settings.autoStartRemovedInstanceName, minimizeToTray: config.settings.minimizeToTray ?? false, + gamePath: config.settings.gamePath ?? '', onboardingCompleted: config.settings.onboardingCompleted ?? false, preActionConnectDelaySec: config.settings.preActionConnectDelaySec ?? 5, hotkeys: config.settings.hotkeys ?? { @@ -1281,6 +1282,10 @@ export const useAppStore = create()( } }, + // 游戏路径设置(窗口未找到时自动启动游戏) + gamePath: '', + setGamePath: (path) => set({ gamePath: path }), + // 新用户引导 onboardingCompleted: false, setOnboardingCompleted: (completed) => set({ onboardingCompleted: completed }), @@ -1662,6 +1667,7 @@ function generateConfig(): MxuConfig { autoRunOnLaunch: state.autoRunOnLaunch, autoStartRemovedInstanceName: state.autoStartRemovedInstanceName, minimizeToTray: state.minimizeToTray, + gamePath: state.gamePath, onboardingCompleted: state.onboardingCompleted, preActionConnectDelaySec: state.preActionConnectDelaySec, hotkeys: state.hotkeys, @@ -1721,6 +1727,7 @@ useAppStore.subscribe( autoRunOnLaunch: state.autoRunOnLaunch, autoStartRemovedInstanceName: state.autoStartRemovedInstanceName, minimizeToTray: state.minimizeToTray, + gamePath: state.gamePath, onboardingCompleted: state.onboardingCompleted, hotkeys: state.hotkeys, recentlyClosed: state.recentlyClosed, diff --git a/src/stores/types.ts b/src/stores/types.ts index 341deca..f6af628 100644 --- a/src/stores/types.ts +++ b/src/stores/types.ts @@ -321,6 +321,8 @@ export interface AppState { // 托盘设置 minimizeToTray: boolean; setMinimizeToTray: (enabled: boolean) => void; + gamePath: string; + setGamePath: (path: string) => void; // 启动后自动执行的实例 ID autoStartInstanceId: string | undefined; diff --git a/src/types/config.ts b/src/types/config.ts index cc6ea89..a08f1e1 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -134,6 +134,8 @@ export interface AppSettings { autoStartRemovedInstanceName?: string; // 被删除的自动执行配置名称(用于提示用户) /** 前置动作轮询设备就绪后、连接前的额外延迟秒数(默认 5,仅通过编辑 mxu.json 修改) */ preActionConnectDelaySec?: number; + /** 游戏可执行文件路径(用于未找到窗口时自动拉起游戏) */ + gamePath?: string; } // MXU 配置文件完整结构