# FFRS 游戏数据提取方法（数值 / 名称 / 特性）

> Final Fantasy Resonance（最终幻想 共鸣，2026-10-22，UE 5.6，IoStore / Oodle，Denuvo）
> 本文记录**从游戏本体直接读取**装备数值、名称、特性文本的完整可复现流程，全部结论均为本机实测。
> 本作共打包 **9 个 `Game.locres` 语言目录**：8 种玩家可选文本语言 + 1 个开发用伪本地化（`is`）。wiki 已全部支持，详见第 6 节。
> 配套脚本：`../tools/extract_ffrs.py`（游戏侧解析）、`../tools/build_wiki_data.py`（生成 wiki 数据集）、`../tools/fetch_media.py`（图标随站点部署）、`../tools/verify_wiki.py`（逐语言验证）。

---

## 0. 一句话总览

| 要拿的东西 | 在哪个文件 | 怎么拿 |
|---|---|---|
| 装备**数值**、价格、效果 ID | `Content/Datatable/Item/DT_EquipmentItemData.uexp` | 二进制行解析（第 4 节） |
| 装备**分类 / 细分类型** | `DT_EquipmentItemData.uasset` 的 NameMap + 行 flags | 见 4.5 |
| 日文**名称 / 描述** | `Content/Datatable/Strings/ST_Item/ST_Equipment{Name,Description}.uexp` | FString 表（第 5 节） |
| **官方 8 语言 + 开发伪本地化** 全文本 | `FFRS-Windows.pak` → `Content/Localization/Game/{ja,en,zh-Hans,zh-Hant,ko,de,fr,es,is}/Game.locres` | locres 解析（第 6 节） |
| **支持语言清单 / 语言自称** | `Game.locmeta`、`FFRS/Config/DefaultGame.ini`、`ST_UI_Language` | 见 6.5 |
| 效果的**具体文案**（火+20%、破防+10%…） | `ST_EquipmentDescription` + 效果 ID 聚合 | 第 7 节 |
| 道具 / 武具 / 技能 / 商店 / 敌人 / 任务 / 地图 | `Content/Datatable/**` 其余 DT_ 表 | 第 8 节 |

**重要前提**：本作 pak **没有 AES 加密**，IoStore 容器可直接读；但 `.exe` 有 Denuvo，**不要试图用 UE4SS 注入**（实测 AOB 全失败，见第 11 节），静态解包才是正路。

---

## 1. 文件布局与工具链

### 1.1 目录

```
<GAME>/FFRS/Content/Paks/
├── FFRS-Windows.utoc        # 16 MB：IoStore 目录表（文件名索引）
├── FFRS-Windows.ucas        # 11 GB：IoStore 数据块
├── FFRS-Windows.pak         # 传统 pak：非资产文件（含 Game.locres）
├── global.utoc / global.ucas# 全局容器：解包时必须放在同一目录
└── ~mods/                   # mod 目录（文件名以 _P 结尾，自然排序越后优先级越高）
```

`.utoc` 是**纯文本可见的路径索引**，最快摸清数据布局的方法：

```bash
strings FFRS-Windows.utoc | grep -o 'DT_[A-Za-z0-9_]*' | sort -u > dt_list.txt
# 实测约 1620 张 DT_ 表
```

### 1.2 工具

