# ESP字符串提取工具 - 架构指南

## 目录

- [1. 项目概览](#1-项目概览)
- [2. 文件格式详解](#2-文件格式详解)
- [3. 核心架构](#3-核心架构)
- [4. 本地化机制](#4-本地化机制)
- [5. 数据流和工作流](#5-数据流和工作流)
- [6. 模块详解](#6-模块详解)
- [7. 关键设计决策](#7-关键设计决策)
- [8. 扩展指南](#8-扩展指南)

---

## 1. 项目概览

### 1.1 项目定位

本项目是一个用于处理Bethesda游戏引擎文件的Rust库和CLI工具，主要用于：
- 提取ESP/ESM/ESL文件中的可翻译字符串
- 应用翻译到游戏文件
- 处理外部字符串表文件（STRINGS/ILSTRINGS/DLSTRINGS）

### 1.2 支持的游戏

- The Elder Scrolls V: Skyrim Special Edition
- 理论上支持所有使用Creation Engine的游戏（Fallout 4、Fallout 76、Starfield等）

### 1.3 关键特性

```
✅ ESP/ESM/ESL文件解析
✅ STRING文件读写（STRINGS/ILSTRINGS/DLSTRINGS）
✅ 压缩记录支持（zlib）
✅ 多编码支持（UTF-8, Windows-1252等）
✅ 智能字符串过滤
✅ 本地化插件支持（StringID映射）
✅ 自动备份机制
```

---

## 2. 文件格式详解

### 2.1 ESP/ESM/ESL文件格式

#### 基本结构

```
┌─────────────────────────────────────┐
│ TES4 Header (Record)                │ ← 文件头，包含插件元信息
│  - Record Header (24 bytes)         │
│  - Subrecords (可变长度)             │
│    - HEDR: 文件版本                  │
│    - CNAM: 作者                      │
│    - MAST: 主文件列表                │
│    - ...                             │
├─────────────────────────────────────┤
│ GRUP 1 (Group)                      │ ← 记录组
│  - Group Header (24 bytes)          │
│  - Records / Subgroups               │
│    ├─ Record 1                       │
│    ├─ Record 2                       │
│    └─ GRUP (nested)                  │
├─────────────────────────────────────┤
│ GRUP 2 (Group)                      │
│  - ...                               │
└─────────────────────────────────────┘
```

#### Record Header（记录头）- 24字节

```
Offset  Size  Description
------  ----  -----------
0x00    4     Record Type (例如: "TES4", "WEAP", "NPC_")
0x04    4     Data Size (数据大小，不包含头部)
0x08    4     Flags (标志位)
0x0C    4     Form ID (唯一标识符)
0x10    2     Timestamp
0x12    2     Version Control Info
0x14    2     Internal Version
0x16    2     Unknown
```

#### 关键标志位

```rust
const MASTER_FILE = 0x00000001;     // ESM标志
const DELETED = 0x00000020;         // 已删除
const LOCALIZED = 0x00000080;       // 本地化（使用外部STRING文件）
const COMPRESSED = 0x00040000;      // 数据已压缩（zlib）
const LIGHT_MASTER = 0x00000200;    // 轻量级主文件（ESL）
```

#### Group Header（组头）- 24字节

```
Offset  Size  Description
------  ----  -----------
0x00    4     "GRUP" (固定标识)
0x04    4     Group Size (包含头部)
0x08    4     Label (组标签/类型)
0x0C    4     Group Type (0=Top, 1=World Children, ...)
0x10    2     Timestamp
0x12    2     Version Control Info
0x14    4     Unknown
0x18    4     Unknown
```

#### Subrecord（子记录）- 6字节头部

```
Offset  Size  Description
------  ----  -----------
0x00    4     Subrecord Type (例如: "FULL", "DESC", "EDID")
0x04    2     Data Size
0x06    N     Data (根据类型不同而不同)
```

---

### 2.2 STRING文件格式

Bethesda游戏使用外部字符串表文件存储本地化文本。有三种类型：

#### 文件类型分类

| 文件类型 | 用途 | 长度前缀 |
|---------|------|---------|
| **STRINGS** | 一般字符串（物品名称、描述等） | ❌ 无 |
| **ILSTRINGS** | 界面字符串 | ✅ 有 (4字节) |
| **DLSTRINGS** | 对话字符串 | ✅ 有 (4字节) |

#### 文件结构

```
┌──────────────────────────────────────┐
│ File Header (8 bytes)                │
│  - String Count (u32)                │ 字符串数量
│  - Data Size (u32)                   │ 数据区总大小
├──────────────────────────────────────┤
│ Directory Table (8 * Count bytes)   │ 目录表
│  - Entry 1:                          │
│    - String ID (u32)                 │
│    - Relative Offset (u32)           │
│  - Entry 2:                          │
│    - String ID (u32)                 │
│    - Relative Offset (u32)           │
│  - ...                               │
├──────────────────────────────────────┤
│ String Data (variable size)          │ 字符串数据区
│                                       │
│ [STRINGS格式]:                        │
│   - String Content (null-terminated) │
│                                       │
│ [DLSTRINGS/ILSTRINGS格式]:            │
│   - Length (u32)                     │ 长度前缀
│   - String Content (UTF-8)           │
│   - Null Terminator (0x00)           │
│                                       │
└──────────────────────────────────────┘
```

#### 关键特点

1. **StringID计算**：从0开始的自增序列，在ESP文件中引用
2. **编码**：UTF-8（官方文件）或Windows-1252（MOD文件）
3. **排序**：目录表按StringID排序
4. **长度前缀差异**：
   - STRINGS：直接存储null终止的字符串
   - DLSTRINGS/ILSTRINGS：4字节长度 + 字符串 + null终止

---

### 2.3 子记录类型 → STRING文件映射

这是**本地化ESP文件**的核心映射关系：

```rust
// 子记录类型决定应该从哪个STRING文件查找
// 基于Bethesda官方约定的简化规则
fn determine_string_file_type(record_type: &str, subrecord_type: &str) -> StringFileType {
    // 1. INFO 记录 → ILSTRINGS
    // INFO 记录包含对话信息，按照 Bethesda 约定存储在 ILSTRINGS 中
    if record_type == "INFO" {
        return StringFileType::ILSTRINGS;
    }

    // 2. DESC 和 CNAM 子记录 → DLSTRINGS
    // 这些通常是较长的描述性文本或内容，按照 Bethesda 约定存储在 DLSTRINGS 中
    if matches!(subrecord_type, "DESC" | "CNAM") {
        return StringFileType::DLSTRINGS;
    }

    // 3. 默认 → STRINGS
    // 包括 FULL, NNAM, SHRT, DNAM 等常规名称和简短文本
    StringFileType::STRINGS
}
```

#### 常见子记录映射表

| 子记录类型 | 说明 | STRING文件 | 示例记录类型 | 备注 |
|-----------|------|-----------|-------------|------|
| **DESC** | 描述文本 | **DLSTRINGS** | PERK, WEAP, ARMO, BOOK, MESG, SPEL, SHOU | 所有DESC都路由到DLSTRINGS |
| **CNAM** | 内容/日志 | **DLSTRINGS** | QUST, BOOK | 所有CNAM都路由到DLSTRINGS |
| FULL | 完整名称 | STRINGS | WEAP, ARMO, NPC_, BOOK, PERK | 通用名称字段 |
| NNAM | 任务目标 | STRINGS | QUST | 任务目标文本 |
| SHRT | 简短名称 | STRINGS | NPC_ | 角色简称 |
| DNAM | 数据/描述 | STRINGS | MGEF | 魔法效果说明（不是DESC） |
| RNAM | 提示文本 | STRINGS | ACTI, FLOR | 交互提示 |
| NAM1 | 对话文本 | **ILSTRINGS** | INFO | INFO记录的对话 |
| ITXT | 界面文本 | STRINGS | MESG | 界面元素（不是INFO） |
| TNAM | 文本内容 | STRINGS | NOTE, WOOP | 笔记/树木文本 |
| RDMP | 地区描述 | STRINGS | REGN | 地区映射描述 |

**简化规则说明：**
- **INFO 记录** → 所有子记录都路由到 **ILSTRINGS**
- **DESC/CNAM 子记录** → 无论record类型，都路由到 **DLSTRINGS**
- **其他所有** → 默认路由到 **STRINGS**

---

## 3. 核心架构

### 3.1 模块组织

```
esp_extractor/
├── lib.rs                    # 库入口，公共API
├── main.rs                   # CLI工具入口
│
├── datatypes.rs              # 基础数据类型
│   ├── RawString             # 多编码字符串
│   ├── RecordFlags           # 记录标志位
│   └── read/write函数         # 字节序处理
│
├── plugin/                   # ESP插件解析器（核心）- v0.7.0 模块化
│   ├── mod.rs                # Plugin 结构体和公共接口
│   ├── parser.rs             # 加载和解析逻辑
│   ├── strings.rs            # 字符串提取
│   ├── translate.rs          # 翻译应用
│   ├── writer.rs             # 文件写入
│   ├── stats.rs              # 统计信息
│   └── esl.rs                # ESL FormID 重编号
│
├── record.rs                 # 记录解析
│   └── Record                # 表示单个ESP记录
│
├── group.rs                  # 组解析
│   ├── Group                 # 表示GRUP块
│   ├── GroupType             # 组类型枚举
│   └── GroupChild            # 递归子结构
│
├── subrecord.rs              # 子记录解析
│   └── Subrecord             # 表示子记录
│
├── string_types.rs           # 提取字符串类型
│   └── ExtractedString       # 提取的字符串结构
│
├── string_file/              # STRING文件处理 - v0.7.0 模块化
│   ├── mod.rs                # 公共接口和基础类型
│   ├── file.rs               # StringFile 结构体和方法
│   ├── set.rs                # StringFileSet 集合操作
│   ├── bsa.rs                # BSA fallback 逻辑
│   ├── io.rs                 # 文件名解析工具
│   └── tests.rs              # 测试模块
│
├── string_routes/            # 字符串路由模块 - v0.7.0 新增
│   ├── mod.rs                # 公共接口导出
│   ├── router.rs             # StringRouter trait 和实现
│   └── data.rs               # 加载 string_records.json
│
├── io/                       # IO 抽象层 - v0.4.0 引入
│   ├── mod.rs                # 公共接口
│   ├── esp_io.rs             # EspReader/EspWriter trait
│   └── string_io.rs          # StringFileReader/Writer trait
│
├── editor/                   # 编辑器层 - v0.4.0 引入
│   ├── mod.rs                # PluginEditor
│   └── delta.rs              # TranslationDelta 变更追踪
│
├── bsa/                      # BSA 归档支持 - v0.6.0 引入
│   ├── mod.rs                # 公共接口
│   ├── archive.rs            # BsaArchive
│   └── strings_provider.rs   # BsaStringsProvider
│
├── utils.rs                  # 工具函数
│   ├── is_valid_string       # 字符串验证
│   ├── create_backup         # 备份创建
│   └── EspError              # 错误类型
│
└── debug.rs                  # 调试工具（仅debug模式）
    └── EspDebugger           # 文件对比和分析
```

### 3.2 核心数据结构

#### Plugin（插件）

```rust
pub struct Plugin {
    pub path: PathBuf,                              // 文件路径
    pub header: Record,                             // TES4头部
    pub groups: Vec<Group>,                         // 所有GRUP组
    pub masters: Vec<String>,                       // 主文件列表
    pub string_records: HashMap<String, Vec<String>>, // 字符串记录定义
    string_files: Option<StringFileSet>,            // 外部STRING文件（私有，自动加载）
    language: String,                               // 语言标识（用于STRING文件）
}
```

**关键特性**：
- `string_files`字段私有，自动在`Plugin::new()`中加载
- 如果检测到`LOCALIZED`标志，自动搜索并加载STRING文件
- 支持多路径搜索：同目录、Strings子目录、strings子目录
- 支持大小写不敏感的文件名匹配

#### Record（记录）

```rust
pub struct Record {
    pub record_type: String,        // 记录类型（4字符）
    pub data_size: u32,             // 数据大小
    pub flags: u32,                 // 标志位
    pub form_id: u32,               // FormID
    pub timestamp: u16,
    pub version_control_info: u16,
    pub internal_version: u16,
    pub unknown: u16,
    pub subrecords: Vec<Subrecord>, // 子记录列表
    pub original_data: Vec<u8>,     // 原始数据（压缩时）
    pub modified: bool,             // 是否已修改
}
```

#### Group（组）

```rust
pub struct Group {
    pub group_type: GroupType,      // 组类型
    pub label: u32,                 // 标签
    pub children: Vec<GroupChild>,  // 子元素（递归）
}

pub enum GroupChild {
    Record(Record),                 // 记录
    Group(Box<Group>),              // 子组（递归）
}
```

#### StringFile（STRING文件）

```rust
pub struct StringFile {
    pub path: PathBuf,
    pub file_type: StringFileType,               // STRINGS/ILSTRINGS/DLSTRINGS
    pub plugin_name: String,                     // 插件名
    pub language: String,                        // 语言（english/chinese等）
    pub entries: HashMap<u32, StringEntry>,      // StringID -> 字符串
}

pub struct StringEntry {
    pub id: u32,                    // StringID
    pub content: String,            // 字符串内容（UTF-8）
    pub raw_data: Vec<u8>,          // 原始字节数据
    pub length: Option<u32>,        // 长度（DLSTRINGS/ILSTRINGS）
    // 元数据（用于调试）
    pub directory_address: u64,
    pub relative_offset: u32,
    pub absolute_offset: u64,
}
```

---

## 4. 本地化机制

### 4.1 本地化标志位

```rust
const LOCALIZED = 0x00000080;  // 十进制128

// 检测本地化插件
fn is_localized(header_flags: u32) -> bool {
    header_flags & 0x00000080 != 0
}
```

当ESP文件的TES4头部设置了`LOCALIZED`标志时，所有字符串子记录存储的是**4字节StringID**，而非实际字符串。

### 4.2 本地化工作流

#### 提取字符串流程（本地化ESP）

```
1. 读取ESP文件
   ├─ 检测LOCALIZED标志
   └─ 如果本地化：需要加载STRING文件

2. 对于每个字符串子记录（FULL, DESC等）
   ├─ 读取4字节StringID
   ├─ 确定STRING文件类型（STRINGS/ILSTRINGS/DLSTRINGS）
   └─ 从对应STRING文件查找实际文本

3. 返回ExtractedString
   ├─ string_id: 原始StringID
   ├─ string_file_type: 所属STRING文件类型
   └─ original_text: 从STRING文件获取的实际文本
```

#### 应用翻译流程（本地化ESP）

```
1. 读取翻译的ExtractedString列表

2. 构建翻译映射
   ├─ (StringFileType, StringID) -> 翻译文本
   └─ 例如: (STRINGS, 123) -> "钢剑"

3. 更新STRING文件
   ├─ StringFile::update_string(id, new_text)
   └─ 批量更新所有翻译

4. 写回STRING文件
   ├─ 重建二进制数据
   ├─ 创建备份（.bak）
   └─ 写入文件
```

### 4.3 文件命名约定

```
插件名_语言.扩展名

示例：
- Skyrim_english.STRINGS
- Skyrim_chinese.STRINGS
- MyMod_english.DLSTRINGS
- MyMod_japanese.ILSTRINGS
```

### 4.4 当前实现状态

| 功能 | 状态 | 备注 |
|------|------|------|
| 检测本地化标志 | ✅ 已实现 | plugin.rs:76 |
| 读取StringID | ✅ 已实现 | plugin.rs:239 |
| STRING文件读取 | ✅ 已实现 | string_file.rs:120-272 |
| STRING文件写入 | ✅ 已实现 | string_file.rs:369-455 |
| Plugin集成STRING | ✅ **已实现** | plugin.rs:25-26, 自动加载 |
| StringID查找映射 | ✅ **已实现** | plugin.rs:159-176, determine_string_file_type |
| 字符串提取支持 | ✅ **已实现** | plugin.rs:229-290, 自动从STRING查找 |
| 统一翻译应用 | ✅ **已实现** | plugin.rs:383-548, apply_translations_unified |
| 多路径搜索 | ✅ **已实现** | plugin.rs:86-126, 支持Strings子目录 |
| 大小写不敏感 | ✅ **已实现** | string_file.rs:494-540, 自动尝试多种变体 |

---

## 5. 数据流和工作流

### 5.1 字符串提取流程

#### 普通ESP（非本地化）

```
文件 (MyMod.esp)
    ↓
Plugin::new(path)
    ↓
解析TES4头部 → 检测flags (LOCALIZED=0)
    ↓
解析所有GRUP → 递归处理子组和记录
    ↓
对于每个Record
    ↓
提取符合类型的Subrecord（FULL, DESC等）
    ↓
RawString::parse_zstring() → 直接读取字符串
    ↓
ExtractedString {
    editor_id,
    form_id,
    original_text,  ← 直接从ESP读取
    record_type,
    subrecord_type,
}
    ↓
JSON输出
```

#### 本地化ESP（带STRING文件）

```
文件 (MyMod.esp) + STRING文件集
    ↓
Plugin::new(path)
    ↓
解析TES4头部 → 检测flags (LOCALIZED=1)
    ↓
Plugin::load_string_files() → 查找并加载STRING文件
    ├─ MyMod_english.STRINGS
    ├─ MyMod_english.ILSTRINGS
    └─ MyMod_english.DLSTRINGS
    ↓
解析所有GRUP → 递归处理
    ↓
对于每个字符串Subrecord
    ↓
读取4字节StringID
    ↓
determine_string_file_type(record_type, subrecord_type)
    ↓
从对应STRING文件查找
    ├─ StringFileSet::get_string_by_type(STRINGS, id)
    ├─ StringFileSet::get_string_by_type(ILSTRINGS, id)
    └─ StringFileSet::get_string_by_type(DLSTRINGS, id)
    ↓
ExtractedString {
    editor_id,
    form_id,
    original_text,      ← 从STRING文件获取
    record_type,
    subrecord_type,
    string_id,          ← 保存StringID
    string_file_type,   ← 保存文件类型
}
    ↓
JSON输出
```

### 5.2 翻译应用流程

#### 普通ESP

```
翻译JSON → Vec<ExtractedString>
    ↓
Plugin::new(input_esp)
    ↓
创建翻译映射: HashMap<UniqueKey, ExtractedString>
    key = editor_id + form_id + record_type + subrecord_type
    ↓
Plugin::apply_translation_map()
    ↓
递归遍历所有Record和Subrecord
    ↓
匹配UniqueKey → 替换Subrecord.data
    ↓
标记Record为modified
    ↓
Plugin::write_to_file(output_esp)
    ↓
重建文件（保持压缩状态）
    ↓
输出文件
```

#### 本地化ESP

```
翻译JSON → Vec<ExtractedString> (包含string_id)
    ↓
Plugin::new(input_esp)
    ↓
Plugin::load_string_files()
    ↓
构建翻译映射: HashMap<(StringFileType, StringID), String>
    ↓
StringFileSet::apply_translations(translations)
    ↓
对每个(file_type, string_id, text)
    └─ StringFile::update_string(id, text)
        └─ 更新StringEntry.content
    ↓
StringFileSet::write_all(output_dir)
    ↓
对每个StringFile
    ├─ 创建备份 (.bak)
    ├─ rebuild() → 重建二进制数据
    └─ write_to_file()
    ↓
输出STRING文件
```

### 5.3 STRING文件读写流程

#### 读取流程

```
StringFile::new(path)
    ↓
解析文件名 → (plugin_name, language, file_type)
    ↓
读取文件数据
    ↓
解析文件头（8字节）
    ├─ string_count (u32)
    └─ data_size (u32)
    ↓
读取目录表（8 * count 字节）
    └─ 每个条目：StringID + Relative Offset
    ↓
对每个目录条目
    ├─ 计算绝对偏移：string_data_start + relative_offset
    ├─ 读取字符串数据
    │   ├─ [DLSTRINGS/ILSTRINGS] 读取4字节长度
    │   ├─ 读取字符串内容（UTF-8）
    │   └─ 读取null终止符
    └─ 创建StringEntry
    ↓
HashMap<StringID, StringEntry>
```

#### 写入流程

```
StringFile::write_to_file(path)
    ↓
rebuild() → 重建二进制数据
    ↓
1. 写入文件头
   ├─ string_count (u32)
   └─ data_size (u32)
    ↓
2. 准备排序的StringID列表
   └─ ids.sort()
    ↓
3. 计算每个字符串的偏移量
   └─ offset += entry.get_total_size()
    ↓
4. 写入目录表
   └─ 对每个ID：write_u32(id) + write_u32(offset)
    ↓
5. 写入字符串数据
   ├─ [DLSTRINGS/ILSTRINGS] write_u32(length)
   ├─ write(content.as_bytes())
   └─ write(0x00)  // null终止符
    ↓
Vec<u8> (完整二进制数据)
    ↓
创建备份（如果文件存在）
    ↓
fs::write(path, data)
```

---

## 6. 模块详解

### 6.1 plugin.rs - 核心插件解析器

**职责**：管理整个ESP文件的解析和操作

**关键方法**：

```rust
impl Plugin {
    // === 构造与解析 ===
    pub fn new(path: PathBuf) -> Result<Self>
    fn validate_esp_file(header: &Record) -> Result<()>
    fn parse_groups(cursor, data) -> Result<Vec<Group>>
    fn extract_masters(header: &Record) -> Vec<String>
    fn load_string_records() -> Result<HashMap>

    // === 字符串提取 ===
    pub fn extract_strings(&self) -> Vec<ExtractedString>
    fn extract_group_strings(&self, group: &Group) -> Vec<ExtractedString>
    fn extract_record_strings(&self, record: &Record) -> Vec<ExtractedString>
    fn extract_string_from_subrecord(...) -> Option<ExtractedString>

    // === 翻译应用 ===
    pub fn apply_translations(input, output, translations) -> Result<()>
    fn create_translation_map(Vec<ExtractedString>) -> HashMap
    fn apply_translation_map(&mut self, map: &HashMap) -> Result<()>
    fn apply_translation_to_group(&mut self, group, map, masters) -> Result<()>
    fn apply_translation_to_record(&mut self, record, map, masters) -> Result<bool>

    // === 文件写入 ===
    pub fn write_to_file(&self, path: PathBuf) -> Result<()>

    // === 信息获取 ===
    pub fn get_name(&self) -> &str
    pub fn get_type(&self) -> &str
    pub fn is_master(&self) -> bool
    pub fn is_localized(&self) -> bool
    pub fn get_stats(&self) -> PluginStats

    // === 工具方法 ===
    fn format_form_id(&self, form_id: u32) -> String
    fn count_group_records(&self, group: &Group) -> usize
    fn count_subgroups(&self, group: &Group) -> usize
}
```

**关键设计**：
- 递归处理GRUP嵌套结构
- 惰性加载：只在需要时解压缩数据
- FormID处理：自动识别主文件索引

### 6.2 record.rs - 记录解析

**职责**：处理单个ESP记录的解析和重建

**关键方法**：

```rust
impl Record {
    pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result<Self>

    // 解压缩记录数据
    fn decompress_data(data: &[u8]) -> Result<Vec<u8>>

    // 子记录解析
    fn parse_subrecords(data: &[u8]) -> Result<Vec<Subrecord>>

    // 获取EDID（编辑器ID）
    pub fn get_editor_id(&self) -> Option<String>

    // 重建记录
    pub fn rebuild(&self) -> Result<Vec<u8>>
    fn rebuild_subrecords(&self) -> Vec<u8>
}
```

**压缩处理**：
```rust
if flags & 0x00040000 != 0 {
    // 压缩记录
    // 1. 保存原始压缩数据
    // 2. 解压缩（zlib）
    // 3. 解析子记录
    // 4. 重建时重新压缩
}
```

### 6.3 group.rs - 组解析

**职责**：处理GRUP块的递归结构

**关键结构**：

```rust
pub enum GroupType {
    Normal,      // 顶级组
    World,       // 世界子组
    Cell,        // 单元格子组
}

pub enum GroupChild {
    Record(Record),        // 叶子节点：记录
    Group(Box<Group>),     // 递归节点：子组
}

impl Group {
    pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result<Self>
    fn parse_children(...) -> Result<Vec<GroupChild>>
    pub fn rebuild(&self) -> Result<Vec<u8>>
}
```

**递归处理**：
```rust
fn parse_children(...) -> Result<Vec<GroupChild>> {
    while position < group_end {
        let type_signature = read_type();

        match type_signature {
            "GRUP" => {
                // 递归解析子组
                let subgroup = Group::parse(cursor)?;
                children.push(GroupChild::Group(Box::new(subgroup)));
            }
            _ => {
                // 解析记录
                let record = Record::parse(cursor)?;
                children.push(GroupChild::Record(record));
            }
        }
    }
}
```

### 6.4 string_file.rs - STRING文件处理

**职责**：处理外部字符串表文件

**关键方法**：

```rust
impl StringFile {
    // === 读取 ===
    pub fn new(path: PathBuf) -> Result<Self>
    fn parse_filename(path: &Path) -> Result<(String, String, StringFileType)>
    fn parse_file(path, file_type) -> Result<HashMap<u32, StringEntry>>
    fn read_string_data(...) -> Result<(String, Vec<u8>, Option<u32>)>

    // === 查询 ===
    pub fn get_string(&self, id: u32) -> Option<&StringEntry>
    pub fn get_string_ids(&self) -> Vec<u32>
    pub fn find_strings_containing(&self, text: &str) -> Vec<&StringEntry>

    // === 修改 ===
    pub fn update_string(&mut self, id: u32, content: String) -> Result<()>
    pub fn update_strings(&mut self, updates: HashMap<u32, String>) -> Result<()>
    pub fn add_string(&mut self, id: u32, content: String) -> Result<()>
    pub fn remove_string(&mut self, id: u32) -> Option<StringEntry>

    // === 写入 ===
    pub fn rebuild(&self) -> Result<Vec<u8>>
    pub fn write_to_file(&self, path: PathBuf) -> Result<()>
}

impl StringFileSet {
    pub fn new(plugin_name: String, language: String) -> Self
    pub fn load_from_directory(dir, plugin, language) -> Result<Self>

    pub fn get_string(&self, id: u32) -> Option<&StringEntry>
    pub fn get_string_by_type(&self, type: StringFileType, id: u32) -> Option<&StringEntry>

    pub fn update_string(&mut self, type, id, content) -> Result<()>
    pub fn apply_translations(&mut self, translations) -> Result<()>

    pub fn write_all(&self, directory: &Path) -> Result<()>
    pub fn write_file(&self, type, directory) -> Result<()>
}
```

**关键点**：
- **自动检测文件类型**：通过扩展名识别
- **文件名解析**：提取插件名和语言
- **大小计算一致性**：使用`content.as_bytes().len()`而不是`raw_data.len()`

### 6.5 datatypes.rs - 基础数据类型

**职责**：提供底层数据结构和编码处理

**RawString - 多编码字符串**：

```rust
pub struct RawString {
    pub content: String,
    pub encoding: String,
}

impl RawString {
    pub fn decode(data: &[u8]) -> Self
    pub fn parse_zstring(data: &[u8]) -> Self       // null终止
    pub fn parse_bstring(cursor) -> Result<Self>    // 长度前缀
}
```

**支持的编码**：
- UTF-8（优先）
- Windows-1252（西欧）
- Windows-1250（中欧）
- Windows-1251（西里尔文）

**RecordFlags - 位标志**：

```rust
bitflags! {
    pub struct RecordFlags: u32 {
        const MASTER_FILE = 0x00000001;
        const LOCALIZED = 0x00000080;
        const COMPRESSED = 0x00040000;
        const LIGHT_MASTER = 0x00000200;
        // ... 更多标志位
    }
}
```

### 6.6 utils.rs - 工具函数

**字符串验证**：

```rust
pub fn is_valid_string(text: &str) -> bool {
    // 1. 检查黑名单
    if blacklist.contains(text) { return false; }

    // 2. 检查白名单
    if is_whitelisted(text) { return true; }

    // 3. 过滤变量名
    if is_variable_name(text) { return false; }

    // 4. 检查字符有效性
    text.chars().all(|c| !c.is_control() || c.is_whitespace())
}

fn is_variable_name(text: &str) -> bool {
    is_camel_case(text) || is_snake_case(text)
}
```

**备份创建**：

```rust
pub fn create_backup(file_path: &Path) -> Result<PathBuf> {
    let timestamp = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S");
    let backup_path = file_path.with_extension(format!("{}.bak", timestamp));
    fs::copy(file_path, &backup_path)?;
    Ok(backup_path)
}
```

---

## 7. 关键设计决策

### 7.1 为什么使用HashMap而不是Vec？

**StringFile.entries: HashMap<u32, StringEntry>**

**原因**：
1. ✅ O(1) 查找复杂度（StringID查找）
2. ✅ 支持稀疏ID（ID可能不连续）
3. ✅ 方便更新和删除
4. ⚠️ 写入时需要排序（但只需一次）

### 7.2 为什么保留原始压缩数据？

**Record.original_data: Vec<u8>**

**原因**：
1. ✅ 验证解压缩正确性
2. ✅ 未修改时直接写回原数据
3. ✅ 避免重新压缩导致的数据差异
4. ⚠️ 增加内存占用（但仅压缩记录）

### 7.3 为什么ExtractedString使用唯一键？

**UniqueKey = editor_id + form_id + record_type + subrecord_type**

**原因**：
1. ✅ 避免ID冲突（不同对象可能共享FormID）
2. ✅ 支持部分翻译（只翻译特定字段）
3. ✅ 精确匹配（不会误替换）

示例冲突：
```
WEAP [0x12345] FULL = "Iron Sword"
WEAP [0x12345] DESC = "A simple iron sword"
```

如果只用FormID，会混淆FULL和DESC。

### 7.4 为什么STRING文件需要长度前缀？

**DLSTRINGS/ILSTRINGS有长度前缀，STRINGS没有**

**原因**：
- STRINGS: 简单字符串，null终止足够
- DLSTRINGS: 对话可能包含多行/特殊字符，长度前缀更可靠
- ILSTRINGS: 界面字符串需要快速跳过（性能优化）

### 7.5 为什么使用Copy trait for StringFileType？

```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StringFileType { ... }
```

**原因**：
1. ✅ 避免所有权问题（可以直接解引用）
2. ✅ 小类型（仅3个变体），Copy无性能损失
3. ✅ 简化API（不需要.clone()）

### 7.6 为什么get_total_size使用content而不是raw_data？

```rust
// 错误的实现
let content_size = self.raw_data.len() as u32;

// 正确的实现
let content_size = self.content.as_bytes().len() as u32;
```

**原因**：
- `raw_data` 可能包含额外数据（长度前缀、元数据）
- `content` 是实际字符串内容，与rebuild()写入的数据一致
- 保证大小计算与实际写入的数据匹配

---

## 8. 扩展指南

### 8.1 添加对新游戏的支持

1. **验证文件格式兼容性**：
   ```rust
   // 检查Record Header是否为24字节
   // 检查Group Header是否为24字节
   // 检查是否使用相同的压缩算法（zlib）
   ```

2. **添加新的记录类型**：
   ```json
   // data/string_records.json
   {
     "NEWTYPE": ["FULL", "DESC"],
     ...
   }
   ```

3. **测试关键功能**：
   - 文件解析
   - 压缩记录处理
   - 字符串提取
   - 翻译应用

### 8.2 本地化支持实现总结

**✅ 已完成实现**（版本 3.0+）

本地化支持已经完全实现，提供了"无感"的API设计，自动处理本地化和非本地化插件。

#### 核心实现

```rust
// 1. Plugin结构已扩展
pub struct Plugin {
    pub path: PathBuf,
    pub header: Record,
    pub groups: Vec<Group>,
    pub masters: Vec<String>,
    pub string_records: HashMap<String, Vec<String>>,
    string_files: Option<StringFileSet>,  // ✅ 已实现（私有字段）
    language: String,                     // ✅ 已实现
}

// 2. 统一的API接口 - 自动处理本地化
impl Plugin {
    /// 创建插件实例（自动加载STRING文件）
    pub fn new(path: PathBuf, language: Option<&str>) -> Result<Self> {
        // ✅ 自动检测LOCALIZED标志
        // ✅ 自动搜索并加载STRING文件（支持多路径、大小写不敏感）
        // ✅ 支持语言参数，默认为"english"
    }

    /// 统一的翻译应用接口（自动判断本地化/非本地化）
    pub fn apply_translations_unified(
        &mut self,
        translations: Vec<ExtractedString>,
        output_dir: Option<&Path>,
    ) -> Result<()> {
        // ✅ 本地化插件 → 写入STRING文件到 output_dir/strings/
        // ✅ 普通插件 → 写入ESP文件到 output_dir/xxx.esp
    }
}

// 3. StringID类型映射（已实现）
impl Plugin {
    fn determine_string_file_type(
        record_type: &str,
        subrecord_type: &str
    ) -> StringFileType {
        // ✅ 对话记录 (DIAL/INFO) 或对话子记录 (NAM1/RNAM) → DLSTRINGS
        // ✅ 界面子记录 (ITXT/CTDA) → ILSTRINGS
        // ✅ 其他所有字符串子记录 → STRINGS (默认)
    }
}
```

#### 关键特性

1. **自动STRING文件加载**
   - 检测到`LOCALIZED`标志时自动加载
   - 支持多路径搜索：同目录、`Strings/`子目录、`strings/`子目录
   - 大小写不敏感文件名匹配（原始名称、小写、大写）

2. **统一的字符串提取**
   - 本地化插件：自动从STRING文件读取实际文本
   - 普通插件：直接从ESP读取
   - 输出统一的JSON格式（ExtractedString结构保持不变）

3. **智能翻译应用**
   - 自动判断插件类型
   - 本地化插件：通过遍历ESP构建StringID映射，更新STRING文件
   - 普通插件：直接修改ESP文件
   - 支持灵活的输出路径

4. **ExtractedString设计决策**
   - ✅ **未添加**`string_id`和`string_file_type`字段
   - 原因：JSON格式保持统一简洁，应用翻译时通过遍历ESP重新获取StringID
   - 优点：对外接口完全透明，用户无需关心内部实现

#### 使用示例

```rust
// 读取插件（自动处理本地化）
let plugin = Plugin::new("MyMod.esp".into(), Some("english"))?;
let strings = plugin.extract_strings();

// 统一的JSON输出（本地化和非本地化格式完全一致）
let json = serde_json::to_string_pretty(&strings)?;

// 应用翻译（自动判断写入目标）
let mut plugin = Plugin::new("MyMod.esp".into(), Some("english"))?;
plugin.apply_translations_unified(translations, Some("output".as_ref()))?;
// - 本地化插件 → output/strings/*.STRINGS
// - 普通插件 → output/MyMod.esp
```

#### 测试验证

| 测试插件 | 类型 | STRING文件 | 提取结果 | 状态 |
|---------|------|-----------|---------|------|
| GostedDimensionalRift.esp | 普通插件 | 无 | 520个字符串 | ✅ 通过 |
| Dismembering Framework.esm | 本地化插件 | 3个文件 | 8个字符串 | ✅ 通过 |
| ccbgssse001-fish.esm | 本地化插件 | 3个文件 (808+219) | STRING独立加载正常 | ✅ 通过* |

*注：ccbgssse001-fish.esm完整解析失败是原有解析器的问题，STRING文件加载和处理功能本身正常。

### 8.3 性能优化建议

1. **大文件处理**：
   ```rust
   // 使用内存映射
   use memmap2::Mmap;

   let file = File::open(path)?;
   let mmap = unsafe { Mmap::map(&file)? };
   let mut cursor = Cursor::new(&mmap[..]);
   ```

2. **并行解析**：
   ```rust
   // 使用rayon并行处理组
   use rayon::prelude::*;

   let strings: Vec<ExtractedString> = groups
       .par_iter()
       .flat_map(|g| extract_group_strings(g))
       .collect();
   ```

3. **惰性加载**：
   ```rust
   // 只在需要时解压缩
   pub struct Record {
       data: LazyData,  // 延迟加载
   }

   enum LazyData {
       Compressed(Vec<u8>),
       Decompressed(Vec<u8>),
   }
   ```

### 8.4 添加新的子记录类型

1. **更新string_records.json**：
   ```json
   {
     "NEWREC": ["FULL", "DESC", "NEWFIELD"]
   }
   ```

2. **测试提取**：
   ```rust
   #[test]
   fn test_new_record_type() {
       let plugin = Plugin::new("test.esp")?;
       let strings = plugin.extract_strings();

       let new_strings: Vec<_> = strings.iter()
           .filter(|s| s.record_type == "NEWREC")
           .collect();

       assert!(!new_strings.is_empty());
   }
   ```

### 8.5 CLI工具增强

**建议添加的命令**：

```bash
# 查看插件信息
esp_extractor info MyMod.esp

# 验证文件完整性
esp_extractor validate MyMod.esp

# 对比两个插件
esp_extractor diff Original.esp Modified.esp

# 合并多个翻译文件
esp_extractor merge base.json patch1.json patch2.json -o merged.json

# 导出为其他格式
esp_extractor export MyMod.esp -f csv -o output.csv
esp_extractor export MyMod.esp -f po -o output.po  # GNU gettext format
```

---

## 附录

### A. 参考资料

- [UESP - Mod File Format](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format)
- [UESP - String Table File Format](https://en.uesp.net/wiki/Skyrim_Mod:String_Table_File_Format)
- [Creation Kit Documentation](https://www.creationkit.com/)
- [xEdit](https://github.com/TES5Edit/TES5Edit) - ESP文件编辑器（参考实现）

### B. 常见FormID前缀

| FormID范围 | 来源 | 说明 |
|-----------|------|------|
| 0x000000-0x000FFF | Skyrim.esm | 原版游戏 |
| 0x01000000- | 第一个主文件 | 根据加载顺序 |
| 0x02000000- | 第二个主文件 | |
| 0xFE000000- | ESL文件 | 轻量级插件 |

### C. 文件扩展名约定

| 扩展名 | 类型 | 说明 |
|-------|------|------|
| .esp | Plugin | 普通插件 |
| .esm | Master | 主文件（依赖项） |
| .esl | Light | 轻量级插件（不占用加载顺序） |
| .STRINGS | Strings | 一般字符串表 |
| .ILSTRINGS | IL Strings | 界面字符串表 |
| .DLSTRINGS | DL Strings | 对话字符串表 |

### D. 调试技巧

**查看文件结构**：
```bash
cargo run --features cli -- -i MyMod.esp --stats

# 对比两个文件
cargo run --features cli -- -i Original.esp --compare-files Modified.esp
```

**启用调试输出**：
```rust
#[cfg(debug_assertions)]
println!("调试信息: {}", value);
```

**使用hexdump查看二进制**：
```bash
hexdump -C MyMod.esp | head -100
```

---

## 版本历史

| 版本 | 日期 | 更新内容 |
|------|------|---------|
| 1.0 | 2025-11-12 | 初始版本，完整架构文档 |
| 3.0 | 2025-11-13 | **本地化支持完整实现**：<br>- 添加STRING文件自动加载功能<br>- 实现StringID类型映射和查找<br>- 统一的翻译应用接口（自动判断本地化/非本地化）<br>- 支持多路径搜索和大小写不敏感匹配<br>- 测试验证：普通插件520个字符串，本地化插件808+219个字符串 |
| 4.0 | 2025-11-27 | **代码架构重构 (v0.7.0)**：<br>- plugin.rs 拆分为 7 个子模块<br>- string_file.rs 拆分为 6 个子模块<br>- 新增 string_routes 模块（StringRouter trait）<br>- IO 抽象层增强（依赖注入支持）<br>- 模块组织更新，反映新目录结构 |

---

**文档维护**：请在重大架构变更时更新此文档。

**贡献者**：如果你修改了核心架构，请在此文档中记录你的设计决策和原因。
