use crate::utils::run_commands;
use serde::Serialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
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
}
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))
}
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)
}