Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export function Toolbar({ showAddPanel, onToggleAddPanel }: ToolbarProps) {
language,
// 调试设置
tcpCompatMode,
gamePath,
} = useAppStore();

const [isStarting, setIsStarting] = useState(false);
Expand Down Expand Up @@ -283,9 +284,10 @@ export function Toolbar({ showAddPanel, onToggleAddPanel }: ToolbarProps) {
schedulePolicyName?: string;
/** 自动连接阶段变化回调(用于 UI 状态更新) */
onPhaseChange?: (phase: AutoConnectPhase) => void;
hasRetriedAfterLaunch?: boolean;
},
): Promise<boolean> => {
const { schedulePolicyName, onPhaseChange } = options || {};
const { schedulePolicyName, onPhaseChange, hasRetriedAfterLaunch } = options || {};
const targetId = targetInstance.id;
const targetTasks = targetInstance.selectedTasks || [];

Expand Down Expand Up @@ -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<boolean | null> => {
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 }),
});
Comment on lines +486 to +489
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: 日志中硬编码的 60 秒可能会与 WINDOW_RETRY_DELAY_MS 不一致。

由于 WINDOW_RETRY_DELAY_MS60_000,这条日志应当从该常量推导秒数(例如 WINDOW_RETRY_DELAY_MS / 1000),这样当延迟修改时,日志信息依然是准确的。

Suggested change
addLog(targetId, {
type: 'info',
message: t('action.autoLaunchGameWaiting', { seconds: 60 }),
});
addLog(targetId, {
type: 'info',
message: t('action.autoLaunchGameWaiting', { seconds: WINDOW_RETRY_DELAY_MS / 1000 }),
});
Original comment in English

suggestion: The hardcoded 60 seconds in the log can drift from WINDOW_RETRY_DELAY_MS.

Since WINDOW_RETRY_DELAY_MS is 60_000, this log should derive the seconds from that constant (e.g. WINDOW_RETRY_DELAY_MS / 1000) so the message stays accurate if the delay changes.

