use anyhow::Result; use base64::engine::general_purpose::STANDARD; use base64::Engine; use regex::Regex; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::fs; use std::path::Path; use tauri_plugin_http::reqwest::blocking::get; use walkdir::WalkDir; use crate::steam; #[derive(Serialize, Deserialize, Debug)] pub struct User { steam_id64: u64, steam_id32: u32, account_name: String, persona_name: String, recent: i32, avatar: String, } #[derive(Serialize, Deserialize, Debug)] pub struct LoginUser { steam_id64: u64, steam_id32: u32, account_name: String, persona_name: String, remember_password: String, wants_offline_mode: String, skip_offline_mode_warning: String, allow_auto_login: String, most_recent: String, timestamp: String, avatar: String, avatar_key: String, } #[derive(Serialize, Deserialize, Debug)] pub struct LocalUser { steam_id32: u32, persona_name: String, avatar_key: String, } #[derive(Serialize, Deserialize, Debug)] pub struct VideoConfig { version: String, vendor_id: String, device_id: String, cpu_level: String, gpu_mem_level: String, gpu_level: String, knowndevice: String, defaultres: String, defaultresheight: String, refreshrate_numerator: String, refreshrate_denominator: String, fullscreen: String, coop_fullscreen: String, nowindowborder: String, mat_vsync: String, fullscreen_min_on_focus_loss: String, high_dpi: String, auto_config: String, shaderquality: String, r_texturefilteringquality: String, msaa_samples: String, r_csgo_cmaa_enable: String, videocfg_shadow_quality: String, videocfg_dynamic_shadows: String, videocfg_texture_detail: String, videocfg_particle_detail: String, videocfg_ao_detail: String, videocfg_hdr_detail: String, videocfg_fsr_detail: String, monitor_index: String, r_low_latency: String, aspectratiomode: String, } impl Default for VideoConfig { fn default() -> Self { VideoConfig { version: "15".to_string(), vendor_id: "0".to_string(), device_id: "0".to_string(), cpu_level: "3".to_string(), gpu_mem_level: "3".to_string(), gpu_level: "3".to_string(), knowndevice: "0".to_string(), defaultres: "1920".to_string(), defaultresheight: "1080".to_string(), refreshrate_numerator: "144".to_string(), refreshrate_denominator: "1".to_string(), fullscreen: "1".to_string(), coop_fullscreen: "0".to_string(), nowindowborder: "1".to_string(), mat_vsync: "0".to_string(), fullscreen_min_on_focus_loss: "1".to_string(), high_dpi: "0".to_string(), auto_config: "2".to_string(), shaderquality: "0".to_string(), r_texturefilteringquality: "3".to_string(), msaa_samples: "2".to_string(), r_csgo_cmaa_enable: "0".to_string(), videocfg_shadow_quality: "0".to_string(), videocfg_dynamic_shadows: "1".to_string(), videocfg_texture_detail: "1".to_string(), videocfg_particle_detail: "0".to_string(), videocfg_ao_detail: "0".to_string(), videocfg_hdr_detail: "3".to_string(), videocfg_fsr_detail: "0".to_string(), monitor_index: "0".to_string(), r_low_latency: "1".to_string(), aspectratiomode: "0".to_string(), } } } pub fn parse_login_users(steam_dir: &str) -> Result> { let t_path = Path::new(steam_dir).join("config/loginusers.vdf"); if !t_path.exists() { return Ok(Vec::new()); } let data = fs::read_to_string(t_path)?; let json_data = super::parse::to_json(&data); let kv: HashMap = serde_json::from_str(&json_data)?; let mut users = Vec::new(); for (k, v) in kv { let props = v.as_object().unwrap(); let avatar = if let Some(img) = read_avatar(&steam_dir, &k) { img } else { String::new() }; let id64 = k.parse::()?; let user = LoginUser { steam_id32: steam::id::id64_to_32(id64), steam_id64: id64, account_name: props .get("AccountName") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), persona_name: props .get("PersonaName") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), remember_password: props .get("RememberPassword") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), wants_offline_mode: props .get("WantsOfflineMode") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), skip_offline_mode_warning: props .get("SkipOfflineModeWarning") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), allow_auto_login: props .get("AllowAutoLogin") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), most_recent: props .get("MostRecent") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), timestamp: props .get("Timestamp") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), avatar, avatar_key: String::new(), }; users.push(user); } Ok(users) } pub fn parse_local_users(steam_dir: &str) -> Result> { let root = Path::new(steam_dir).join("userdata"); if !root.exists() { return Ok(Vec::new()); } let mut local_users = Vec::new(); for entry in WalkDir::new(&root) { let entry = entry?; let path = entry.path(); // 跳过根目录 if path == root { continue; } // 只处理目录 if entry.file_type().is_dir() { let id = path.file_name().unwrap().to_str().unwrap(); // 检查 localconfig.vdf 文件是否存在 let local_config_path = path.join("config/localconfig.vdf"); if !local_config_path.exists() { continue; } // 读取并解析 localconfig.vdf 文件 let data = fs::read_to_string(local_config_path)?; let json_data = super::parse::to_json(&data); let kv: HashMap = serde_json::from_str(&json_data)?; // 剥离顶层 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(); // 获取 PersonaName let persona_name = friends .get("PersonaName") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); // 获取 AvatarKey let avatar_key = friends .get(id) .and_then(|v| v.as_object()) .and_then(|props| props.get("avatar")) .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); // 创建 LocalUser 并加入列表 local_users.push(LocalUser { steam_id32: id.parse::().unwrap(), persona_name, avatar_key, }); // 跳过子目录 WalkDir::new(path).max_depth(1).into_iter().next(); } } Ok(local_users) } pub fn merge_users(login: Vec, local: Vec) -> Vec { let mut users = Vec::new(); for i in login { let avatar: String; let mut avatar_key = String::new(); // 匹配获取 avatar_key 在线获取头像使用 let t_usr: Vec<&LocalUser> = local .iter() .filter(|j| i.steam_id32 == j.steam_id32) .collect(); if let Some(usr) = t_usr.first() { avatar_key = usr.avatar_key.clone(); } // 获取头像的base64 本地头像文件不存在时使用在线api获取 if i.avatar.is_empty() && !avatar_key.is_empty() { avatar = download_avatar(&avatar_key).unwrap_or_default(); } else { avatar = i.avatar; } users.push(User { steam_id64: i.steam_id64, steam_id32: i.steam_id32, account_name: i.account_name, persona_name: i.persona_name, recent: i.most_recent.parse().unwrap_or(0), avatar, }); } // 把第一个recent=1的放在头部 for (id, usr) in users.iter_mut().enumerate() { if usr.recent == 1 { let tmp = users.remove(id); users.insert(0, tmp); break; } } users } pub fn get_users(steam_dir: &str) -> Result> { let login_users = parse_login_users(steam_dir)?; let local_users = parse_local_users(steam_dir)?; let users = merge_users(login_users, local_users); Ok(users) } fn download_avatar(avatar_key: &str) -> Result { // 下载并转换成Base64 "https://avatars.cloudflare.steamstatic.com/" + avatarKey + "_full.jpg" let url = format!( "https://avatars.cloudflare.steamstatic.com/{}_full.jpg", avatar_key ); if let Ok(resp) = get(&url) { if let Ok(bytes) = resp.bytes() { return Ok(STANDARD.encode(bytes)); } } return Err(anyhow::anyhow!("Failed to download avatar")); } fn read_avatar(steam_dir: &str, steam_id64: &str) -> Option { let t_path = Path::new(steam_dir).join(format!("config\\avatarcache\\{}.png", steam_id64)); if !t_path.exists() { return None; } if let Ok(img) = fs::read(t_path) { Some(STANDARD.encode(img)) } else { None } } pub fn get_cs2_video(file_path: &str) -> Result { // TODO: no kv let data = fs::read_to_string(file_path)?; let json_data = super::parse::to_json(&data); let kv: HashMap = serde_json::from_str(&json_data)?; let video_config = VideoConfig { version: kv.get("Version").unwrap_or(&"".to_string()).to_string(), vendor_id: kv.get("VendorID").unwrap_or(&"".to_string()).to_string(), device_id: kv.get("DeviceID").unwrap_or(&"".to_string()).to_string(), cpu_level: kv .get("setting.cpu_level") .unwrap_or(&"".to_string()) .to_string(), gpu_mem_level: kv .get("setting.gpu_mem_level") .unwrap_or(&"".to_string()) .to_string(), gpu_level: kv .get("setting.gpu_level") .unwrap_or(&"".to_string()) .to_string(), knowndevice: kv .get("setting.knowndevice") .unwrap_or(&"".to_string()) .to_string(), defaultres: kv .get("setting.defaultres") .unwrap_or(&"".to_string()) .to_string(), defaultresheight: kv .get("setting.defaultresheight") .unwrap_or(&"".to_string()) .to_string(), refreshrate_numerator: kv .get("setting.refreshrate_numerator") .unwrap_or(&"".to_string()) .to_string(), refreshrate_denominator: kv .get("setting.refreshrate_denominator") .unwrap_or(&"".to_string()) .to_string(), fullscreen: kv .get("setting.fullscreen") .unwrap_or(&"".to_string()) .to_string(), coop_fullscreen: kv .get("setting.coop_fullscreen") .unwrap_or(&"".to_string()) .to_string(), nowindowborder: kv .get("setting.nowindowborder") .unwrap_or(&"".to_string()) .to_string(), mat_vsync: kv .get("setting.mat_vsync") .unwrap_or(&"".to_string()) .to_string(), fullscreen_min_on_focus_loss: kv .get("setting.fullscreen_min_on_focus_loss") .unwrap_or(&"".to_string()) .to_string(), high_dpi: kv .get("setting.high_dpi") .unwrap_or(&"".to_string()) .to_string(), auto_config: kv.get("AutoConfig").unwrap_or(&"".to_string()).to_string(), shaderquality: kv .get("setting.shaderquality") .unwrap_or(&"".to_string()) .to_string(), r_texturefilteringquality: kv .get("setting.r_texturefilteringquality") .unwrap_or(&"".to_string()) .to_string(), msaa_samples: kv .get("setting.msaa_samples") .unwrap_or(&"".to_string()) .to_string(), r_csgo_cmaa_enable: kv .get("setting.r_csgo_cmaa_enable") .unwrap_or(&"".to_string()) .to_string(), videocfg_shadow_quality: kv .get("setting.videocfg_shadow_quality") .unwrap_or(&"".to_string()) .to_string(), videocfg_dynamic_shadows: kv .get("setting.videocfg_dynamic_shadows") .unwrap_or(&"".to_string()) .to_string(), videocfg_texture_detail: kv .get("setting.videocfg_texture_detail") .unwrap_or(&"".to_string()) .to_string(), videocfg_particle_detail: kv .get("setting.videocfg_particle_detail") .unwrap_or(&"".to_string()) .to_string(), videocfg_ao_detail: kv .get("setting.videocfg_ao_detail") .unwrap_or(&"".to_string()) .to_string(), videocfg_hdr_detail: kv .get("setting.videocfg_hdr_detail") .unwrap_or(&"".to_string()) .to_string(), videocfg_fsr_detail: kv .get("setting.videocfg_fsr_detail") .unwrap_or(&"".to_string()) .to_string(), monitor_index: kv .get("setting.monitor_index") .unwrap_or(&"".to_string()) .to_string(), r_low_latency: kv .get("setting.r_low_latency") .unwrap_or(&"".to_string()) .to_string(), aspectratiomode: kv .get("setting.aspectratiomode") .unwrap_or(&"".to_string()) .to_string(), }; Ok(video_config) } pub fn set_cs2_video(file_path: &str, data: VideoConfig) -> Result<()> { // 读取文件内容 let file_content = fs::read_to_string(file_path)?; // 定义正则表达式匹配模式 let re = Regex::new(r#""(setting\.\w+)"\s+"\d+""#).unwrap(); // 替换字段值 let updated_content = re.replace_all(&file_content, |caps: ®ex::Captures| { let key = &caps[1]; // 捕获的键名 let value = match key { "Version" => &data.version, "VendorID" => &data.vendor_id, "DeviceID" => &data.device_id, "setting.cpu_level" => &data.cpu_level, "setting.gpu_mem_level" => &data.gpu_mem_level, "setting.gpu_level" => &data.gpu_level, "setting.knowndevice" => &data.knowndevice, "setting.defaultres" => &data.defaultres, "setting.defaultresheight" => &data.defaultresheight, "setting.refreshrate_numerator" => &data.refreshrate_numerator, "setting.refreshrate_denominator" => &data.refreshrate_denominator, "setting.fullscreen" => &data.fullscreen, "setting.coop_fullscreen" => &data.coop_fullscreen, "setting.nowindowborder" => &data.nowindowborder, "setting.mat_vsync" => &data.mat_vsync, "setting.fullscreen_min_on_focus_loss" => &data.fullscreen_min_on_focus_loss, "setting.high_dpi" => &data.high_dpi, "AutoConfig" => &data.auto_config, "setting.shaderquality" => &data.shaderquality, "setting.r_texturefilteringquality" => &data.r_texturefilteringquality, "setting.msaa_samples" => &data.msaa_samples, "setting.r_csgo_cmaa_enable" => &data.r_csgo_cmaa_enable, "setting.videocfg_shadow_quality" => &data.videocfg_shadow_quality, "setting.videocfg_dynamic_shadows" => &data.videocfg_dynamic_shadows, "setting.videocfg_texture_detail" => &data.videocfg_texture_detail, "setting.videocfg_particle_detail" => &data.videocfg_particle_detail, "setting.videocfg_ao_detail" => &data.videocfg_ao_detail, "setting.videocfg_hdr_detail" => &data.videocfg_hdr_detail, "setting.videocfg_fsr_detail" => &data.videocfg_fsr_detail, "setting.monitor_index" => &data.monitor_index, "setting.r_low_latency" => &data.r_low_latency, "setting.aspectratiomode" => &data.aspectratiomode, _ => "", // 默认情况 }; format!(r#""{}" "{}""#, key, value) }); fs::write(file_path, updated_content.as_ref())?; Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_get_users() { let users = get_users("D:\\Programs\\Steam").unwrap(); println!("{:?}", users); } #[test] fn test_get_cs2_video() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let file_path = format!("{}/src/vdf/tests/cs2_video.txt", manifest_dir); let video_config = get_cs2_video(&file_path).unwrap(); println!("{:?}", video_config); } #[test] fn test_set_cs2_video() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let file_path = format!("{}/temp/cs2_video.txt", manifest_dir); fs::copy( format!("{}/src/vdf/tests/cs2_video.txt", manifest_dir), file_path.clone(), ) .unwrap(); let video_config = VideoConfig::default(); set_cs2_video(&file_path, video_config).unwrap(); } }