windows_tool 0.0.9

一个 Windows 库 可以设置端口代理 elevate 提升权限 管理员权限 registry 注册表 port_forwarding 端口转发设置 steam vdf 格式 steam apex 启动项设置
Documentation
//! EA Desktop:`%LOCALAPPDATA%\\Electronic Arts\\EA Desktop\\user_*.ini`
//! Apex 启动项键:`user.gamecommandline.origin.ofr.50.0002694`(大小写不敏感)

use crate::utils::run_commands;
use serde::Serialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

/// Apex Legends 在 EA Desktop INI 中的 catalog 键(与社区文档一致)
const APEX_EA_COMMAND_LINE_KEY_LOWER: &str = "user.gamecommandline.origin.ofr.50.0002694";

#[derive(Clone, Debug, Serialize)]
pub struct EaDesktopUser {
    pub id: String,
    pub name: String,
    pub avatar: String,
    pub config_path: String,
}

pub fn ea_desktop_dir() -> Result<PathBuf, String> {
    let local = std::env::var("LOCALAPPDATA")
        .map_err(|_| "未设置 LOCALAPPDATA(仅 Windows 支持 EA Desktop)".to_string())?;
    Ok(PathBuf::from(local).join("Electronic Arts").join("EA Desktop"))
}

fn user_ini_path(ea_user_id: &str) -> Result<PathBuf, String> {
    if !ea_user_id.chars().all(|c| c.is_ascii_digit()) {
        return Err("无效的 EA 用户 id".to_string());
    }
    let dir = ea_desktop_dir()?;
    let p = dir.join(format!("user_{}.ini", ea_user_id));
    if !p.is_file() {
        return Err(format!("未找到 EA 用户配置文件: {:?}", p));
    }
    Ok(p)
}

fn parse_ini_lines(content: &str) -> Vec<(String, String)> {
    content
        .lines()
        .filter_map(|line| {
            let t = line.trim_end();
            if t.is_empty() {
                return None;
            }
            let t = t.trim_start();
            if t.starts_with('#') || t.starts_with(';') {
                return None;
            }
            t.split_once('=').map(|(k, v)| (k.trim().to_string(), v.trim().to_string()))
        })
        .collect()
}

fn ini_to_lower_map(lines: &[(String, String)]) -> HashMap<String, String> {
    let mut m = HashMap::new();
    for (k, v) in lines {
        m.insert(k.to_lowercase(), v.clone());
    }
    m
}

/// 按顺序取 INI(已转为小写键名)中第一个非空值。
fn first_non_empty(lower: &HashMap<String, String>, keys: &[&str]) -> Option<String> {
    keys.iter()
        .filter_map(|k| lower.get(*k).filter(|v| !v.is_empty()).map(|v| v.clone()))
        .next()
}

fn display_name_from_ini(lower: &HashMap<String, String>, file_id: &str) -> String {
    const KEYS: &[&str] = &[
        "user.eadisplayname",
        "user.maynickname",
        "user.displayname",
        "user.personaname",
        "user.nickname",
        "user.username",
    ];
    first_non_empty(lower, KEYS).unwrap_or_else(|| format!("EA {}", file_id))
}

fn avatar_from_ini(lower: &HashMap<String, String>) -> String {
    const KEYS: &[&str] = &[
        "user.avataruri",
        "user.avatarurl",
        "user.pictureurl",
        "user.portraiturl",
        "user.profileimageurl",
    ];
    first_non_empty(lower, KEYS).unwrap_or_default()
}

fn find_apex_command_line_line_index(lines: &[(String, String)]) -> Option<usize> {
    for (i, (k, _)) in lines.iter().enumerate() {
        if k.to_lowercase() == APEX_EA_COMMAND_LINE_KEY_LOWER {
            return Some(i);
        }
    }
    None
}

fn read_user_ini(path: &Path) -> Result<Vec<(String, String)>, String> {
    let content =
        fs::read_to_string(path).map_err(|e| format!("读取 {:?} 失败: {}", path, e))?;
    Ok(parse_ini_lines(&content))
}

fn write_user_ini(path: &Path, lines: &[(String, String)]) -> Result<(), String> {
    let mut out = String::new();
    for (k, v) in lines {
        out.push_str(k);
        out.push('=');
        out.push_str(v);
        out.push('\n');
    }
    fs::write(path, out).map_err(|e| format!("写入 {:?} 失败: {}", path, e))
}

/// 通过 tasklist 判断 EA Desktop / EA 相关进程是否在运行(写入 `user_*.ini` 前需关闭,否则会被客户端覆盖)。
pub fn ea_desktop_is_running_by_tasklist() -> Result<bool, String> {
    const NAMES: &[&str] = &[
        "EADesktop.exe",
        "EALauncher.exe",
        "EABackgroundAgent.exe",
        "EASteamProxy.exe",
    ];
    for name in NAMES {
        let cmd = format!("tasklist /fi \"imagename eq {}\" /fo csv", name);
        let output = run_commands(cmd, true, false);
        let output_str = String::from_utf8_lossy(&output.stdout);
        if output_str.to_lowercase().contains(&name.to_lowercase()) {
            return Ok(true);
        }
    }
    Ok(false)
}

pub fn get_ea_desktop_users() -> Result<Vec<EaDesktopUser>, String> {
    let dir = ea_desktop_dir()?;
    if !dir.is_dir() {
        return Ok(vec![]);
    }
    let mut users = Vec::new();
    let entries = fs::read_dir(&dir).map_err(|e| format!("读取 EA Desktop 目录失败: {}", e))?;
    for ent in entries.flatten() {
        let name = ent.file_name().to_string_lossy().to_string();
        let Some(id) = name
            .strip_prefix("user_")
            .and_then(|s| s.strip_suffix(".ini"))
            .filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
        else {
            continue;
        };
        let path = ent.path();
        let lines = match read_user_ini(&path) {
            Ok(l) => l,
            Err(_) => continue,
        };
        let lower = ini_to_lower_map(&lines);
        users.push(EaDesktopUser {
            id: id.to_string(),
            name: display_name_from_ini(&lower, id),
            avatar: avatar_from_ini(&lower),
            config_path: path.to_string_lossy().to_string(),
        });
    }
    users.sort_by(|a, b| a.id.cmp(&b.id));
    Ok(users)
}

pub fn get_apex_launch_option_ea(ea_user_id: &str) -> Result<String, String> {
    let path = user_ini_path(ea_user_id)?;
    let lines = read_user_ini(&path)?;
    let idx = find_apex_command_line_line_index(&lines);
    Ok(idx.map(|i| lines[i].1.clone()).unwrap_or_default())
}

pub fn set_apex_launch_option_ea(ea_user_id: &str, launch_option: &str) -> Result<(), String> {
    let path = user_ini_path(ea_user_id)?;
    let mut lines = read_user_ini(&path)?;
    let key_canonical = "user.gamecommandline.origin.ofr.50.0002694";
    if let Some(idx) = find_apex_command_line_line_index(&lines) {
        lines[idx].1 = launch_option.to_string();
    } else {
        lines.push((key_canonical.to_string(), launch_option.to_string()));
    }
    write_user_ini(&path, &lines)
}