Suggested change
addLog(targetId, {
type: 'info',
message: t('action.autoLaunchGameWaiting', { seconds: 60 }),
});
addLog(targetId, {
type: 'info',
message: t('action.autoLaunchGameWaiting', { seconds: WINDOW_RETRY_DELAY_MS / 1000 }),
});

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) => {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -850,6 +915,7 @@ export function Toolbar({ showAddPanel, onToggleAddPanel }: ToolbarProps) {
clearScheduleExecution,
setShowAddTaskPanel,
addLog,
gamePath,
t,
],
);
Expand Down
26 changes: 25 additions & 1 deletion src/components/settings/GeneralSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import {
Power,
Play,
Rocket,
Gamepad2,
ChevronDown,
Check,
} from 'lucide-react';

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() {
Expand All @@ -36,6 +37,8 @@ export function GeneralSection() {
autoRunOnLaunch,
setAutoRunOnLaunch,
autoStartRemovedInstanceName,
gamePath,
setGamePath,
} = useAppStore();

// 开机自启动状态(直接从 Tauri 插件查询,不走 store)
Expand Down Expand Up @@ -234,6 +237,27 @@ export function GeneralSection() {
</div>
</div>

{/* 游戏路径(用于未找到窗口时自动拉起游戏) */}
<div className="bg-bg-secondary rounded-xl p-4 border border-border">
<div className="flex items-center gap-3 mb-3">
<Gamepad2 className="w-5 h-5 text-accent" />
<div>
<span className="font-medium text-text-primary">{t('settings.gamePath')}</span>
<p className="text-xs text-text-muted mt-0.5">{t('settings.gamePathHint')}</p>
</div>
</div>
<FileField
label={t('settings.gamePath')}
value={gamePath}
onChange={(value) => setGamePath(value.trim())}
placeholder={t('settings.gamePathPlaceholder')}
filters={[
{ name: 'Executable', extensions: ['exe', 'bat', 'cmd', 'ps1', 'sh'] },
{ name: 'All Files', extensions: ['*'] },
]}
/>
</div>

{/* ④ 最小化到托盘 */}
<div className="bg-bg-secondary rounded-xl p-4 border border-border">
<div className="flex items-center justify-between">
Expand Down
9 changes: 9 additions & 0 deletions src/i18n/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export default {
autoRunOnLaunch: '手动启动时也自动执行',
autoRunOnLaunchHint:
'每次手动打开程序时,也自动执行上方选定的配置(关闭则仅在开机自启动时触发)',
gamePath: '游戏路径',
gamePathHint: '开始任务时若未找到游戏窗口,将启动此路径并等待 60 秒后重试',
gamePathPlaceholder: '选择游戏可执行文件路径(如 xxx.exe)',
confirmBeforeDelete: '删除操作需要二次确认',
confirmBeforeDeleteHint: '删除任务、清空列表、导入覆盖等操作会先弹出确认对话框',
maxLogsPerInstance: '每个实例保留的日志上限',
Expand Down Expand Up @@ -280,6 +283,11 @@ export default {
preActionFailed: '前置程序执行失败: {{error}}',
preActionExitCode: '前置程序退出码: {{code}}',
preActionConnectDelay: '等待 {{seconds}} 秒后连接...',
gamePathNotSet: '未设置游戏路径,无法自动拉起游戏重试',
autoLaunchGameStarting: '未找到游戏窗口,正在启动游戏:{{path}}',
autoLaunchGameWaiting: '游戏已启动,等待 {{seconds}} 秒后重试...',
autoLaunchGameRetrying: '正在重试查找游戏窗口{{name}}',
autoLaunchGameFailed: '自动启动游戏失败:{{error}}',
},

// 选项编辑器
Expand Down
7 changes: 7 additions & 0 deletions src/stores/appStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ export const useAppStore = create<AppState>()(
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 ?? {
Expand Down Expand Up @@ -1281,6 +1282,10 @@ export const useAppStore = create<AppState>()(
}
},

// 游戏路径设置(窗口未找到时自动启动游戏)
gamePath: '',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 第二次对 gamePath 的初始化会覆盖从配置中加载的值,从而破坏持久化。

这个在 store 初始状态中的 gamePath: '' 定义会覆盖之前的 config.settings.gamePath ?? '' 初始化,因此启动时任何从配置中持久化的路径都会被丢弃。请移除这一初始化(并依赖 setGamePath),或者确保不会覆盖从配置中得到的值。

Original comment in English

issue (bug_risk): The second gamePath initialization overwrites the value loaded from config, breaking persistence.

This gamePath: '' definition in the store’s initial state will override the earlier config.settings.gamePath ?? '' initialization, so any persisted path from config is discarded on startup. Please remove this initializer (and rely on setGamePath) or otherwise ensure the config-derived value is not overwritten.

setGamePath: (path) => set({ gamePath: path }),

// 新用户引导
onboardingCompleted: false,
setOnboardingCompleted: (completed) => set({ onboardingCompleted: completed }),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/stores/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ export interface AppState {
// 托盘设置
minimizeToTray: boolean;
setMinimizeToTray: (enabled: boolean) => void;
gamePath: string;
setGamePath: (path: string) => void;

// 启动后自动执行的实例 ID
autoStartInstanceId: string | undefined;
Expand Down
2 changes: 2 additions & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ export interface AppSettings {
autoStartRemovedInstanceName?: string; // 被删除的自动执行配置名称(用于提示用户)
/** 前置动作轮询设备就绪后、连接前的额外延迟秒数(默认 5,仅通过编辑 mxu.json 修改) */
preActionConnectDelaySec?: number;
/** 游戏可执行文件路径(用于未找到窗口时自动拉起游戏) */
gamePath?: string;
}

// MXU 配置文件完整结构
Expand Down
Loading