| 工具 | 版本 | 用途 |
|---|---|---|
| [retoc](https://github.com/trumank/retoc) | v0.1.5 | Zen(IoStore) ⇄ Legacy(uasset/uexp) 双向转换 |
| [repak](https://github.com/trumank/repak) | v0.2.3 | 解传统 `.pak`（自带 Oodle 解码） |
| FModel / UAssetGUI | 5.6 | 图形化查看（可选） |
| Python 3 | — | 本文所有解析脚本 |

```bash
# Zen → Legacy（必须把 global.utoc/.ucas 一起带上，否则 to-legacy 无法解析属性名）
retoc.exe to-legacy --version UE5_6 --no-shaders \
    "<GAME>/FFRS/Content/Paks" legacy_out/

# Legacy → Zen（改完打包）
retoc.exe to-zen --version UE5_6 mod_stage/FFRS/Content/Datatable/Item out/t.utoc

# 传统 pak 里取全部 9 个 locres（代码即 Game.locmeta 声明的 culture）
for L in de en es fr is ja ko zh-Hans zh-Hant; do
  repak.exe get FFRS-Windows.pak \
    "FFRS/Content/Localization/Game/$L/Game.locres" "extracted/locres/$L.bin"
done
repak.exe get FFRS-Windows.pak "FFRS/Content/Localization/Game/Game.locmeta" extracted/config/Game.locmeta
```

> `repak.exe list FFRS-Windows.pak` 可直接列出全部 `FFRS/Content/Localization/Game/*/Game.locres`；`DefaultGame.ini` 里的 `+CulturesToStage=` 也逐条列出了同样 9 个代码。

> `to-legacy` 输出的 `.uasset` 是 retoc 重建的标准 UE 头（magic `0x9E2A83C1`），可直接按 UE 头解析 NameMap。

---

## 2. 已解包的数据表清单

`legacy_out/FFRS/Content/` 下 23 张表，覆盖本 wiki 的绝大部分内容：

```
Datatable/Item/DT_EquipmentItemData          ← 454 行，装备主表
Datatable/Item/DT_ConsumablesItemData        ← 消耗品
Datatable/Item/DT_ArtifactItemData           ← 武具（神器）
Datatable/Skill/DT_SkillData                 ← 技能
Datatable/Shop/DT_ShopEquipItemParameterList← 商店装备参数
Datatable/Event/Text/12ArmsEvent/DT_12Arms_WeaponEnhance ← 十二武具强化
Datatable/Strings/ST_Item/ST_{Equipment,Consumables,Artifact,Valuable,Vision}{Name,Description}
Datatable/UI/Item/DT_ItemIcon, DA_ItemIcon   ← 图标映射
Datatable/UI/Common/Item/DT_ItemCategoryIcon
UI/MainMenu/ActionEquip/Data/DA_ActionEquipment{,TypeProperty}
```

`DT_EquipmentItemData.uexp` = 144 208 字节，454 行；`.uasset` = 12 451 字节，NameMap 481 项。

---

## 3. 快速定位一行装备

uexp 每行都含一个 ASCII FString `EquipmentName{ID}\0`（ID 为 5 位数字），这是最稳定的行锚点：

```python
import re, struct
e = open('legacy_out/FFRS/Content/Datatable/Item/DT_EquipmentItemData.uexp','rb').read()
keys = [(m.start(), int(m.group(1)))
        for m in re.finditer(rb'EquipmentName(\d{5})\x00', e)]
# → 454 个，ID 范围 10100..12103
```

文件头 20 字节内 `int32@10 = 454`（行数）、`int32@14 = 67`，首行属性块从偏移 22 开始。

---

## 4. `DT_EquipmentItemData.uexp` 行结构（核心）

### 4.1 一行数据的物理布局

```
┌─ 属性块（在 rowid 之前） ─────────────────────────────────────┐
│ int32 MARK        0x00042380 (358 行) 或 0x06238000 (96 行)   │  ← 两种变体
│ int32 FLAGS       装备类别 / 是否有描述 等位域                  │
│ int32 0                                                        │
│ int32 COUNT       参数个数 0..4                                │
│ int32 CONST       18 / 19（普通行）、14 / 15 / 4（12000 系列）  │
│ int32 0           COUNT>0 时恒为 0                             │
│ ── COUNT 个参数，每个 = [int32 VALUE][int32 TAG]，               │
│    参数之间用 1 个 int32 0 分隔 ──                             │
│ int32[≤4] 效果槽  -1 = 空；否则为效果/被动技能 ID               │
│ 剩余槽位用 -1 (0xFF×4) 补齐，共 4 槽                          │
└──────────────────────────────────────────────────────────────┘
│ int32 ROWID       = 装备 ID                                    │
│ int32 PRICE       基础价格（如 101000）                        │
│ ... 键区：10~12 字节头 + 5 个 FString 本地化键                  │
│      EquipmentName{ID} / _sg / _pl / _det_sg / _det_pl          │
│ ...（若有描述）EquipmentDescription{ID} + 变体                  │
│ ... 尾部 int16 模板 (768, 200, 0, 50, 0, X, 0, 0, 0)           │
└─ 下一行 ─────────────────────────────────────────────────────┘
```

**实测样例**：`10100 深红烈焰`

```
80 23 04 00  01 00 01 01  00 00 00 00  02 00 00 00
12 00 00 00  00 00 00 00  15 00 00 00  0f 00 00 00
00 00 00 00  09 00 00 00  04 00 00 00  ff ff ff ff
ff ff ff ff  ff ff ff ff  ff ff ff ff  74 27 00 00
88 8a 01 00 ...
MARK=0x42380  FLAGS=0x01010001  COUNT=2  CONST=18
参数 = (21, tag15) (9, tag4)       ← 物攻 21 / 魔攻 9
效果槽 = [-1,-1,-1,-1]            ← 无特性
ROWID = 10100  PRICE = 101000
```

`10309 グレートボウ/强弓` 的效果槽为 `fe 04 00 00` = **1278**（破防伤害 +10%）。

### 4.2 完整解析器（454/454 行成功）

```python
import struct, re, json

MARKS = {0x42380, 0x6238000}

def parse_equipment(uexp_path):
    e = open(uexp_path, 'rb').read()
    keys = [(m.start(), int(m.group(1)))
            for m in re.finditer(rb'EquipmentName(\d{5})\x00', e)]
    out = {}
    for name_pos, rid in keys:
        # 1) rowid：名称键往前 45 字节内找 int32 == rid（行内非 4 字节对齐，必须按字节搜）
        pat = struct.pack('<i', rid)
        rp = next((o for o in range(name_pos-45, name_pos-4)
                   if e[o:o+4] == pat), None)
        if rp is None:
            continue
        # 2) 从 rowid 往前穷举属性块起点，用头部特征校验
        best = None
        for s in range(rp-220, rp-16):
            ints = [struct.unpack_from('<i', e, s+4*k)[0] for k in range(50)]
            if ints[0] not in MARKS or ints[2] != 0:            continue
            if not (0 <= ints[3] <= 6) or not (2 <= ints[4] <= 25): continue
            cnt, q, ent = ints[3], 6, []
            if cnt > 0:
                if ints[5] != 0: continue
                for k in range(cnt):
                    if k > 0:
                        if ints[q] != 0: break
                        q += 1
                    ent.append((ints[q], ints[q+1])); q += 2
                if len(ent) != cnt: continue
            else:
                q = 5                       # 12000 系列：无参数时头部只有 5 个 int32
            ri = ints.index(rid, q)
            slots = ints[q:ri]
            if not (0 < len(slots) <= 7) or not all(
                    v == -1 or 900 <= v <= 1700 for v in slots): continue
            cand = dict(id=rid, flags=ints[1], const=ints[4],
                        params=ent, effects=[v for v in slots if v != -1],
                        price=struct.unpack_from('<i', e, rp+4)[0])
            if best is None or s > best['_s']:
                cand['_s'] = s; best = cand
        if best: out[rid] = best
    return out
```

**为什么用穷举而不是固定偏移**：有描述文本的行键区变长，且 12000 系列（DLC / 特殊饰品）头部结构不同、字节对齐会漂移。穷举 + 结构自校验是最稳的做法。

实测结果：

| 指标 | 值 |
|---|---|
| 解析成功 | **454 / 454** 行 |
| MARK 0x42380 / 0x6238000 | 358 / 96 行 |
| 参数个数分布 | 0 个 81 行、1 个 14 行、2 个 223 行、3 个 103 行、4 个 33 行 |
| 有效果 ID 的装备 | 279 行（其中 34 行有 2 个效果） |
| CONST 分布 | 18(251) / 19(110) / 4(81) / 15(4) / 14(3) / 17(3) / 11(1) / 16(1) |

### 4.3 ⚠️ 最大的坑：TAG **不是**参数类型枚举

属性对写作 `(VALUE, TAG)`，很容易把 TAG 当成 `eUnitParameterType` 的枚举值——**这是错的**。

实测（用 454 件装备的带标签参数做交叉验证）：

```
tag 4  → MagicalAttack 196 次、Divine 61、Agility 59、MagicalDefense 32、
         HitPoint 8、MagicPoint 9、PhysicalDefense 6、PhysicalAttack 2 …
tag 15 → PhysicalAttack 249 次（很稳定）
tag 16 → PhysicalDefense 108 次（很稳定）
```

同一个 TAG 在不同装备上对应不同参数；即使加上「装备细分类型」仍有 18 组歧义。TAG 更像是**该行参数数组的槽位 / 掩码标记**，而非全局枚举。

**正确拿到参数名的方式**（按可靠性排序）：

1. **顺序 + 装备细分类别**：uasset NameMap 里内嵌了 9 个 `eUnitParameterType::*` 名称，参数在数组中的**位置**与类别共同决定含义。常见默认顺序：

   | 装备类别 | 槽 1 | 槽 2 | 槽 3 | 槽 4 |
   |---|---|---|---|---|
   | 武器（大剑/刀/锤/枪/鞭/书/棍） | 物理攻击 | 魔法攻击 | 追加（HP/MP/速度/神聖力） | 速度 / 神聖力 |
   | 弓 | 物理攻击 | 魔法攻击 | 神聖力 | — |
   | 轻装 / 重装 | 物理防御 | 魔法防御 | HP / MP | 速度 |
   | 法袍 | 物理防御 | 魔法防御 | 神聖力 | — |
   | 饰品 | 单一参数（多为魔法攻击或 HP） | — | — | — |

2. **用描述文本反查**：`HPが550上昇する` / `MPが20上昇する` 之类描述直接点名参数与数值，可与 VALUE 精确匹配。
3. **用独立全表导出交叉验证**（本 wiki 采用）：`ffrs.fireemblem.top` 公开的 `world-data.js` 含 454 件装备的 `params:[{key,value}]`。本项目解析出的 **454 行 × 全部参数值与 key 顺序 100% 一致（0 处不符）**，因此直接采用其 `key` 作为参数标签，同时保留自己的二进制数值作为独立校验。

> 结论：**数值来自游戏二进制，参数名来自「NameMap 枚举 + 槽位规则」，并用第三方全表导出做交叉验证**。任何只靠 TAG 查表的做法都会出错。

### 4.4 效果槽 = 特性 ID

- `-1`（`ff ff ff ff`）= 无特性。本作 8 个类型的初始武器**全部无特性**，且共用尾部模板 `(768, 200, 0, 50, 0, X, 0, 0, 0)`。
- 非 -1 的值指向被动技能表 `ST_PassiveSkillName / Description`。已验证的常用 ID：

| ID | 效果（日文原文） | 中文 |
|---|---|---|
| 1521 | 火属性のダメージが20％上昇する | 火属性伤害提升 20% |
| 1522 | 無属性の物理攻撃に氷属性を付与する | 无属性物理攻击赋予冰属性 |
| 1523 | 無属性の物理攻撃に雷属性を付与する | 无属性物理攻击赋予雷属性 |
| 1527 | 光属性付与 | 光属性 |
| 1278 | ブレイクダメージが10％上昇する | 破防伤害提升 10% |
| 1310 / 1308 / 1282 / 1099 | 其他常见被动 | 见 wiki 效果索引页 |

反查方法：**按效果 ID 聚合所有引用它的装备描述**，出现次数最多的描述即为该 ID 的官方文案；也可直接查 `ST_PassiveSkillDescription`。

### 4.5 装备分类

- `FLAGS` 的高字节（bits 24-31）取 1..11，对应 11 个装备细分类别；次字节区分武器 / 防具；低位标记该行是否带描述键（`0b 01` 标记在键区表现为 `0b 01 00 00` 而非 `0b 03 00 00`）。
- 更可靠：从 `DT_EquipmentItemData.uasset` 的 NameMap 读 `Equipment_Weapon_{Brade|Katana|Bow|Gun|Hummer|Rod|Whip|Book}` 等字符串，或用独立全表导出的 `fields.装备细分`（`GreatSowrd` / `Katana` / `Bow` / `Gun` / `Hammer` / `Rod` / `Whip` / `Grimoire` / `LightArmor` / `HeavyArmor` / `Robe` / `Accessory`）。

---

## 5. 日文名称 / 描述：`ST_*` 字符串表

`legacy_out/.../Strings/ST_Item/ST_EquipmentName.uexp`（456 条）与 `ST_EquipmentDescription.uexp`。

FString 编码（UE 标准）：

```python
def fstr(d, o):
    n = struct.unpack_from('<i', d, o)[0]
    if n < 0:      # UTF-16LE
        c = -n
        return d[o+4:o+4+c*2].decode('utf-16-le').rstrip('\x00'), o+4+c*2
    if 0 < n < 10000:
        return d[o+4:o+4+n].split(b'\x00')[0].decode('utf-8','replace'), o+4+n
    return None, None
```

- 行键格式固定：`EquipmentName{ID}` + 4 个语法变体 `_sg` / `_pl` / `_det_sg` / `_det_pl`。
- 描述键：`EquipmentDescription{ID}`；**值为 `None` 表示无特性**（456 件中 310 件有描述）。
- 描述文本是判定特性的最可靠来源，例：`10105 = 火属性のダメージが20％上昇する`。

---

## 6. 官方全语言文本：`Game.locres`

### 6.1 位置与提取

官方本地化**不在 IoStore 里**，而在传统 pak。**游戏共打包 9 个语言目录**（不是 3 个）：

```
FFRS/Content/Localization/Game/
├── Game.locmeta
├── de/Game.locres       en/Game.locres      es/Game.locres
├── fr/Game.locres       is/Game.locres      ja/Game.locres
├── ko/Game.locres       zh-Hans/Game.locres zh-Hant/Game.locres
└── DebugMenu/{de,en,es,fr,ja,ko,zh}/DebugMenu.locres   （调试菜单，另一套）
```

```bash
for L in de en es fr is ja ko zh-Hans zh-Hant; do
  repak.exe get FFRS-Windows.pak \
    "FFRS/Content/Localization/Game/$L/Game.locres" "extracted/locres/$L.bin"
done
```

> 注意 `zh` 不是目录名：正式代码是 `zh-Hans`（简中）与 `zh-Hant`（繁中），二者是**不同文本**，不能互相代替。早期只取 `zh-Hans` 会漏掉繁中。

### 6.2 二进制格式（SQEX / UE5.6 变体）

```
偏移 0  ..19   20 字节哈希
偏移 20 ..36   版本 / 保留（共 37 字节头）
偏移 29        int32 = 命名空间数量（实测 955）
偏移 37        键区起点
  重复 955 次：
    [FString 命名空间]
    [int32 该命名空间键数][uint32 hash]
    重复 N 次：
      [FString 键][int32 B][int32 C = 文本索引][int32 D]
偏移 947159    文本区起点（ja 实测；各语言因文本长度不同，起点不同，须动态求）
  重复到文件尾：
    [FString 文本][uint32 固定 4 字节跳过]
```

> **关键坑 1**：`C`（文本索引）**各语言独立**。同一个 `EquipmentName10100`：ja=10273、zh-Hans=10368、en=10861。必须**逐语言各解析一份键区**，不能复用索引表。
> **关键坑 2**：文本区起点不是全语言固定的。键区长度随语言变化（英文键区更长），正确做法是**从键区解析结束位置 `o` 直接开始读文本**，不要硬编码 `947159`。
> **关键坑 3**：英文 locres 中少数键被重映射到别的文本（例如 `EquipmentDescription10100`：ja / zh-Hans 均为 `None`，en 却是 `Increases attack by 5.`）。生成数据集时以**原生语言 ja + zh-Hans 的一致性**做收敛：两者皆空则全语言置空。

### 6.3 完整解析器

```python
def parse_locres(path):
    d = open(path, 'rb').read()
    ns_count = struct.unpack_from('<i', d, 29)[0]
    o, mp = 37, {}
    for _ in range(ns_count):
        ns, o = fstr(d, o)
        cnt = struct.unpack_from('<i', d, o)[0]; o += 8     # count + hash
        for _ in range(cnt):
            key, o = fstr(d, o)
            B, C, D = struct.unpack_from('<3i', d, o); o += 12
            mp.setdefault(ns, {})[key] = C
    texts, p = [], o
    while p + 8 <= len(d):
        s, p2 = fstr(d, p)
        if s is None: break
        texts.append(s); p = p2 + 4
    return mp, texts

LANGS = ('ja','en','zh-Hans','zh-Hant','ko','de','fr','es','is')
L = {lg: parse_locres(f'extracted/locres/{lg}.bin') for lg in LANGS}
def T(lang, ns, key):
    mp, texts = L[lang]
    i = mp.get(ns, {}).get(key)
    if i is None or i >= len(texts): return ''
    return '' if texts[i] in ('None','none') else texts[i]
```

`Game.locmeta` 的解析（120 字节，格式固定）：

```
[16-byte GUID][1 byte version]
[FString NativeCulture] → "ja"
[FString native locres path] → "ja/Game.locres"
[int32 count=9][9 个 FString] → de en es fr is ja ko zh-Hans zh-Hant
```

### 6.4 实测规模（9 语言全量）

| 语言代码 | 文件字节 | 命名空间 | 文本条数 | `ST_UI_Language` 中的自称 |
|---|---:|---:|---:|---|
| `ja`（原生） | 2 281 952 | 955 | 19 983 | 日本語 |
| `en` | 3 112 749 | 955 | 21 790 | English |
| `zh-Hans` | 2 090 258 | 955 | 20 271 | 简体中文 |
| `zh-Hant` | 2 079 973 | 955 | 20 261 | 繁體中文 |
| `ko` | 2 370 795 | 955 | 20 394 | 한국어 |
| `de` | 3 519 979 | 955 | 21 199 | Deutsch |
| `fr` | 3 661 655 | 955 | 21 906 | Français |
| `es` | 3 411 798 | 955 | 21 410 | Español (España) |
| `is` | 2 156 996 | 955 | 26 698 | *（占位文本，见 6.5）* |

955 个命名空间中，本 wiki 用到的主要有：

| 命名空间 | 键数 | 内容 |
|---|---:|---|
| `ST_EquipmentName` | 2 280 | 456 件装备 × 5 语法变体 |
| `ST_SkillName` / `ST_SkillDescription` | 1 161 / 1 161 | 技能 |
| `ST_UnitDescription` / `ST_UnitName` | 772 / 566 | 单位（角色 + 敌人） |
| `ST_EquipmentDescription` | 456 | 装备特性 |
| `ST_PassiveSkillName` / `Description` | 378 / 378 | 被动技能 |
| `ST_ConsumablesName` | 255 | 消耗品 |
| `ST_MainProgress` / `ST_MainSummary` | 304 / 231 | 主线 |
| `ST_CharaName` | 198 | 角色名 |
| `ST_SubProgress` / `ST_SubSummary` | 139 / 141 | 支线 |
| `ST_MAP_NAME` | 128 | 地图名 |
| `ST_12Arms_WeaponEnhance` | 99 | 十二武具强化 |
| `ST_UI_*`（25 个命名空间合计 706 键） | 706 | UI 术语 / 参数名 / 选项 / 语言名 |

参数名的官方文本来自 `ST_UI_Unit` 的 `UI_UnitParameter{Key}`（44 键），9 语言齐备，例如 `UI_UnitParameterPhysicalAttack`：

| ja | en | zh-Hans | zh-Hant | ko | de | fr | es |
|---|---|---|---|---|---|---|---|
| 物理攻撃力 | Phys. Atk. | 物理攻击力 | 物理攻擊力 | 물리 공격력 | Phys. Angr. | Att. phys. | Ataque físico |

**9 语言样例**（`EquipmentName10300` 轮回之翼）：

| 代码 | 文本 | 代码 | 文本 |
|---|---|---|---|
| ja | リンカネーション | ko | 리인카네이션 |
| en | Reincarnation | de | Reinkarnation |
| zh-Hans | 轮回之翼 | fr | Réincarnation |
| zh-Hant | 輪迴之翼 | es | Rencarnación |
| is | `EquipmentName10300, ST_EquipmentName` | | *（伪本地化占位）* |

描述样例（`EquipmentDescription10105`）9 语言均可查到，例如 fr = `Augmente les dégâts infligés par les attaques de feu de 20 %.`

### 6.5 游戏到底支持哪些语言：证据链

「支持语言」不能凭目录猜，本 wiki 用三份**游戏内证据**交叉确认：

| 证据 | 位置 | 结论 |
|---|---|---|
| `Game.locmeta` | pak 内 `FFRS/Content/Localization/Game/` | `NativeCulture=ja`；`SupportedCultures` = `de en es fr is ja ko zh-Hans zh-Hant`（9 个） |
| `DefaultGame.ini` | pak 内 `FFRS/Config/` | `+CulturesToStage=` 逐条列出同样 9 个代码 |
| `ST_UI_Language` | 每个 `Game.locres` 内，8 个键 | 游戏「テキスト言語」菜单实际提供的 8 项：`Japanese / English / SimplifiedChinese / TraditionalChinese / Korean / German / French / Spanish` |

**结论**：

- **玩家可选的 8 种文本语言**：`ja`、`en`、`zh-Hans`、`zh-Hant`、`ko`、`de`、`fr`、`es`。wiki 全部支持，`?lang=<code>` 可深链（兼容旧别名 `?lang=zh` → `zh-Hans`）。
- **`is` 不是面向玩家的语言**，而是 UE 的伪本地化（pseudo-localization）调试文化：实测 26 698 条文本中 **98.5%** 为 `Key, Namespace` 形式的占位串（如 `EquipmentName10300, ST_EquipmentName`），游戏菜单里也没有它。wiki 仍将其作为第 9 项保留，默认折叠在 **DEV** 开关后，专供检查「哪些字符串没走本地化」。
- **语音语言**只有 2 种：`ST_UI_Option_Common` 的 `RadioButton_Voice_JP / EN`（日 / 英），与文本语言独立。
- 语言切换器里的**自称**（日本語 / 简体中文 / 한국어 / Deutsch…）不是手写的，而是从各语言自己的 `ST_UI_Language` 命名空间读出的官方译名。

---

## 7. 特性（效果）的完整获取链路

```
DT_EquipmentItemData.uexp
   └─ 效果槽 ID ──────┬─→ ST_PassiveSkillName / Description（技能表）
                      └─→ 按 ID 聚合 EquipmentDescription{ID}（装备自带文案）
                            ↓
                     Game.locres 全 9 语言（8 正式 + is 伪本地化）
                            ↓
                     wiki「效果索引」页：ID → 全语言文案 → 关联装备列表
```

判定一件装备「有没有特性」的三重校验（避免误判）：
1. 效果槽是否为 `-1`；
2. `ST_EquipmentDescription{ID}` 是否为 `None`；
3. 键区是否有描述键（`0b 01` 标记）。

---

## 8. 其他数据表

同名套路即可解析：

| 表 | 行数（全表导出统计） | 备注 |
|---|---:|---|
| `DT_ConsumablesItemData` | 51 | 消耗品 |
| `DT_ValuableItemData` | 37 | 贵重品 |
| `DT_ArtifactItemData` | 19 | 武具 / 神器 |
| `DT_VisionItemData` | 26 | 幻兵 |
| `DT_SkillData` | 773 技能 + 206 被动 | 配 `ST_SkillName/Description` |
| `DT_EnemyData` | 596 敌人 | 弱点、掉落、偷窃、出现地点 |
| `DT_QuestData` | 230 任务 | 主线 8 章 + 委托 |
| `DT_Shop*` | 105 商店 | 库存与开放条件 |
| `DT_MapData` | 186 地图 | 地点 / 宝箱 / 采集点 |
| `DT_12Arms_WeaponEnhance` | 99 文本 | 十二武具觉醒 |

名称统一走 `ST_*Name` + `Game.locres` 全 9 语言，数值走对应 uexp。
任务 / 商店 / 地图的名称**只有中文公开导出**，没有官方多语言文本；数据集里仅 `zh-Hans` 有值，其余语言留空，由前端 `nm()` 做缺译回退并显示 ↺ 标记，绝不假装已翻译。

---

## 9. 校验方法（务必做）

1. **往返转换**：改完 `uexp` → `to-zen` → `to-legacy`，逐字节比对，确认插入/修改的字节完整保留。
   - `to-legacy` 需要把 `global.utoc/.ucas` 复制到测试容器目录，否则属性名解析失败。
2. **改长度要同步**：插入参数会让 uexp 变长（每加一组参数 +12 字节），`COUNT` 字段要 +1；retoc 重建的 legacy 头会自行计算长度，无需手改 uasset。
3. **官方数值交叉验证**：用攻略站公开的装备数值抽样比对（本项目 40+ 件武器全部对上）。
4. **全表导出交叉验证**：454 行参数值与独立导出的 `params` 逐项比对（0 处不符，见 4.3）。
5. **描述一致性**：有效果 ID 的装备，`ST_EquipmentDescription` 不应为 `None`。

---

## 10. 已知限制与坑

- **UE4SS 不可用**：Denuvo Shipping 版（495 MB 的 `FFRS-Win64-Shipping.exe`），AOB 扫描 61 次全失败；`steamclient64.dll` 报错来自破解链而非 UE4SS。
- **TAG 不是枚举**（见 4.3），这是最容易出错的地方。
- **有描述的行结构会变**：键区变长，固定偏移解析必然错位。
- **locres 文本索引各语言独立**，不能复用。
- **12000 系列**（DLC / 特殊饰品）属性块 MARK 与 CONST 都不同，且常为 0 个参数 + 1~2 个效果 ID。
- 名称语法变体 `_sg/_pl/_det_sg/_det_pl` 在 locres 里多共享同一文本索引，取 `base` 键即可。
- Mod 打包文件名必须以 `_P` 结尾；回退只需删除 `~mods/FFRS_Mod_P.*`。

---

## 11. 一键复现

```bash
WIKI="H:/game/FINAL FANTASY RESONANCE/FFRS_Wiki"
WORK="./work"          # 解包产物 / 中间 JSON / 参考站缓存
PAKS="H:/game/FINAL FANTASY RESONANCE/FFRS/Content/Paks"

# 1) 解包（必须把 global.utoc/.ucas 一起带上，否则 to-legacy 无法解析属性名）
"$WORK/retoc/retoc.exe" to-legacy --version UE5_6 --no-shaders "$PAKS" "$WORK/legacy_out/"

# 2) 从传统 pak 取全部 9 个语言 + locmeta
mkdir -p "$WORK/extracted/locres" "$WORK/extracted/config"
for L in de en es fr is ja ko zh-Hans zh-Hant; do
  "$WORK/repak_tool/repak.exe" get "$PAKS/FFRS-Windows.pak" \
    "FFRS/Content/Localization/Game/$L/Game.locres" > "$WORK/extracted/locres/$L.bin"
done
"$WORK/repak_tool/repak.exe" get "$PAKS/FFRS-Windows.pak" \
  "FFRS/Content/Localization/Game/Game.locmeta" > "$WORK/extracted/config/Game.locmeta"

# 3) 参考站公开数据（可选但强烈建议：补获取来源 / 敌人 / 任务 / 地图 / 职能）
curl -o "$WORK/refs/world-data.js"          https://ffrs.fireemblem.top/图鉴资源/data/world-data.js
curl -o "$WORK/refs/character-relations.js" https://ffrs.fireemblem.top/图鉴资源/data/character-relations.js
curl -o "$WORK/refs/map-relations.js"       https://ffrs.fireemblem.top/图鉴资源/data/map-relations.js
curl -o "$WORK/refs/trophy-relations.js"    https://ffrs.fireemblem.top/图鉴资源/data/trophy-relations.js
curl -o "$WORK/refs/ja_visions.html"        https://ffresonance.wiki/ja/database/visions/
curl -o "$WORK/refs/ja_chars.html"          https://ffresonance.wiki/ja/characters/

# 4) 游戏侧解析 → 中间 JSON（454 行 / 481 NameMap / 9 语言文本 / 语言元数据）
python "$WIKI/tools/extract_ffrs.py" --work "$WORK" --out "$WORK/extracted/"

# 5) 生成 wiki 数据集（全语言合并 + 关系数据 + 图标本地路径）
python "$WIKI/tools/build_wiki_data.py" \
    --work "$WORK" --refs "$WORK/refs" --out "$WIKI/data/wiki-data.js"

# 6) 图标资源随站点部署（458 张，约 5.3 MB，已存在则跳过，可反复运行）
python "$WIKI/tools/fetch_media.py" \
    --refs "$WORK/refs" --out "$WIKI" --base https://ffrs.fireemblem.top --jobs 16

# 7) 逐语言无头浏览器验证
python "$WIKI/tools/verify_wiki.py" --wiki "$WIKI"
```

`extract_ffrs.py` 也兼容旧的三文件命名（`locres_ja.bin` / `locres_zh.bin` / `locres_en.bin`），但只有 9 个 `<code>.bin` 齐备时才会生成完整的 `langmeta.json`。

---

## 12. wiki 构建、资源自托管与验证

### 12.1 数据管线

```
游戏本体  ── extract_ffrs.py ──→ equipment_raw.json / locres.json / langmeta.json / namemap.json
参考站    ── world-data.js + *-relations.js + ja_*.html ──┐
                                                          ├─ build_wiki_data.py ─→ data/wiki-data.js
                                                          │                       (window.FFRS_DATA)
媒体图标  ── fetch_media.py ───────────────────────────────→ media/Content/... (458 PNG)
```

`wiki-data.js` 约 **5.90 MB**（含官方剧情梗概的 9 语言全文），含：

| 集合 | 条数 | 集合 | 条数 |
|---|---:|---|---:|
| 装备 | 454 | 技能 / 被动 | 979 |
| 道具 / 幻兵 | 135 | 效果索引 | 174 |
| 角色 | 11 | 幻兵卡 | 26 |
| 敌人 | 596 | 任务 | **214** |
| 商店 | 105 | 地图 | 186 |
| 成就 / 奖杯 | **40** | UI 术语（25 个 `ST_UI_*`） | 706 |
| 成就条件配置 | **16** | 数据值 / 字段名标签 `labels` | **5 373** |

每个实体的 `name` / `desc` 都是 **9 语言对象**；`params`、`roles`、`effects`、`glossary` 同理。
另外补入了三类此前未使用的关联数据（key 均为 `world-data.js` 的 `rid`）：

- `character-relations.js`（515 键）→ 装备 / 道具 / 幻兵的 `usableBy`（可装备角色）
- `map-relations.js`（3 570 键）→ `mapPoints` / `points`（关联地图与事件触发点）
- `trophy-relations.js`（154 键）→ 19 个奖杯的关联实体；**奖杯主表改用官方 `ST_TrophyName` / `ST_TrophySummary` 的全部 40 条**

### 12.2 前端

- `index.html` → `data/wiki-data.js` + `assets/i18n.js` + `assets/app.js` + `assets/style.css`，**纯静态、无构建、无网络依赖**。
- `i18n.js` 用紧凑行式字典：每个 key 一行放 9 个语言值，运行时展开；**200 个 UI key × 9 语言全覆盖**，`is` 留空自动回退英文。另导出 `_tokens`（25 条组合词表）供 `L()` 兜底。空值按「当前语言 → en → zh-Hans」兜底。
- `nm()` 对数据文本按「当前语言 → ja → en → zh-Hans → 任意非空」回退；只有中文来源的字段会显示 ↺ 标记，避免误导。
- `L(v)` 查数据层的 `labels` 表（**5 373 条 × 9 语言**），把装备类型 `Weapon → Waffe`、地区 `Grandshelt → グランシェルト`、任务分类、敌人种族、来源规则、原始字段键等统一本地化；命中优先级为「官方整串 → 条件表达式 → 分隔符分段 → 名称+数字 → 组合词表」。`fieldLabel(k)` 让详情抽屉的字段名也随语言切换。敌人 `stats` 的 `MaxHitPoint / Attack / Defence / Intelligence / Mind / Divine / Agility` 等键直接映射到官方 `ST_UI_Unit` 的 `UI_UnitParameter*` 文本。
- 语言切换器为下拉（8 项）+ **DEV** 开关（`is`）；`?lang=<code>` 深链会同步写入 URL，`localStorage` 记忆；`#/equipment/10300` 详情深链在所有语言下可用（如 `?lang=de#/equipment/10300` 显示 `Reinkarnation`）。
- 详情抽屉给出**9 语言对照表**（当前语言高亮）、原始二进制字段、可装备角色、地图点位、关联成就、**8 属性相性图标网格** / 掉落 / 库存等结构化字段。

### 12.3 验证结果

`verify_wiki.py` 用无头 Edge 逐个打开 9 个语言 + 15 个栏目页 + 深链，并额外检查本轮三处修复：

| 检查 | 结果 |
|---|---|
| 9 语言 `?lang=` 深链 → 选择器选中值一致 | 9/9 PASS |
| 详情抽屉装备名逐语言正确 | リンカネーション / Reincarnation / 轮回之翼 / 輪迴之翼 / 리인카네이션 / Reinkarnation / Réincarnation / Rencarnación / 占位串 |
| 首页 `hero` 多语言 | zh-Hans / ja / de / ko 均正确 |
| 15 个栏目页渲染（含成就页、语言页） | 全部 PASS，`undefined` / `NaN` / `[object Object]` = 0 |
| 详情深链 | `#/equipment/10300` → 轮回之翼；`#/quests/49498` → 全新的花朵；`#/glossary/ST_UI_Unit::UI_UnitParameterDivine` → 神圣力；`#/trophies/10` → 反抗命运的人们 |
| 敌人耐性图标 | 列表出现属性徽记；敌人详情固定渲染 8 个 `.aff-cell` |
| 装备类型下拉 | `de → Waffe`、`fr → Arme`、`ja/zh → 武器`、`Bow → Bogen` 全部命中 |
| 商店 / 地图 / 奖杯详情字段 | 不再出现 `<dt>region</dt>` / `<dt>type</dt>` / `<dt>count</dt>` 等原始 key |
| 跨语言数据值 | `de#/maps/10001 → Verlorene Höhle`、`fr#/trophies/10 → Défier le destin`、`en#/shops/0 → Item Shop` |
| 明暗主题 | `?theme=dark` 与按钮切换均正常，法文深色装备页实测对比度合格 |
| 站内图标 | 458/458 下载成功，0 失败，5.30 MB；随页面一同部署，浏览器直接请求站点路径 |

`node --check` 三个 JS 文件、`python -m py_compile` 五个 py 脚本均通过。

> **唯一键修正**：公开导出中 230 条任务有 187 条 `id=null`（现拆出 16 条成就条件后剩 214 条），706 条术语里有 25 组跨命名空间同名 key。若直接用 `id`/`key` 做详情深链会打开错条目。数据层为这两类记录生成 `uid`（任务用 `rid`、术语用 `ns::key`），前端统一用 `eid()` 取键；任务表另加 `table`（来源数据表）与 `placeholder`（2 条 `test` 空行）标记。

### 12.4 数据值国际化、`Achievement####` 与属性相性图标

**① `Achievement0009` 是什么。** 公开导出把 `DT_AchievementCondition` 的 16 行混进了 `kind=quest`，因此任务图鉴出现看不懂的 `Achievement0009`。它们**不是任务**，而是游戏内「实绩 / 奖杯」达成条件的内部配置行：`EAchievementId::AchievementNNNN` 是内部 ID，附带参数原值是阈值或对象 ID 列表。

处理：数据层把这 16 行拆成独立的 `achievementConds`，任务表缩到 214 条；成就页底部新增「成就条件配置」表，逐行给出内部 ID、参数原值、来源表，并对照**官方 9 语言奖杯文本**。奖杯主表不再用资料站反向聚合的 19 条，而是直接读官方 `ST_TrophyName` / `ST_TrophySummary` 的 **40 条**（`TrophyName_001..040`），`is` 为占位串自动回退。

对应关系依据官方达成条件文本推断，并在页面标注「推断」：`AchievementNNNN → Trophy_(NNNN+1)`。实测吻合：

| 内部 ID | 参数原值 | 对应官方奖杯 |
|---|---|---|
| `Achievement0009` | `1001…1008`（8 名同伴 ID） | 010「让所有同伴加入」 |
| `Achievement0018` / `0019` | `100000` / `1000000` | 019「累计使用 10 万金币」/ 020「累计获得 100 万金币」 |
| `Achievement0031` / `0032` | `10` / `26` | 032「获得 10 具幻兵」/ 033「获得所有幻兵」 |
| `Achievement0036` / `0037` | `1000` / `400` | 037「破防 1000 次」/ 038「释放 400 次共鸣」 |

**② 属性相性图标。** `world-data.js` 的敌人 `resists` 原为 `[{key,value}…]`，旧前端用通用 `objTable()` 渲染，结果是 `0 → {"key":"Fire","value":"None"}` 这类 JSON 文本。现在数据层规范成 `{元素: 状态}`（`Fire / Ice / Thunder / Water / Wind / Earth / Light / Dark` × `None / Weakness / Halved / Invalid / Absorb`），前端用**内联 SVG 元素徽记**（无外部图片、明暗主题自适应）：图标表元素，角标 `! / ½ / ∅ / +` 表相性，列表只显示非普通相性，详情固定 8 格网格并附本地化图例；另加「仅看有弱点」筛选。tooltip 与图例全部 9 语言。

**③ 补齐页面国际化。** 三个来源：
- **官方文本精确匹配**：`build_name_index()` 从 `ST_MAP_NAME` / `ST_QuestBoardTitle` / `ST_EventQuestTitle` / `ST_TrophyName` / `ST_UnitName` / `ST_UI_QuestMenu` 以简体中文文本建索引（685 条），把公开导出的中文名升级为官方 9 语言。实测地图 **163/186**、任务 **40/214** 命中；未命中的保留中文并显示 ↺ 回退标记，**不伪造翻译**。
- **枚举 / 字段标签表**：`LABEL_ROWS` 手工给出装备类型（含游戏内拼写 `GreatSowrd`）、道具 / 任务 / 地图 / 敌人分类、地区、种族、来源规则、关联规则、商店名（7 种日文原值）、61 个顶层字段名与 34 个原始数据字段键的 9 语言标签，合并官方人名地名索引与 `ST_UI_Unit` 参数名后共 **897 条**，写入 `labels`。
- **UI 字典**：`i18n.js` 新增成就条件说明、耐性图例、筛选按钮与各页面修订说明，共 181 个 key × 9 语言。

前端 `L(v)` / `fieldLabel(k)` 统一查表，因此装备类型下拉、列表 badge、详情抽屉字段名与值、商店店名、地图地区、奖杯名称与达成条件在 9 种语言下都同步切换。

### 12.5 章节名、来源灰字、掉落 / 偷窃、角色字段与站外链接

**① 章节名。** `DT_ActivityData` 的 9 行原本把日文行名 `プロローグ / 1章…8章` 当成中文名塞进任务表，切换语言不变。官方 `Game.locres` 的 `ST_ChapterTitle` 恰有 10 条 9 语言文本：

| 行名 | 官方键 | ja | en | zh-Hans |
|---|---|---|---|---|
| `プロローグ` | `ChapterTitle_010` | プロローグ | Prologue | 序章 |
| `1章` | `ChapterTitle_001` | 探求のはじまり | Tailing the Truth | 探究的开端 |
| … | … | … | … | … |
| `8章` | `ChapterTitle_008` | 生命の夢の終わりに | Beyond Hope | 生命之梦的终结 |

数据层 `chapter_name()` 按官方键精确升级，并拼上本地化的「第 N 章 / Chapter N / Kapitel N」序号；`row` 保留原行名以便回溯。任务板里另有的日文原名（如 `後方支援でヨーソロー！`）通过新增的**按日文原文反查索引**（`build_official_index(..., 'ja')`，覆盖 17 个官方命名空间）升级为 9 语言。

**①b 任务描述其实有官方译文。** 之前把公开导出里的中文剧情梗概当成「资料站自撰、只有中文」，因此英文 / 德文任务页的 Description 列一直显示中文。实测 `Game.locres` 的 `ST_MainSummary`（231 条）/ `ST_SubSummary`（141 条）/ `ST_EventDialog` / `ST_TrophySummary` 等命名空间里**就有这些梗概的官方 9 语言原文**，只是资料站把 CRLF 段落压成了空格与换行。用「去空白全文精确匹配」建 `summary_idx`（481 条），`summarized()` 命中即升级为官方译文：

| 检查 | 结果 |
|---|---|
| 98 条唯一梗概中直接命中官方 | 85（86.7%） |
| 9 条「包含 N 组活动开始 / 完成检查点」 | `chapter_desc()` 模板生成 9 语言 |
| 7 条日文内部名 / 2 条 `test` 占位 | 按官方 `ST_UnitName` 索引 / 本地化占位词处理 |
| 最终任务描述有英文译文 | **213 / 214**（唯一未命中是空描述） |

例如 `雷因主动和雷根搭话……` → `Rain attempts to speak with Raegen, but the conversation awkwardly ends…` / `レインはレーゲンに声をかけるも…` / `Rain versucht, eine Unterhaltung mit Raegen zu führen…`。**这是官方文本，不是机器翻译或臆造。**

**② 来源全量与国际化的灰字。** 公开导出单件装备最多 3 条 `sources` + 11 条 `shops` + 2 条 `loot`，旧管线 `brief_list(..., 4)` 会静默截断，表格列又只取 `s.place` 再 `cut(34)`，且没有走本地化。现在：

- `brief_list()` 默认保留全量；`sourceList()` 去掉 `slice(0,12)`；表格「获取来源（全部）」列与详情**同源同序**，把 `sources / shops / loot / recipes` 全部渲染为 `类型 · 地点` chips（`hover` 另给完整文本）。
- `labels` 从 897 扩到 **5 373 条**：除 `LABEL_ROWS` 外，把 17 个官方名称命名空间按**简体中文与日文双键**登记，因此 `格兰谢尔特城 → Grandshelt Castle / Schloss Grandshelt`、`帝皇之剑 → Kaiser Sword / Kaiserschwert`、`雷因 → Rain` 等专名都随语言切换。
- 官方没有整串译文的组合值（`宝箱 · 帝皇之剑`、`综合商店 10001`、`NPC · 9`、`甲板 · 艾琳 · …`）由 `L()` 按 ` · ` / ` / ` / `,` 分段递归 + 「名称 + 数字」拆解 + `_tokens` 25 条中文短语表整词替换处理。
- 条件表达式 `{progress:31}>=60` / `{item:29001}>=1|117` 用 `conditionText()` 转成 9 语言可读文本（如德语 `Fortschritt 31 >= 60`），**详情里的灰字不再是固定中文**。

**③ 掉落与偷窃分开。** 公开导出 `drops` 已带 `field`（`DropNormalItem / DropRareItem / StealNormalItem / StealRareItem`）与 `rate`，旧前端只把整条对象塞进通用列表。现在 `norm_drops()` 规范成 `kind: drop|steal` × `rarity: normal|rare` 并保留概率，列表用双色小徽记显示两类数量，详情拆成「掉落」「偷窃」两张表，列 `普通 / 稀有 · 物品 · 概率`，四种类型各有 9 语言标签。

**④ 角色页与站外链接。** 参考站给的 `title / weapon / lb / attack / armor` 是日文原文，详情抽屉则把它们当普通字符串直接输出。现在：

- `weapon` → 映射装备 subtype 的官方标签（`大剣 → Greatsword / Großschwert`）；
- `lb` → 拆出每个技能名，按 `ST_SkillName` 官方 9 语言文本回查（`インフィニット・ドライブ → Infinity Drive / Unendlichkeitstrieb`），保留 `(Lv N)`；
- `title / attack / armor` → 7 + 7 + 3 条等义译法，其中 `attack` 由「物理 / 魔法」与官方元素名模板拼出；
- 上述字段改为 9 语言对象，`showEntity()` 对语言对象取 `nm()`，字段名走 `fieldLabel()`；游戏本体 `UnitDescriptionNNNN` 的「説明文」开发占位描述直接置空。
- **详情内不放站外跳转**：`characters[].url`、`visions[].url`、奖杯关联的 `url` 全部从数据层删除；`refLinks` 保留 9 语言来源署名，并在「参考与致谢」页附上两个原站网址（上线前新增），详情抽屉仍无任何外链。`index.html` 里残留的静态中文（`title` / `meta description` / 占位符 / `aria-label` / 主题按钮 tooltip）也换成中性英文或由 `renderTop()` 本地化。

**⑤ 本轮验证。** `verify_wiki.py` 增加 5 组专项：3 语言章节名与 Prologue、装备来源 chips / 11004 全量 18 条 / 10128 条件灰字、敌人 10 的掉落与偷窃双表 + 列表徽记、德文角色列表与详情字段 + 无 `href="http"`、链接页零外链。详见 12.3 验证表。

### 12.6 `Vision_Face` 幻兵 / 召唤兽头像接入 UI

**问题。** `media/Content/UI/Textures/Icon/Vision_Face/` 下有 34 张官方头像，但幻兵图鉴显示的却是同一张 `ICON_Item_Crystal.png`。根因不在前端，而在数据归属：

- 公开导出 `world-data.js` 里 `kind=vision` 记录的 `image` 只是**通用水晶图标**，26 条全部相同；
- 真正的头像挂在 `kind=unit` 记录上，路径形如 `/media/Content/UI/Textures/Icon/Vision_Face/ICON_UnitFace_summon<ID>.png`，文件名里的数字就是单位 ID；
- 共 36 条记录引用该目录、34 个唯一 PNG。其中 25 个 ID 对应 26 个幻兵，另外 9 个 ID（10451–10459，`subtype=Beast`）是**召唤兽**：セイレーン / イフリート / シヴァ / ラムウ / カーバンクル / オーディン / タイタン / バハムート / リヴァイアサン；
- 两条记录复用他人头像：1016「シャルロット（ゲスト）」→ 13060，10460「バハムート（700年前）」→ 10458。后者按记录 ID 补入映射得到 35 个 ID→路径键，但图鉴不重复收录；
- `13113 セフィロス` 是唯一没有头像的幻兵：打包资源、公开导出与参考站均无 `summon13113.png`（线上直接请求返回 404），不能用别的图冒充。

**数据层。** `build_wiki_data.py` 新增 `vision_face_map(recs)`：正则取文件名中的 `summon(\d+)` 建映射，再用 `kind=unit` 记录的 `id` 补入复用关系（`10000 < id < 20000`，避免把客串角色 1016 混进幻兵）。结果写进每条幻兵的 `face` 字段，`icon` 保留为回退图；`noPortrait` 标记缺失者。另生成 `summons` 9 条，名称用 `fillnames(T, 'ST_UnitName', 'UnitName', id, w)` 取**官方 9 语言**文本（例：10451 = セイレーン / Siren / 塞壬 / Sirene / Sirène / Sirena）。`meta.visionFaceIndex = 35`、`meta.visionFaceWithPortrait = 25`。

**前端。** `thumb()` 优先取 `face`，`thumbOrMissing()` 为 13113 渲染首字占位框（不再拿水晶图假充头像）；幻兵列表名称列头像放大到 38 px，全局搜索结果卡片也带 22 px 缩略图；`summonGallery()` 在幻兵页表格上方渲染 9 张召唤兽头像卡，点击走 `data-entity="summon"` 打开 `showSummon()` 详情（128 px 大图 + 9 语言名称表 + 图标文件路径）；幻兵详情用 `portraitHero()` 在顶部展示 128 px 大头像与图标文件名，取代原来右侧 72 px 的浮动小图。`readHash()` 支持 `#/visions/<summon id>` 深链。新增 `portraitRows` 共 5 个 9 语言 UI key（标题、说明、头像、图标文件、缺失提示），`is` 伪语言回退英文。

**验证。** `verify_wiki.py` 专项 5：zh-Hans / ja / de / fr 四语言幻兵页均须出现 `25` 个列表头像、`9` 张召唤兽卡、`1` 个缺失占位、`34` 个唯一 `Vision_Face` 路径，0 个 `undefined`；`#/visions/13110` 大图、`#/visions/13113` 占位文案、`de#/visions/10451 → Sirene` 深链与语言表全部命中。浅色 / 深色主题另做截图目视检查。

### 12.7 上线部署

站点是纯静态 SPA：入口 `index.html`，运行时只依赖 `assets/`（app.js / i18n.js / style.css）、`data/wiki-data.js` 与 `media/` 下的图标；`docs/` 是方法文档、`tools/` 是本地构建脚本，二者不参与前端运行。无需数据库、后端或打包步骤，把整个目录放进静态站点根目录即可；路由用 hash（`#/visions/13110`），因此不需要服务端 rewrite 规则。

- 当前部署：`https://ffrs.redqd.com`，Nginx 站点根目录 `/www/wwwroot/ffrs.redqd.com`；文件属主 `www:www`，目录 755 / 文件 644。
- 线上验证：首页、三个 JS/CSS、数据集、`Vision_Face` PNG、`docs/数据提取方法.md` 全部返回 200；无头浏览器在真实域名下确认 9 语言、25 个幻兵头像、9 张召唤兽卡与致谢链接渲染正常，0 个 `undefined`。
- 「参考与致谢」页列出两个原站网址（`https://ffresonance.wiki/ja/`、`https://ffrs.fireemblem.top/`）并可点击；角色 / 幻兵等详情抽屉仍不含任何站外链接。
- 页面文案、页脚与 `meta description` 已统一为线上站点表述（`file://` 本地预览阶段的说明全部移除）；图标仍随站点一同托管，不依赖外部图床。
- Nginx 当前仅监听 80 端口、未配置 SSL 证书。如需 HTTPS，在宝塔面板为该站点申请证书并开启强制跳转即可，静态资源本身无需改动。

### 12.8 推广与可发现性基建

主站是 hash 路由 SPA（`#/equipment`），URL 片段不会发送到服务器，栏目页本身无法被单独索引；同时原页面只有 `title` 与 `description` 两个元信息。为让站点可被搜索、可被分享，补了以下一层（全部为静态资源，不引入框架与服务端）：

| 层 | 实现 | 作用 |
|---|---|---|
| 站内元信息 | `index.html`：canonical、8 语言 `hreflang` + `x-default`、Open Graph / Twitter Card、`WebSite` JSON-LD、keywords、favicon / apple-touch-icon、`<noscript>` 静态摘要 | 搜索结果与社交分享卡片；`<noscript>` 给不执行 JS 的爬虫一段可读的中文 + 英文摘要 |
| 动态语言元信息 | `assets/app.js` 的 `renderTop()` 按当前语言改写 `document.title` 与 `meta[name=description]`；`assets/i18n.js` 新增 `siteDesc` 等 4 个 9 语言 key | 切换语言后浏览器标签与搜索摘要同步本地化 |
| 静态落地页 | `about.html`：8 语言介绍、数据规模、15 个栏目入口、FAQ（`FAQPage` JSON-LD）、来源署名、二维码 | **爬虫无需执行 JS 就能读到全部 8 语言文本**；JS 只负责按 `?lang=` 隐藏其他语言 section（HTML 里默认全部可见，保证可抓取） |
| 站点地图 | `robots.txt` + `sitemap.xml`（19 条 URL，含 hreflang 交叉引用） | 供 Google / Bing / 百度提交 |
| 视觉素材 | `assets/og-image.png`(1200×630)、`qr.png`(350×350)、`favicon.svg`、`apple-touch-icon.png`，渲染源 `tools/og-card.html` | 分享卡、二维码、标签图标；用无头 Edge `--screenshot` 生成，`qr.png` 由 `segno` 生成 |
| 站内分享 | 首页新增「复制分享链接」按钮（`navigator.clipboard` + `execCommand` 兜底）与「关于本站」入口 | 生成带当前 `?lang=` 的链接，便于访客转发 |
| 生成器 | `tools/gen_about.py` 一键产出 `about.html` / `robots.txt` / `sitemap.xml`，条数自动读 `data/wiki-data.js` | 数据更新或切换协议时重跑即可：`FFRS_SITE_URL=https://ffrs.redqd.com python tools/gen_about.py` |

`tools/verify_wiki.py` 的「详情零外链」断言改为只检查 `<body>`（`body_only()`），因为 `<head>` 中的 canonical / hreflang 是合法的绝对链接；另新增专项 6 校验 SEO 元信息、推广文件、落地页 8 语言可见性、sitemap XML 与分享按钮。

推广文案、渠道清单、UTM 模板、搜索引擎提交步骤与合规边界见 `docs/推广素材.md`（8 语言短文案 + 中 / 日 / 英长文案）。

**已知限制**：① 栏目级收录仍需为 15 个栏目各做静态快照页；② 服务器当前仅监听 80、无 SSL 证书，Google 排名与微信 / X 分享卡片均偏好 HTTPS，开启后需把 `index.html` 的绝对 URL 改为 `https://` 并重跑 `gen_about.py`；③ 服务器在国内，百度收录通常要求域名完成 ICP 备案。

### 12.9 页内搜索框的中文 / IME 输入修复

**症状**：装备图鉴等栏目页的页内搜索框（`#page-q`）无法输入中文（日文假名、韩文同理），英文却可以逐字符输入。

**根因**：`input` 事件里每次都调用 `render()`，后者整体重写 `#main.innerHTML`，输入框 DOM 节点随之销毁。IME 的合成（composition）状态绑定在节点上，节点一销毁，拼音 / 假名候选就被取消，中文根本无法上屏。原代码事后用 `setTimeout` 做 `focus()` + `setSelectionRange()`，只能把英文光标救回来，救不回输入法状态 —— 这正是「英文能用、中文不行」的原因。

**修复**（`assets/app.js`）：

1. `compositionstart` / `compositionend` 维护 `imeComposing`；**合成期间只更新 `state.q`，不做任何重渲染**，拼音阶段表格不刷新也不打断输入法。
2. `compositionend`（选字上屏）后刷新一次筛选结果；合成结束后的那次 `input` 也会走同一路径，节点依旧保留。
3. 新增 `renderKeepSearch()`：重渲染前用一个注释节点把 `#page-q` 从 DOM 摘出，`render()` 完成后把**同一个 input 节点**放回新生成的 toolbar，并恢复滚动位置、焦点与光标位置。节点身份不变，因此可以连续输入、连续使用 IME。
4. 删除原来的 `setTimeout(focus)` 兜底。

**回归**：`tools/verify_wiki.py` 专项 7 生成临时同源测试页，用 iframe 派发 `CompositionEvent` / `InputEvent`（Edge 需 `--allow-file-access-from-files`），在装备图鉴输入「轮回」、敌人图鉴输入「ゴブリン」，断言合成中与合成后输入框是同一个节点、值未丢失、筛选结果正确，以及全局搜索框的中文值与 `#/search` 跳转正常。两套栏目分别覆盖专用页 `pageEquipment()` 与通用页 `pageGeneric()`。

### 12.10 MCP 端点、AI 可发现性与人与 AI 分别计数

按 `mcp-website-promotion.md` 的判断，本站从「只给人看的静态站」补上「可被 AI agent 直接调用」这一层：
`POST /mcp` 是 Streamable HTTP（JSON-RPC 2.0，协议版本 `2025-06-18`，只读、免鉴权），
由 `tools/ffrs_mcp_server.py` 以 Python 标准库实现，直接读 `data/wiki-data.js` 输出 9 语言结构化 JSON。
8 个工具（`search_ffrs` / `get_entity` / `list_dataset` / `compare_equipment` / `find_sources` /
`find_drops` / `lookup_label` / `wiki_stats`）的描述按「用户会派什么任务」撰写，
输出键名保持稳定、值经 5,373 条 `labels` 官方标签本地化。

发现层：`/llms.txt`（意图 → 工具映射）、`/.well-known/mcp.json`、`/.well-known/agent.json`、
`/server.json`、`/.well-known/mcp-registry-auth`，并已上架官方 MCP Registry
（`com.redqd.ffrs/ffrs-wiki@1.0.0`，HTTP 域名验证，status active）。
站点同时启用 Let's Encrypt 证书，80 / 443 双栈，全部绝对 URL 统一为 `https://`。

访问计数由同一服务提供：Nginx combined access log（覆盖不执行 JS 的爬虫）+ `mcp-events.jsonl`
（记录每次工具调用），按 UA 与路径分为 `human` / `ai` / `search_bot` / `monitor` / `other`；
所有 `/mcp` 请求归入 AI。按自然日分桶覆盖写入，重复刷新与日志轮转都不会重复计数；
无 Cookie、无跨站追踪。结果见 `/stats.html` 与 `/api/stats`，主站首页与侧栏也有实时入口。

**验证**：`tools/verify_mcp.py` 自启本地服务或指向线上，跑 38 项断言
（initialize / 会话头 / 通知 / 8 个工具 / resources / prompts / SSE / 批量 / 错误码 / 计数分类 / 发现文件），
本地与线上均 PASS；`tools/verify_wiki.py` 增加 MCP 发现文件与 sitemap/robots 专项后 9 语言回归 PASS。
部署、协议细节、计数口径与隐私边界见 `docs/MCP接入与推广.md`。

### 12.11 IndexNow：把搜索层收录也补齐

`mcp-website-promotion.md` 追加的分发渠道清单里，Registry 之外还缺「搜索引擎主动申报」这一环。
`robots.txt` 只是允许抓取，IndexNow 才是把 URL 主动推进 Bing / Yandex / Seznam / Naver / Yep 索引的动作；
Bing 索引同时是 GPTBot / ClaudeBot 等 AI 搜索 grounding 的主要数据源之一，因此它也能反过来改善 GEO。

实现：`tools/indexnow_submit.py`（纯标准库）读 `sitemap.xml` 的 `<loc>`，过滤 `/.well-known/`、
`/server.json` 等机器文件后得到 22 条 URL，向 `https://api.indexnow.org/indexnow`
POST `{host, key, urlList}`；域名所有权通过站点根目录的 `<32位hex>.txt` 验证
（文件内容等于 key 本身，是公开验证串、不是凭据）。首次提交返回 202，复跑 200。
`tools/gen_about.py` 的 `<lastmod>` 改为按当天日期生成，状态指纹取「URL 列表 + sitemap 原始字节」，
因此每次重建 sitemap 都会触发 cron（每日 04:17）自动重推，未变化则跳过。
`tools/verify_wiki.py` 新增专项，确保 key 文件与提交脚本始终随站点打包。

渠道逐项状态（含仍需账号的 GSC / 百度站长、需人工表单的 llms.txt 目录站、需公开仓库的第三方 MCP 目录）
见 `docs/推广素材.md` 第 12 节；协议与部署细节见 `docs/MCP接入与推广.md` 第 9 节。

---

## 13. 数据来源与署名

| 来源 | 用途 | 说明 |
|---|---|---|
| **游戏本体**（`DT_*.uexp` / 9 份 `Game.locres` / `Game.locmeta` / `DefaultGame.ini`） | 全部数值、官方 8 语言名称与描述、支持语言清单、语言自称、UI 术语、参数名 | 本文方法直接提取 |
| `ffrs.fireemblem.top` 公开 `world-data.js`（2026-10-23 导出） | 装备参数标签、获取来源、敌人 / 任务 / 地图 / 商店等结构化数据；`*-relations.js` 三类关联 | 交叉验证 + 补全，已在 wiki 页脚署名 |
| `ffrs.fireemblem.top/media/**` | 458 张装备 / 角色 / 敌人 / 幻兵图标，其中 `Vision_Face` 34 张为幻兵与召唤兽官方头像 | 下载到 `media/` 并随站点一同部署，文件名与路径原样保留；头像与数据记录的关联方法见 12.6 |
| `ffresonance.wiki/ja` | 栏目结构、分类与页面组织参考 | 另解析其幻兵 / 角色列表页的**职能、出处作品、武器、极限技**（26 体 / 8 位），按官方日文名匹配后合并进 wiki；正文未复制 |

> 参考站补充数据的解析入口见 `../tools/build_wiki_data.py` 的 `parse_ref_sites()`，输入为 `refs/ja_visions.html` 与 `refs/ja_chars.html`（可选，缺失时自动跳过）。幻兵职能分布实测：防御者 4 / 攻击者 8 / 破防者 10 / 治疗者 2 / 干扰者 2。

> 本 wiki 为**非官方粉丝资料站**，与 SQUARE ENIX 无关联。所有游戏文本版权归原权利方所有。
