optim fpstest ui and try to fix steam path related crash

This commit is contained in:
2025-11-06 23:11:41 +08:00
parent 9f29857fd3
commit f7c9e455f7
9 changed files with 354 additions and 183 deletions

View File

@@ -156,6 +156,22 @@ pub fn check_path(path: &str) -> Result<bool, String> {
Ok(std::path::Path::new(&path).exists())
}
#[tauri::command]
pub fn check_steam_dir_valid(steam_dir: &str) -> Result<bool, String> {
use std::path::Path;
let path = Path::new(steam_dir);
if !path.exists() {
return Ok(false);
}
// 检查是否存在 steam.exe 或 config 目录(至少有一个即可)
let steam_exe = path.join("steam.exe");
let config_dir = path.join("config");
Ok(steam_exe.exists() || config_dir.exists())
}
///// 录像
#[tauri::command]
pub async fn analyze_replay(app: tauri::AppHandle, path: &str) -> Result<String, String> {

View File

@@ -162,6 +162,7 @@ fn main() {
cmds::get_cs2_video_config,
cmds::set_cs2_video_config,
cmds::check_path,
cmds::check_steam_dir_valid,
cmds::analyze_replay,
cmds::get_console_log_path,
cmds::read_vprof_report,

View File

@@ -134,7 +134,10 @@ pub fn parse_login_users(steam_dir: &str) -> Result<Vec<LoginUser>> {
let mut users = Vec::new();
for (k, v) in kv {
let props = v.as_object().unwrap();
let props = match v.as_object() {
Some(p) => p,
None => continue, // 跳过非对象类型的值
};
let avatar = if let Some(img) = read_avatar(&steam_dir, &k) {
img
@@ -142,7 +145,11 @@ pub fn parse_login_users(steam_dir: &str) -> Result<Vec<LoginUser>> {
String::new()
};
let id64 = k.parse::<u64>()?;
// 跳过无法解析为 u64 的键
let id64 = match k.parse::<u64>() {
Ok(id) => id,
Err(_) => continue,
};
let user = LoginUser {
steam_id32: steam::id::id64_to_32(id64),
@@ -216,7 +223,11 @@ pub fn parse_local_users(steam_dir: &str) -> Result<Vec<LocalUser>> {
// 只处理目录
if entry.file_type().is_dir() {
let id = path.file_name().unwrap().to_str().unwrap();
// 安全获取文件名
let id = match path.file_name().and_then(|n| n.to_str()) {
Some(id_str) => id_str,
None => continue, // 跳过无法获取文件名的路径
};
// 检查 localconfig.vdf 文件是否存在
let local_config_path = path.join("config/localconfig.vdf");
@@ -224,21 +235,26 @@ pub fn parse_local_users(steam_dir: &str) -> Result<Vec<LocalUser>> {
continue;
}
// 读取并解析 localconfig.vdf 文件
let data = fs::read_to_string(local_config_path)?;
// 读取并解析 localconfig.vdf 文件,如果失败则跳过
let data = match fs::read_to_string(&local_config_path) {
Ok(d) => d,
Err(_) => continue, // 跳过无法读取的文件
};
let json_data = super::parse::to_json(&data);
let kv: HashMap<String, Value> = serde_json::from_str(&json_data)?;
let kv = match serde_json::from_str::<HashMap<String, Value>>(&json_data) {
Ok(kv) => kv,
Err(_) => continue, // 跳过无法解析的 JSON
};
// 剥离顶层 UserLocalConfigStore
// let kv = kv.get("UserLocalConfigStore").and_then(|v| v.as_object()).unwrap();
// 获取 friends 节点
let friends = kv.get("friends").and_then(|v| v.as_object());
if friends.is_none() {
continue;
}
let friends = friends.unwrap();
let friends = match kv.get("friends").and_then(|v| v.as_object()) {
Some(f) => f,
None => continue,
};
// 获取 PersonaName
let persona_name = friends
@@ -256,9 +272,15 @@ pub fn parse_local_users(steam_dir: &str) -> Result<Vec<LocalUser>> {
.unwrap_or("")
.to_string();
// 安全解析 ID如果失败则跳过
let steam_id32 = match id.parse::<u32>() {
Ok(id) => id,
Err(_) => continue, // 跳过无法解析为 u32 的 ID
};
// 创建 LocalUser 并加入列表
local_users.push(LocalUser {
steam_id32: id.parse::<u32>().unwrap(),
steam_id32,
persona_name,
avatar_key,
});

View File

@@ -19,12 +19,31 @@ export default function RootLayout({ children }: { children: React.ReactNode })
void init()
void listen<string>("tray://launch_game", async (event) => {
// 验证路径
if (!steamStore.state.steamDir || !steamStore.state.steamDirValid) {
addToast({
title: "Steam 路径无效,请先配置路径",
color: "warning"
})
return
}
try {
await invoke("launch_game", {
steamPath: `${steamStore.state.steamDir}\\steam.exe`,
launchOption: toolStore.state.launchOptions[toolStore.state.launchIndex].option || "",
server: event.payload || "worldwide",
})
addToast({ title: `启动${event.payload === "worldwide" ? "国际服" : "国服"}成功` })
addToast({
title: `启动${event.payload === "worldwide" ? "国际服" : "国服"}成功`,
color: "success"
})
} catch (error) {
console.error("启动游戏失败:", error)
addToast({
title: `启动失败: ${error instanceof Error ? error.message : String(error)}`,
color: "danger"
})
}
})
void listen("tray://kill_steam", async () => {
@@ -82,10 +101,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
void steam.checkCs2DirValid()
}, [debounceCs2Dir])
useEffect(() => {
if (debounceSteamDirValid) {
if (debounceSteamDirValid && steam.state.steamDir) {
// 安全地获取用户列表(内部已有错误处理)
void steam.getUsers()
// 启动文件监听
void invoke("start_watch_loginusers", { steamDir: steam.state.steamDir })
// 启动文件监听,添加错误处理
void invoke("start_watch_loginusers", { steamDir: steam.state.steamDir }).catch((error) => {
console.error("启动文件监听失败:", error)
})
}
}, [debounceSteamDirValid, steam.state.steamDir])

View File

@@ -20,13 +20,29 @@ const FastLaunch = () => {
<div className="flex gap-2">
<Button
size="md"
onPress={() => {
void invoke("launch_game", {
onPress={async () => {
// 验证路径
if (!steam.state.steamDir || !steam.state.steamDirValid) {
addToast({
title: "Steam 路径无效,请先配置路径",
color: "warning"
})
return
}
try {
await invoke("launch_game", {
steamPath: `${steam.state.steamDir}/steam.exe`,
launchOption: tool.state.launchOptions[tool.state.launchIndex].option || "",
server: "perfectworld",
})
addToast({ title: "启动国服成功" })
addToast({ title: "启动国服成功", color: "success" })
} catch (error) {
console.error("启动游戏失败:", error)
addToast({
title: `启动失败: ${error instanceof Error ? error.message : String(error)}`,
color: "danger"
})
}
}}
className="px-4 py-1 font-medium transition bg-red-200 rounded-full select-none dark:bg-red-900/60"
>
@@ -34,13 +50,29 @@ const FastLaunch = () => {
</Button>
<Button
size="md"
onPress={() => {
void invoke("launch_game", {
onPress={async () => {
// 验证路径
if (!steam.state.steamDir || !steam.state.steamDirValid) {
addToast({
title: "Steam 路径无效,请先配置路径",
color: "warning"
})
return
}
try {
await invoke("launch_game", {
steamPath: `${steam.state.steamDir}/steam.exe`,
launchOption: tool.state.launchOptions[tool.state.launchIndex].option || "",
server: "worldwide",
})
addToast({ title: "启动国际服成功" })
addToast({ title: "启动国际服成功", color: "success" })
} catch (error) {
console.error("启动游戏失败:", error)
addToast({
title: `启动失败: ${error instanceof Error ? error.message : String(error)}`,
color: "danger"
})
}
}}
className="px-4 py-1 font-medium transition bg-orange-200 rounded-full select-none dark:bg-orange-900/60"
>

View File

@@ -210,6 +210,26 @@ function extractFpsMetrics(result: string): { avg: number | null; p1: number | n
return { avg, p1 }
}
// 备注单元格组件
function NoteCell({ note, onEdit }: { note: string; onEdit: () => void }) {
return (
<div className="flex items-center min-w-0 gap-1">
<span className="flex-1 min-w-0 truncate">
{note || "无备注"}
</span>
<Button
size="sm"
isIconOnly
variant="light"
onPress={onEdit}
className="h-5 min-w-5 shrink-0"
>
<Edit size={12} />
</Button>
</div>
)
}
export function FpsTest() {
const steam = useSteamStore()
const tool = useToolStore()
@@ -291,9 +311,7 @@ export function FpsTest() {
tool.state.videoSetting.defaultresheight || ""
)
}
if (fpsTest.state.config.isFullscreen === true && tool.state.videoSetting.fullscreen !== "1") {
fpsTest.setIsFullscreen(tool.state.videoSetting.fullscreen === "1")
}
// 全屏/窗口化设置不再同步游戏设置,只控制启动项参数
}
}, [tool.state.videoSetting, fpsTest])
@@ -360,14 +378,38 @@ export function FpsTest() {
try {
// 获取 console.log 路径
const consoleLogPath = await invoke<string>("get_console_log_path", {
let consoleLogPath: string
try {
consoleLogPath = await invoke<string>("get_console_log_path", {
csPath: steam.state.cs2Dir,
})
} catch (error) {
console.error("获取控制台日志路径失败:", error)
if (!silent) {
addToast({
title: "获取控制台日志路径失败",
color: "warning"
})
}
return false
}
// 读取 VProf 报告
const report = await invoke<string>("read_vprof_report", {
let report: string
try {
report = await invoke<string>("read_vprof_report", {
consoleLogPath: consoleLogPath,
})
} catch (error) {
console.error("读取性能报告失败:", error)
if (!silent) {
addToast({
title: "读取性能报告失败",
color: "warning"
})
}
return false
}
if (report && report.trim().length > 0) {
const parsed = parseVProfReport(report)
@@ -427,7 +469,7 @@ export function FpsTest() {
// 添加带批量标识和分辨率信息的备注
let batchNote = ""
if (currentResolution) {
batchNote = `[分辨率:${currentResolution.label}]`
batchNote = `[${currentResolution.label}]`
}
if (testNote) {
batchNote = batchNote ? `${testNote} ${batchNote}` : testNote
@@ -472,7 +514,7 @@ export function FpsTest() {
// 单次测试,添加分辨率信息到备注
let singleNote = testNote
if (currentResolution) {
const resolutionNote = `[分辨率:${currentResolution.label}]`
const resolutionNote = `[${currentResolution.label}]`
singleNote = singleNote ? `${testNote} ${resolutionNote}` : resolutionNote
}
@@ -636,7 +678,21 @@ export function FpsTest() {
isFirstTest: boolean = false,
resolution?: { width: string; height: string; label: string }
): Promise<boolean> => {
// 验证路径是否存在且有效
if (!steam.state.steamDir || !steam.state.cs2Dir) {
addToast({
title: "Steam 或 CS2 路径未设置,请先配置路径",
color: "warning"
})
return false
}
// 验证 Steam 路径是否有效
if (!steam.state.steamDirValid) {
addToast({
title: "Steam 路径无效,请检查路径设置",
color: "warning"
})
return false
}
@@ -684,14 +740,14 @@ export function FpsTest() {
label: `${resolutionWidth}x${resolutionHeight}`,
}
// 只有在启用分辨率和全屏设置时才添加相关参数
if (isResolutionEnabled) {
// 添加分辨率设置(如果启用分辨率功能或分辨率组)
if (isResolutionEnabled || isResolutionGroupEnabled) {
// 添加分辨率设置(如果有设置)
if (currentResolution.width && currentResolution.height) {
baseLaunchOption += ` -w ${currentResolution.width} -h ${currentResolution.height}`
}
// 添加全屏/窗口化设置
// 添加全屏/窗口化设置(独立控制,不依赖游戏设置)
if (isFullscreen) {
baseLaunchOption += ` -fullscreen`
} else {
@@ -705,11 +761,20 @@ export function FpsTest() {
: baseLaunchOption
// 启动游戏强制使用worldwide国际服
try {
await invoke("launch_game", {
steamPath: `${steam.state.steamDir}\\steam.exe`,
launchOption: launchOption,
server: "worldwide",
})
} catch (error) {
console.error("启动游戏失败:", error)
addToast({
title: `启动游戏失败: ${error instanceof Error ? error.message : String(error)}`,
color: "danger"
})
return false
}
// 视频设置将在测试结束后读取在readResult中
// 保存当前测试的分辨率信息
@@ -859,7 +924,7 @@ export function FpsTest() {
const second = String(now.getSeconds()).padStart(2, "0")
const testTime = `${month}/${day} ${hour}:${minute}:${second}`
let averageNote = `[分辨率:${currentResolution.label}]`
let averageNote = `[${currentResolution.label}]`
if (testNote) {
averageNote = `${testNote} ${averageNote}`
}
@@ -1209,99 +1274,81 @@ export function FpsTest() {
</CardHeader>
<CardBody>
{showResultsTable ? (
<div className="flex flex-col gap-2">
<div className="max-h-[500px] overflow-auto">
<div className="relative flex flex-col gap-2">
<Table
aria-label="测试结果表格"
selectionMode="none"
removeWrapper
classNames={{
wrapper: "overflow-auto",
base: "min-h-[222px]",
th: "px-2 py-1.5 text-xs",
td: "px-2 py-1.5 text-xs",
table: "min-w-full",
th: "px-3 py-2 text-xs font-semibold whitespace-nowrap",
td: "px-3 py-2 text-xs",
}}
>
<TableHeader>
<TableColumn></TableColumn>
<TableColumn></TableColumn>
<TableColumn></TableColumn>
<TableColumn>P1低帧</TableColumn>
<TableColumn>CPU</TableColumn>
<TableColumn></TableColumn>
<TableColumn>GPU</TableColumn>
<TableColumn></TableColumn>
<TableColumn></TableColumn>
<TableColumn></TableColumn>
<TableColumn width={100}></TableColumn>
<TableColumn minWidth={140}></TableColumn>
<TableColumn width={80}></TableColumn>
<TableColumn width={80}></TableColumn>
<TableColumn width={80}>P1低帧</TableColumn>
<TableColumn width={150}>CPU</TableColumn>
<TableColumn minWidth={80}></TableColumn>
<TableColumn minWidth={100}>GPU</TableColumn>
<TableColumn width={80}></TableColumn>
<TableColumn width={120}></TableColumn>
<TableColumn minWidth={100}></TableColumn>
<TableColumn width={80} align="center"></TableColumn>
</TableHeader>
<TableBody emptyContent="暂无测试记录">
{fpsTest.state.results.map((result) => (
<TableRow key={result.id}>
<TableCell className="text-xs">{result.testTime}</TableCell>
<TableCell className="text-xs">{result.mapLabel}</TableCell>
<TableCell className="text-xs whitespace-nowrap">{result.testTime}</TableCell>
<TableCell className="text-xs">
<div className="truncate">
{result.mapLabel}
</div>
</TableCell>
<TableCell className="text-xs">
{result.avg !== null ? `${result.avg.toFixed(1)}` : "N/A"}
</TableCell>
<TableCell className="text-xs">
{result.p1 !== null ? `${result.p1.toFixed(1)}` : "N/A"}
</TableCell>
<TableCell
className="text-xs max-w-[160px]"
title={result.hardwareInfo?.cpu || undefined}
>
<div className="truncate whitespace-nowrap">
<TableCell className="text-xs max-w-[150px]">
<div className="truncate">
{result.hardwareInfo?.cpu || "N/A"}
</div>
</TableCell>
<TableCell
className="text-xs max-w-[160px]"
title={result.hardwareInfo?.os || undefined}
>
<div className="truncate whitespace-nowrap">
<TableCell className="text-xs">
<div className="truncate">
{result.hardwareInfo?.os || "N/A"}
</div>
</TableCell>
<TableCell
className="text-xs max-w-[180px]"
title={result.hardwareInfo?.gpu || undefined}
>
<div className="truncate whitespace-nowrap">
<TableCell className="text-xs">
<div className="truncate">
{result.hardwareInfo?.gpu || "N/A"}
</div>
</TableCell>
<TableCell className="text-xs">
<TableCell className="text-xs whitespace-nowrap">
{result.hardwareInfo?.memory ? `${result.hardwareInfo.memory}GB` : "N/A"}
</TableCell>
<TableCell
className="text-xs"
title={formatVideoSettingSummary(result.videoSetting)}
>
<TableCell className="text-xs">
<Tooltip content={formatVideoSettingSummary(result.videoSetting)}>
<span className="cursor-help">
<span className="truncate cursor-help">
{result.videoSetting
? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}`
: "N/A"}
</span>
</Tooltip>
</TableCell>
<TableCell className="text-xs max-w-[150px]">
<div className="flex items-center gap-1">
<span className="truncate" title={result.note || "无备注"}>
{result.note || "无备注"}
</span>
<Button
size="sm"
isIconOnly
variant="light"
onPress={() => handleEditNote(result.id, result.note || "")}
className="h-5 min-w-5 shrink-0"
>
<Edit size={12} />
</Button>
</div>
<TableCell className="text-xs">
<NoteCell
note={result.note || ""}
onEdit={() => handleEditNote(result.id, result.note || "")}
/>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<div className="flex items-center justify-center">
<Button
size="sm"
isIconOnly
@@ -1324,7 +1371,6 @@ export function FpsTest() {
</TableBody>
</Table>
</div>
</div>
) : (
<div className="flex flex-col gap-4">
{/* 备注单独一行 - 放在最上面 */}
@@ -1554,7 +1600,7 @@ export function FpsTest() {
variant={isFullscreen ? "solid" : "flat"}
color={isFullscreen ? "primary" : "default"}
onPress={() => fpsTest.setIsFullscreen(!isFullscreen)}
isDisabled={!isResolutionEnabled || isMonitoring}
isDisabled={isMonitoring || (!isResolutionEnabled && !isResolutionGroupEnabled)}
className="font-medium"
>
{isFullscreen ? "全屏" : "窗口化"}

View File

@@ -331,11 +331,15 @@ const VideoSetting = () => {
useEffect(() => {
if (steam.state.steamDirValid && steam.currentUser()) {
// 安全地获取视频配置(内部已有错误处理)
void tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
// 启动文件监听
// 启动文件监听,添加错误处理
void invoke("start_watch_cs2_video", {
steamDir: steam.state.steamDir,
steamId32: steam.currentUser()?.steam_id32 || 0,
}).catch((error) => {
console.error("启动视频配置文件监听失败:", error)
// 不显示错误提示,避免干扰用户
})
}
// 清理函数:停止后端监听

View File

@@ -74,12 +74,21 @@ const setCs2DirChecking = (checking: boolean) => {
const checkSteamDirValid = async () => {
setSteamDirChecking(true)
const pathExist = await invoke<boolean>("check_path", { path: steamStore.state.steamDir })
setSteamDirValid(pathExist)
try {
// 使用专门的 Steam 路径验证,检查 steam.exe 或 config 目录
const isValid = await invoke<boolean>("check_steam_dir_valid", {
steamDir: steamStore.state.steamDir
})
setSteamDirValid(isValid)
} catch (error) {
console.error("验证 Steam 路径时出错:", error)
setSteamDirValid(false)
} finally {
setTimeout(() => {
setSteamDirChecking(false)
}, 500)
}
}
const checkCs2DirValid = async () => {
setCs2DirChecking(true)
@@ -95,8 +104,21 @@ const currentUser = () => {
}
const getUsers = async () => {
const users = await invoke<SteamUser[]>("get_steam_users", { steamDir: steamStore.state.steamDir })
// 只有在路径有效时才尝试获取用户
if (!steamStore.state.steamDirValid || !steamStore.state.steamDir) {
return
}
try {
const users = await invoke<SteamUser[]>("get_steam_users", {
steamDir: steamStore.state.steamDir
})
setUsers(users)
} catch (error) {
console.error("获取 Steam 用户列表失败:", error)
// 如果获取失败,清空用户列表,避免显示错误数据
setUsers([])
}
}
const selectUser = (index: number) => {

View File

@@ -262,9 +262,15 @@ const setVideoSetting = (setting: VideoSetting) => {
}
const getVideoConfig = async (steam_dir: string, steam_id32: number) => {
try {
const video = await invoke<VideoSetting>("get_cs2_video_config", { steamDir: steam_dir, steamId32: steam_id32 })
// console.log(video)
setVideoSetting(video)
} catch (error) {
console.error("读取视频配置失败:", error)
// 如果文件不存在或读取失败,使用默认配置或保持当前配置
// 不抛出错误,避免影响其他功能
}
}
const setVideoConfig = async (steam_dir: string, steam_id32: number, video_config: VideoSetting) => {