# 26. Pipeline Trích Xuất Read-Only Content.ggpk → SQLite

> **Mã**: `SPEC-ARCH-26-GGPK-OFFLINE-STATIC-DB-20260910`  
> **Mốc**: 10/09/2026  
> **Nguyên tắc**: Chỉ đọc `Content.ggpk`. Không sửa byte, không repack, không đụng Merkle / PackCheck.

---

## 1. Hiện trạng đã đo trên client thật (10/09/2026)

| Mục | Giá trị đo được |
|---|---|
| Đường dẫn | `C:\Program Files (x86)\Grinding Gear Games\Path of Exile 2\Content.ggpk` |
| Dung lượng | **161,927,115,259** bytes (khớp thẩm định 07/09/2026) |
| Root PDIR | 10 entries; tâm dữ liệu là `Bundles2` (42 entries) |
| `Bundles2/_.index.bin` | nén 114,953,184 B → giải nén 149,123,238 B; encoder=12; 569 block × 256 KiB; magic chunk `8C 0C` (Oodle Leviathan) |
| `Bundles2/Folders/data.dat.bundle.bin` | nén 6,853,303 B → giải nén 9,667,076 B; 37 block; cùng magic `8C 0C` |
| `HashCache.dat` | 2,846 B — SHA-256 các file *ngoài* GGPK, không phải bảng `.datc64` |
| `oo2core*.dll` trong thư mục game | **Không có** (Oodle gắn tĩnh trong `PathOfExile.exe`) |

Kết luận vận hành: vỏ GGPK vẫn là `GGPK/PDIR/FILE`; >99% payload nằm trong Bundles2 nén Leviathan. Công cụ GGPK đời PoE1 không đọc được bảng dữ liệu.

---

## 2. Kiến trúc pipeline (Tier 2 only — cấm Hot Path)

```
Content.ggpk (read-only)
    → GgpkFs (PDIR/FILE walker)
    → BundleHeader + Oodle chunk 256 KiB
    → _.index.bin (MurmurHash64A seed 0x1337b33f, path spec 3.21.2+)
    → data/*.datc64 slices
    → dat-schema (validFor 2|3) → SQLite WAL
         assets/game_data/poe2_ggpk.db
    → projection base_items / world_areas / … cho wiki_db.py
```

C++ Core 120 Hz **không** mở SQLite, không đọc GGPK.

Cấm tuyệt đối: patch `Content.ggpk`, ghi `FILE` record, chạy PackCheck để “sửa” hash.

---

## 3. Hợp đồng SQLite

File: `assets/game_data/poe2_ggpk.db` (`PRAGMA journal_mode=WAL`)

| Nhóm bảng | Nguồn | Khi nào có |
|---|---|---|
| `extract_meta` | path/size/mtime GGPK, backend Oodle, schema `createdAt` | mọi lần chạy |
| `ggpk_files` | cây PDIR/FILE (tên, offset, size, SHA-256 header) | mọi lần chạy |
| `hashcache_files` | `HashCache.dat` | mọi lần chạy |
| `logical_files` | index Bundles2 (path, bundle, offset, size) | khi giải nén `_.index.bin` |
| `dat_*` | từng bảng `.datc64` | khi giải nén bundle chứa `data/` |
| `base_items`, `world_areas`, `skill_gems`, `uniques`, `item_classes`, `mods`, `monsters` | projection cho HUD | cùng lúc với `dat_*` |

`uniques` = `UniqueStashLayout.WordsKey` ⋈ `Words.Text` (schema PoE2 không có cột `Name` trên UniqueStashLayout).

Bảng `.datc64` ưu tiên: `BaseItemTypes`, `ItemClasses`, `WorldAreas`, `Mods`, `Stats`, `Tags`, `MonsterVarieties`, `DefaultMonsterStats`, `SkillGems`, `ActiveSkills`, `Quest`, `QuestStates`, `Words`, `UniqueStashLayout`, `ArmourTypes`, `WeaponTypes`, `CurrencyItems`, `NPCs`.

---

## 4. Oodle (Leviathan `8C 0C`)

Client PoE2 **0.5.5 không** ship `oo2core*.dll`. Pipeline nạp:

1. `AUTOPOE2_OODLE_DLL`
2. `tools/bin/libooz.dll` (build `zao/ooz`, export `Ooz_Decompress` — không commit DLL)

Export: `Ooz_Decompress` (14 đối số, libooz), `OodleLZ_Decompress` (oo2core), hoặc `Kraken_Decompress`.

Parser `.datc64` cắt cột schema cho khớp `rowSize` của **0.5.5** (schema cộng đồng có thể rộng hơn 8–24 byte). Mỗi bảng chỉ lấy 1 file canonical `data/<name>.datc64`.

Không có DLL → catalog + HashCache, **không** bịa `dat_*`. Mã thoát 2.

---

## 5. CLI

```powershell
python tools/extract_ggpk_to_db.py
python tools/extract_ggpk_to_db.py --catalog-only
python tools/extract_ggpk_to_db.py --tables BaseItemTypes,WorldAreas,Mods
```

Schema cộng đồng: `assets/game_data/schema.min.json` (poe-tool-dev/dat-schema, `validFor` 1=PoE1, 2=PoE2, 3=cả hai). Tải lại nếu thiếu.

---

## 6. Kiểm thử

- Parser `HashCache.dat` trên file thật 2,846 B tại thư mục client.
- Root GGPK + header `_.index.bin` / `data.dat.bundle.bin` đọc trực tiếp từ `Content.ggpk`.
- `datc64` layout (rowCount + marker `0xBB`×8) trên fixture hợp đồng parser.
- Ánh xạ bundle `data` → `Bundles2/Folders/data.dat.bundle.bin`; projection `uniques` = WordsKey ⋈ Words.Text.
- Nếu Oodle có mặt: tra `Divine Orb` / `The Riverbank` từ `poe2_ggpk.db` (dữ liệu client thật).
- Không có Oodle: CLI thoát mã 2, SQLite vẫn có `ggpk_files` + `hashcache_files`, **không** bịa `dat_*`.

### Bằng chứng chạy thật (10/09/2026 — client 0.5.5 Forbidden Rites)

GGPK `161,927,115,259` B, mtime 05/09/2026. `libooz.dll` (export `Ooz_Decompress`) build local `zao/ooz`.

```
python tools/extract_ggpk_to_db.py
# status=complete  dat_tables=23  oodle_backend=ooz14:libooz.dll
# elapsed 585186 ms
# extract_meta.client_patch=0.5.5  client_league=Forbidden Rites

# Hàng đo từ poe2_ggpk.db (cùng số với RePoE JSON 07/09 khi có):
#   base_items 5496   world_areas 442   soul_cores 313
#   skill_gems 1191   uniques 449       mods 16784
#   map_tablets 8     item_classes 118
# Divine Orb = Metadata/Items/Currency/CurrencyModValues  stack 10
# Jiquani's Soul Core of Abundance = Metadata/Items/SoulCores/SoulCoreSpecial26  drop 65
# Sacred Bloom = Metadata/Items/MapFragments/CurrencyWildwoodFragment
# The Riverbank = G1_1  act 1  area_level 1

python -m pytest tests/test_ggpk_extract.py tests/test_wiki_db.py -v
# 16 passed in 0.59s
```

`wiki_db.resolve` trỏ `poe2_ggpk.db` khi đã có `base_items`.
