use std::collections::{HashMap, HashSet};
use std::path::Path;
use crate::utils::{create_backup, EspError};
use crate::io::StringFileReader;
use super::file::StringFile;
use super::io::build_filename_variants;
use super::{StringEntry, StringFileType};
#[derive(Debug, Clone)]
pub struct StringFileStats {
pub plugin_name: String,
pub language: String,
pub file_type: StringFileType,
pub string_count: usize,
pub total_content_size: usize,
pub total_raw_size: usize,
pub average_string_length: f64,
}
impl std::fmt::Display for StringFileStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== 字符串文件统计 ===")?;
writeln!(f, "插件名称: {}", self.plugin_name)?;
writeln!(f, "语言: {}", self.language)?;
writeln!(f, "文件类型: {:?}", self.file_type)?;
writeln!(f, "字符串数量: {}", self.string_count)?;
writeln!(f, "内容总大小: {} 字节", self.total_content_size)?;
writeln!(f, "原始数据大小: {} 字节", self.total_raw_size)?;
writeln!(f, "平均字符串长度: {:.1} 字符", self.average_string_length)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct StringFileSet {
pub files: HashMap<StringFileType, StringFile>,
pub plugin_name: String,
pub language: String,
}
impl StringFileSet {
pub fn new(plugin_name: String, language: String) -> Self {
StringFileSet {
files: HashMap::new(),
plugin_name,
language,
}
}
pub fn from_memory(
files_data: HashMap<StringFileType, &[u8]>,
plugin_name: String,
language: String,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut set = StringFileSet::new(plugin_name.clone(), language.clone());
for (file_type, data) in files_data {
let string_file = StringFile::from_bytes(data, plugin_name.clone(), language.clone(), file_type)?;
set.files.insert(file_type, string_file);
}
Ok(set)
}
pub fn load_from_directory_with_reader(
directory: &Path,
plugin_name: &str,
language: &str,
reader: &dyn StringFileReader,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut set = StringFileSet::new(plugin_name.to_string(), language.to_string());
for file_type in [
StringFileType::STRINGS,
StringFileType::ILSTRINGS,
StringFileType::DLSTRINGS,
] {
for filepath in build_filename_variants(directory, plugin_name, language, file_type) {
if filepath.exists() {
let string_file = reader.read(&filepath)?;
set.files.insert(file_type, string_file);
break;
}
}
}
Ok(set)
}
pub fn load_from_directory(
directory: &Path,
plugin_name: &str,
language: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut set = StringFileSet::new(plugin_name.to_string(), language.to_string());
for file_type in [
StringFileType::STRINGS,
StringFileType::ILSTRINGS,
StringFileType::DLSTRINGS,
] {
for filepath in build_filename_variants(directory, plugin_name, language, file_type) {
if filepath.exists() {
let string_file = StringFile::new(filepath)?;
set.files.insert(file_type, string_file);
break;
}
}
}
Ok(set)
}
pub(crate) fn load_auto_for_plugin(
plugin_path: &Path,
plugin_name: &str,
language: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
let plugin_dir = plugin_path.parent().ok_or("无法获取插件目录")?;
let search_dirs = [
plugin_dir.to_path_buf(),
plugin_dir.join("Strings"),
plugin_dir.join("strings"),
];
#[cfg(debug_assertions)]
let mut search_attempts: Vec<String> = Vec::new();
for dir in search_dirs {
if !dir.exists() {
#[cfg(debug_assertions)]
search_attempts.push(format!("{:?} (目录不存在)", dir));
continue;
}
match StringFileSet::load_from_directory(&dir, plugin_name, language) {
Ok(set) if !set.files.is_empty() => {
#[cfg(debug_assertions)]
eprintln!(
"✅ 已加载 STRING 文件: {} 个文件类型(从 {:?})",
set.files.len(),
dir
);
return Ok(set);
}
Ok(_) => {
#[cfg(debug_assertions)]
search_attempts.push(format!("{:?} (目录存在但无匹配文件)", dir));
}
Err(_e) => {
#[cfg(debug_assertions)]
search_attempts.push(format!("{:?} (加载失败: {})", dir, _e));
}
}
}
#[cfg(debug_assertions)]
{
eprintln!("⚠️ 文件系统中未找到 STRING 文件,已尝试以下路径:");
for attempt in &search_attempts {
eprintln!(" - {}", attempt);
}
eprintln!("🔍 尝试从 BSA 归档中加载...");
}
StringFileSet::load_from_bsa(plugin_path, plugin_name, language)
.map_err(|_e| "未找到任何 STRING 文件(文件系统和 BSA 都失败)".into())
}
pub fn get_file(&self, file_type: &StringFileType) -> Option<&StringFile> {
self.files.get(file_type)
}
pub fn get_file_mut(&mut self, file_type: &StringFileType) -> Option<&mut StringFile> {
self.files.get_mut(file_type)
}
pub fn add_file(&mut self, file_type: StringFileType, string_file: StringFile) {
self.files.insert(file_type, string_file);
}
pub fn get_string(&self, id: u32) -> Option<&StringEntry> {
for file_type in [
StringFileType::STRINGS,
StringFileType::ILSTRINGS,
StringFileType::DLSTRINGS,
] {
if let Some(file) = self.files.get(&file_type) {
if let Some(entry) = file.get_string(id) {
return Some(entry);
}
}
}
None
}
pub fn total_count(&self) -> usize {
self.files.values().map(|f| f.count()).sum()
}
pub fn get_all_string_ids(&self) -> Vec<u32> {
let mut all_ids = HashSet::new();
for file in self.files.values() {
for id in file.get_string_ids() {
all_ids.insert(id);
}
}
let mut ids: Vec<u32> = all_ids.into_iter().collect();
ids.sort();
ids
}
pub fn get_string_by_type(&self, file_type: StringFileType, id: u32) -> Option<&StringEntry> {
self.files.get(&file_type)?.get_string(id)
}
pub fn update_string(&mut self, file_type: StringFileType, id: u32, new_content: String) -> Result<(), EspError> {
if let Some(file) = self.files.get_mut(&file_type) {
file.update_string(id, new_content)
} else {
Err(EspError::InvalidFormat)
}
}
pub fn update_strings(&mut self, file_type: StringFileType, updates: HashMap<u32, String>) -> Result<(), EspError> {
if let Some(file) = self.files.get_mut(&file_type) {
file.update_strings(updates)
} else {
Err(EspError::InvalidFormat)
}
}
pub fn apply_translations(
&mut self,
translations: &HashMap<(StringFileType, u32), String>,
) -> Result<(), Box<dyn std::error::Error>> {
for ((file_type, id), content) in translations {
self.update_string(*file_type, *id, content.clone())?;
}
Ok(())
}
pub fn write_all(&self, directory: &Path) -> Result<(), Box<dyn std::error::Error>> {
for (file_type, file) in &self.files {
let filename = format!("{}_{}.{}", self.plugin_name, self.language, file_type.to_extension());
let filepath = directory.join(filename);
if filepath.exists() {
let backup_path = create_backup(&filepath)?;
println!("已创建备份: {:?}", backup_path);
}
file.write_to_file(filepath)?;
}
Ok(())
}
pub fn write_file(&self, file_type: StringFileType, directory: &Path) -> Result<(), Box<dyn std::error::Error>> {
if let Some(file) = self.files.get(&file_type) {
let filename = format!("{}_{}.{}", self.plugin_name, self.language, file_type.to_extension());
let filepath = directory.join(filename);
if filepath.exists() {
let backup_path = create_backup(&filepath)?;
println!("已创建备份: {:?}", backup_path);
}
file.write_to_file(filepath)?;
Ok(())
} else {
Err("指定的STRING文件类型不存在".into())
}
}
}