# 29. Đặc Tả BotBrain, Máy Trạng Thái Ưu Tiên, Pickit DSL Và Vòng Farm Khép Kín

- **Mã tài liệu**: `SPEC-DOC-29-BOTBRAIN-PICKIT-FARM-LOOP-20260911`
- **Mốc thời gian / phiên bản tham chiếu**: `11/09/2026` (đính chính vận hành `13/09/2026`, hàng đợi tự hành `15/09/2026`) — Path of Exile 2 **0.5.5**, AutoPOE2 IPC `PROTOCOL_VERSION = 0x00050005`
- **Trạng thái**: `[CODE-PRESENT P0–P5 — LIVE LOOP CHƯA NGHIỆM THU]` — Arbiter / pickit / opcode 5–8 **có trong mã**. **Không** tuyên bố Closed Farming Loop ổn định. Vitals/XYZ/portal `.`: [Doc 43 (archive)](./43_current_status_and_next_development_basis.md). Hàng đợi: [Doc 61 leftover](./61_autonomy_gap_audit_and_next_development_basis.md).
- **Nguồn học**: nghiên cứu ExiledBot 2 Beta v0.10.1 (config, WebUI, public docs, tên module log). **Cấm** copy file `.ipd` mặc định, DLL, hoặc rule list có bản quyền của họ. Chỉ tái sử dụng **ý tưởng kiến trúc**.
- **Độc giả**: mọi agent viết C++ Core, Python Companion, test, hoặc UI. Đọc mục 0 trước khi đụng code.

---

## 0. Cách dùng tài liệu này (bắt buộc)

Tài liệu này là **Single Source of Truth** cho vòng full-auto. Nếu code lệch tài liệu, **sửa code hoặc sửa tài liệu trong cùng một đợt**, không để lệch.

| Agent định làm việc | Đọc trước | File được phép sửa |
|---|---|---|
| Arbiter / một action mỗi tick | 2, 3, 4, 5, 18.P0 | `src/core/brain/*`, `src/core/main.cpp` |
| Loot / pickit | 1.1, 8, 9, 10, 18.P1–P2 | `src/core/looting/*`, `src/assistant_tool/pickit/*`, `data/pickit/*` |
| Entity types / sự kiện map | 7, 11, 18.P1 | `game_layout.hpp`, `protocol.hpp`, `entity_manager.*`, `quest_navigator.*` |
| Stash / vendor | 12, 16, 18.P3 | `main.cpp` `DispatchCommand`, `src/core/inventory/*`, Python policy |
| Combat skill table | 13, 18.P4 | `src/core/combat/*`, Control Center |
| Atlas / waystone loop | 11, 14, 18.P5 | **sau khi P0–P3 PASS** |
| Test | 17, 19 | `tests/test_core.cpp`, `tests/test_pickit_evaluator.py` |

**Cấm khi implement:**

1. Gọi `LootController::Update` + `ComboManager::Update` + `QuestNavigator::Update` cùng tick mà cả ba đều click chuột (đó là hiện trạng 11/09/2026, **không** phải đích).
2. Parse WeightedSum / IPD đầy đủ trong vòng 120Hz.
3. Fake HP, fake loot, `|| true`, comment-out assertion.
4. Inject DLL vào `PathOfExile.exe` theo kiểu EB2. AutoPOE2 giữ RPM ngoài process + KMBox.
5. Copy `ExiledBot 2 Beta v0.10.1/Configuration/**/*.ipd`.
6. Đánh dấu `[COMPLETED]` khi chưa có stdout test thật trong phiên đó.

---

## 1. Hiện trạng code (11/09/2026) vs đích

### 1.1. Đã có — được phép tái sử dụng, không viết lại

| Module | Đường dẫn | Việc đang làm đúng |
|---|---|---|
| Vòng 120Hz | `src/core/main.cpp` ~1131–1165 | Đọc RAM → telemetry → reflex → (combo/loot/quest song song) → SHM |
| Survival | `src/core/combat/reflex_manager.*` | Flask, dodge, kiting, chicken/portal, CI ES, auto-respawn |
| Combo | `src/core/combat/combo_manager.*` | Xoay skill / stagger khi không Co-Pilot |
| Loot vacuum | `src/core/looting/loot_controller.*` | Click nhặt theo **tên + rarity**, ưu tiên 10–100 |
| Explore in-map | `src/core/navigation/quest_navigator.*` | `MapScenario` BOSS_RUSH / FAST_CLEAR / FULL_CLEAR / ATLAS_QUEST_RUSH — **heuristic trong map**, không phải atlas UI |
| Terrain A* | `terrain_reader.*`, `terrain_grid.*`, `pathfinder.*` | Lưới walkable quanh player, không Recast full-map |
| Entity map | `entity_manager.*`, `pointer_chain_resolver.*` | `InGameState + 0x3E8` → EntityMap |
| IPC | `protocol.hpp`, `core_shm_bridge.py` | SHM 4MB, seqlock, opcode 0–6 và 95–99 |
| Town travel | `town_quest_engine.*` | Phím U world map khi **đang ở town** |
| Visual | `visual_mods.*`, `src/visual_tool/` | Zoom/fog — tách khỏi farm loop |
| Companion | `control_center.py` | F8 start Core, scenario UI |

### 1.2. Phạm vi SPEC 29 — snapshot 11/09 vs code 13/09

> **Đính chính 13/09/2026:** bảng dưới là *baseline 11/09* (lúc viết spec). Tại 13/09: arbiter `bot_brain.*` **có**; `EntityType` 0–12 **có** (heuristic tên); pickit DSL Python → JSON **có**; opcode 5–8 **có** `DispatchCommand`; `map_device_handler` / `npc_interaction_handler` **có**. Vẫn thiếu nghiệm thu live và (PLANNED) atlas craft, party follow, lịch nghỉ. Chi tiết: [doc 43 mục 3.2](./43_current_status_and_next_development_basis.md).

| Hạng mục | Bằng chứng thiếu |
|---|---|
| Arbiter một action/tick | `main.cpp` gọi reflex rồi combo rồi loot rồi quest **liên tiếp**, cả ba module tự click |
| Entity Chest/Shrine/Essence/Breach/Ritual | `EntityType` chỉ 0–4 trong `game_layout.hpp` |
| Minimap icon hunting | không có code minimap |
| Pickit DSL / mods | `ShouldLoot` so khớp substring `"orb"`, `"waystone"`… |
| Stash / vendor | `MacroOpCode` 5 và 6 **khai báo** nhưng `DispatchCommand` rơi `default` → `"chưa được hỗ trợ"` (`main.cpp` ~257–260) |
| Atlas / map device / tablet / waystone craft | không có handler |
| Party follow | `log_sensor` chỉ parse chat |
| Humanization lịch nghỉ | chỉ Bézier + reaction latency |
| Log `Running X with priority` | không có |

### 1.3. Đính chính tài liệu cũ

- `docs/development/25_spec_endgame_atlas_map_scenarios_and_boss_rush_architecture.md` mô tả **kịch bản tuần tra trong map**. Không phải vòng hideout → stash → waystone → atlas. Vòng đó là SPEC 29 (**CODE-PRESENT**, live chưa chứng minh — doc 43).
- `docs/development/03_combat_and_assist_system.md` mục Full-Auto “về thị trấn cất hòm”: opcode stash/vendor **đã** dispatch; FSM UI **chưa** live-proven.

---

## 2. Bất biến kiến trúc (Invariant) — vi phạm = bug P0

| ID | Bất biến | Kiểm tra |
|---|---|---|
| **INV-BRAIN-01** | Mỗi tick 120Hz, **tối đa một** hành động chuột/di chuyển/tương tác thế giới (click loot, click quái, click chest, click portal, A* move). | Test: hai handler cùng Propose > 0 → chỉ `Execute` cái priority cao hơn. |
| **INV-BRAIN-02** | Reflex sinh tồn (flask, dodge, chicken, emergency portal) **được chạy trước arbiter** và nếu đã tiêu thụ input thì arbiter **không Execute** click/move tick đó. | Survival không bao giờ chờ pickit/explore. |
| **INV-BRAIN-03** | Panic / `isRunning==0` / mất focus (`EnsurePoe2WindowFocus` false, trừ ignore-focus test) → không Propose click. | Khớp doc 11 interlock. |
| **INV-BRAIN-04** | Khi `autoLogin.IsActive()` hoặc `townQuestEngine.IsActive()` → BotBrain **không** tranh input (giữ nhánh hiện tại trong `main.cpp`). | |
| **INV-BRAIN-05** | Mọi Execute phải log một dòng: `Running {Kind} with priority: {Name} ({n}) entity={id}`. | Parse được bằng test / log watcher. |
| **INV-HOT-01** | BotBrain + Propose **không** syscall DB, không parse file IPD, không heap alloc lớn mỗi tick. IPD parse một lần lúc load. | 120Hz hot path. |
| **INV-PICKIT-01** | WeightedSum / post-ID chỉ chạy **cold path** (Python hoặc dump F7), không trong `LootController::Update`. | |
| **INV-PICKIT-02** | Cùng một rule engine cho loot và map policy; **khác action namespace**. `IgnoreMap` thắng `RunMap` và mọi craft flag. | Test đơn vị. |
| **INV-PICKIT-03** | Pre-`#` không được đọc UniqueName / Computed\* / WeightedSum. Post-`#` mới được. | Parser reject. |
| **INV-SAFE-01** | Có quái sống trong `safe_range` → **cấm** Pickup và OpenChest. Combat/EmergencyAttack thắng. | Test mock pack sát chân. |
| **INV-EVENT-01** | Map không được đánh dấu complete khi còn `AreaEvent` committed chưa Complete/Fail/timeout. | |
| **INV-IPC-01** | Thêm giá trị `EntityType` 5–12 **không** đổi `sizeof(EntityTelemetryData)==88`. Đổi layout struct thì **bắt buộc** tăng `PROTOCOL_VERSION` và sửa Python `struct` cùng commit. | `static_assert` hiện có. |
| **INV-IPC-02** | Opcode 5 và 6 không được im lặng `default`. Implement hoặc log `unsupported` **có tên opcode** và không crash. Phase P3 phải Execute thật. | |
| **INV-DOC-01** | Chưa merge P0 thì README/spec khác không được ghi “BotBrain đã chạy”. | |

---

## 3. Kiến trúc đích: BotBrain

### 3.1. Sơ đồ tick (thay `main.cpp` 1151–1165)

```mermaid
flowchart TD
    Read["gameSession.Update(packet)  đọc RAM"] --> Reflex["reflexMgr.Update  INV-BRAIN-02"]
    Reflex -->|ConsumedExclusiveInput == true| Skip["Không Execute click/move\nvẫn WriteTelemetry"]
    Reflex -->|false và không Co-Pilot| Propose["Mỗi handler.Propose(packet)"]
    Propose --> Pick["BotBrain chọn max priority\ntie-break: dist nhỏ hơn, rồi entityId nhỏ hơn"]
    Pick --> Exec["handler.Execute(chosen)  đúng 1 lần"]
    Exec --> Log["INV-BRAIN-05 log"]
    Skip --> SHM["shm.WriteTelemetry"]
    Log --> SHM
    SHM --> Cmd["FetchNextCommand DispatchCommand"]
```

**Chế độ Co-Pilot** (`isPassiveCoPilotMode`): chỉ ReflexManager. BotBrain không Propose combat/loot/explore. Giữ nguyên hành vi F8 Co-Pilot hiện tại.

**Chế độ Full-Auto**: Reflex + BotBrain. Combo/Loot/Quest **không** tự click trong `Update`; chúng implement `IActionHandler`.

### 3.2. API C++ bắt buộc

Tạo `src/core/brain/bot_brain.hpp` và `bot_brain.cpp`. Thêm vào `CORE_LIB_SOURCES` trong `CMakeLists.txt`.

```cpp
enum class BotActionKind : uint16_t {
    None = 0,
    Chicken = 1,
    Flask = 2,
    Dodge = 3,
    Unstuck = 4,
    EmergencyAttack = 5,
    Combat = 6,
    AreaEvent = 7,
    Pickup = 8,
    OpenChest = 9,
    WaitLoot = 10,
    WaitBoss = 11,
    Explore = 12,
    Stash = 13,
    Vendor = 14,
    SwitchArea = 15,
    Idle = 16
};

struct ActionProposal {
    BotActionKind kind = BotActionKind::None;
    uint32_t priority = 0;       // 0 = không đề xuất
    uint32_t entityId = 0;
    float targetX = 0.0f;
    float targetY = 0.0f;
    float targetZ = 0.0f;
    float distance = 0.0f;
    const char* priorityName = "NONE"; // literal tĩnh, không alloc
};

class IActionHandler {
public:
    virtual ~IActionHandler() = default;
    virtual const char* Name() const = 0;
    virtual ActionProposal Propose(const TelemetryPacket& packet, uint64_t nowMs) = 0;
    virtual bool Execute(const ActionProposal& chosen, const TelemetryPacket& packet,
                         KMBoxNet& kmbox, uint64_t nowMs) = 0;
};

class BotBrain {
public:
    void AddHandler(IActionHandler* handler); // không sở hữu
    const ActionProposal& LastChosen() const;
    // Chọn + Execute. Trả true nếu đã Execute.
    bool Process(const TelemetryPacket& packet, KMBoxNet& kmbox, uint64_t nowMs);
};
```

Thuật toán `Process` (đúng từng bước):

1. `best = {}` (priority 0).
2. Với mỗi handler: `p = handler->Propose(packet, nowMs)`.
3. Nếu `p.priority == 0` hoặc `p.kind == None` → bỏ.
4. Thắng nếu `p.priority > best.priority`.
5. Hòa priority: `p.distance < best.distance` thắng; hòa tiếp: `p.entityId < best.entityId` (0 coi như max uint32 để proposal không entity không ăn tie-break entity).
6. Nếu `best.priority == 0` → Idle, return false.
7. Tìm handler sở hữu `best.kind` (mỗi kind một handler). `Execute`. Log INV-BRAIN-05. Lưu `LastChosen`.

`ReflexManager` bổ sung:

```cpp
bool ConsumedExclusiveInput() const; // true nếu tick này đã flask / dodge / chicken / portal
```

`Update` hiện trả true **chỉ khi emergency portal**. Agent implement P0 **phải** set flag nội bộ cho flask/dodge/chicken nữa, không đổi ngưỡng flask.

### 3.3. Ánh xạ handler hiện có → kind

| Handler (mới hoặc bọc) | Source cũ | Kind đề xuất |
|---|---|---|
| `UnstuckHandler` | **mới P0 stub** (Propose=0 cho đến P4) | `Unstuck` |
| `CombatHandler` | `ComboManager` + kiting đã có trong Reflex | `Combat`, `EmergencyAttack` |
| `LootHandler` | `LootController` tách Propose/Execute | `Pickup` |
| `ExploreHandler` | `QuestNavigator` tách Propose/Execute | `Explore`, `SwitchArea`, `WaitBoss`, `WaitLoot` |
| `AreaEventHandler` | **mới P1** | `AreaEvent`, `OpenChest` |
| `InventoryHandler` | **mới P3** | `Stash`, `Vendor` |

Một class có thể Propose nhiều kind; `Execute` switch theo `chosen.kind`.

---

## 4. Bảng ưu tiên canonical (số tuyệt đối)

Dùng **đúng** các hằng này trong code (`namespace bot_priority`). Không bịa số khác. Tên log = cột `priorityName`.

| priority | priorityName | Kind | Điều kiện Propose |
|---:|---|---|---|
| 10000 | `CHICKEN` | Chicken | Reflex đã quyết định thoát; nếu Reflex tự Execute thì BotBrain không Propose thêm |
| 9000 | `FLASK` | Flask | Chỉ khi Reflex **không** tự uống và policy yêu cầu (mặc định Reflex tự uống → Propose=0) |
| 8500 | `DODGE` | Dodge | Tương tự Flask |
| 8000 | `UNSTUCK` | Unstuck | Lặp action / không dịch chuyển / kẹt nhãn (P4) |
| 7500 | `EMERGENCY_ATTACK` | EmergencyAttack | Quái sống `distance <= safe_range` (mặc định **50** world unit) |
| 7000 | `COMBAT` | Combat | Quái sống trong `monsters_attack_range` (mặc định **150**) và không bị Explore/Event chiếm khi `WAIT_*` |
| 6500 | `AREA_EVENT` | AreaEvent | Event đã Commit (essence, ritual, shrine, breach, strongbox, delirium) |
| 6000 | `PICKUP` | Pickup | Item `ShouldLoot` và **INV-SAFE-01** thỏa |
| 5500 | `OPEN_CHEST` | OpenChest | Chest/strongbox trong `chest_range` (mặc định **60**) và INV-SAFE-01 |
| 5000 | `WAITING_FOR_LOOT` | WaitLoot | Boss vừa chết, trong `boss_loot_wait_ms` (mặc định **2500** đang có ở QuestNavigator; đích farm map dùng **9000** khi bật map-farm — config, không hardcode hai nơi) |
| 4800 | `WAITING_FOR_BOSS` | WaitBoss | Đã thấy arena/boss icon hoặc `bossSpotted` nhưng boss chưa spawn |
| 4000 | `EXPLORE` | Explore | BFS/patrol `MapScenario` |
| 3500 | `STASH` | Stash | Đang town/hideout **và** (inventory đầy **hoặc** map run vừa xong) |
| 3400 | `VENDOR` | Vendor | Town **và** có item action sell/salvage/disenchant |
| 3000 | `SWITCH_AREA` | SwitchArea | Portal / map device / waypoint |
| 1000 | `IDLE` | Idle | Không việc; humanization pause (P4+) |

`QuestNavigator` hiện dùng priority nội bộ 1–4 (NPC/portal/boss). Đó là **priority chọn target trong explore**, không được trộn với bảng này. Khi bọc ExploreHandler: chọn target nội bộ xong rồi Propose `EXPLORE` = 4000 hoặc `COMBAT` = 7000 nếu đang chạy tới boss.

---

## 5. Hợp đồng Propose / Execute (chi tiết từng kind)

Mỗi Propose **chỉ đọc** `TelemetryPacket` + state nội bộ. Không MoveMouse / KeyPress.

Mỗi Execute **được phép** KMBox, nhưng phải gọi `IsGameWindowFocused` (hoặc `IsAnyIgnoreWindowFocus` trong test).

### 5.1. Pickup (`LootController`)

**Hiện trạng:** `Update` tự tìm item + click.

**Đích:**

- `Propose`: lặp `entities`, `type == EntityTypeItemDrop` (2), `ShouldLoot`, không nằm `ignoredOrLootedIds`, lấy `GetPriority` **item** (10–100) chỉ để chọn item tốt nhất. Proposal.priority **luôn 6000** nếu có candidate (không đưa 100 xuống arbiter — tránh loot vàng thắng combat).
- Nếu có quái sống `distance <= safe_range` → Propose Pickup **= 0** (INV-SAFE-01). CombatHandler sẽ Propose 7500/7000.
- `Execute`: giữ logic click hiện tại (`ExecutePickup`), ultra-fast dwell, Bézier. Đánh dấu id đã nhặt.

Gold: `ShouldLoot` true. Pickup vẫn 6000; gold không được đánh quái.

### 5.2. Combat / EmergencyAttack (`ComboManager`)

- `EmergencyAttack` nếu bất kỳ monster sống `distance <= safe_range`.
- `Combat` nếu monster sống `distance <= monsters_attack_range` (config; default 150) **hoặc** đang kiting.
- `Execute`: gọi nhánh hiện tại của `ComboManager::Update` **nhưng** chỉ khi `chosen.entityId` khớp target (hoặc 0 = tự chọn lại cùng tick, chấp nhận).
- Unique/Boss (`rarity==3` hoặc `extraFlags & 1`) luôn Propose Combat nếu nhìn thấy, kể cả ngoài attack range: lúc đó kind vẫn Combat, Execute = di chuyển tiếp cận bằng A* **hoặc** để ExploreHandler Propose Explore tới boss. **Chọn một nơi duy nhất**: CombatHandler Propose Combat 7000 với targetX/Y của boss khi `distance > combatStopDistance`; Execute gọi pathfinder bước một click. ExploreHandler **không** Propose Explore cho cùng entity tick đó (Combat thắng 7000 > 4000).

### 5.3. Explore / WaitBoss / WaitLoot / SwitchArea (`QuestNavigator`)

Giữ `MapScenario` (doc 25) như **policy khám phá**, không phải vòng atlas.

| Tình huống | Kind | priorityName |
|---|---|---|
| `bossSlain` và `now - slainTs < lootWait` | WaitLoot | `WAITING_FOR_LOOT` |
| `bossSpotted` nhưng không còn entity boss sống và chưa slain (đang spawn) | WaitBoss | `WAITING_FOR_BOSS` |
| Có portal/waypoint cần vào | SwitchArea | `SWITCH_AREA` |
| Patrol / A* frontier | Explore | `EXPLORE` |

WaitLoot/WaitBoss: Execute **không click** (hoặc chỉ click loot nếu LootHandler không Propose — không: loot 6000 > 5000 nên Pickup vẫn chạy trong cửa sổ chờ). Wait* chỉ **chặn Explore** (4000). Đúng: 6000 > 5000 nên vẫn nhặt. 4000 < 5000 nên không đi tuần. 7000 > 5000: nếu còn quái, vẫn đánh. Đó là đúng.

### 5.4. AreaEvent / OpenChest (P1)

Xem mục 11. Chưa classify được type 5+ thì Propose=0. **Cấm** giả chest bằng NPC type 4.

### 5.5. Stash / Vendor (P3)

Chỉ khi `AreaTelemetryData.areaName` là town/hideout (dùng cùng bộ lọc scene doc 27: không `Atlas`, không map chiến đấu). Execute gửi chuỗi click theo `coordinates` config; không block 120Hz quá 8ms — **state machine nhiều tick** (mỗi tick một click, giống stash_delay 400ms của EB2 nhưng số liệu nằm config AutoPOE2).

---

## 6. Sửa `main.cpp` (đoạn thay thế)

**Hiện trạng (cấm giữ khi P0 merge):**

```text
reflexMgr.Update(...)
comboMgr.Update(...)
lootCtrl.Update(...)
questNav.Update(...)
```

**Đích P0:**

```text
reflexMgr.Update(...)
if (!isPassiveCoPilotMode) {
    if (!reflexMgr.ConsumedExclusiveInput()) {
        botBrain.Process(samplePacket, kmbox, tickMs);
    }
}
```

`botBrain` được đăng ký handlers: combat, loot, explore. Unstuck/event/inventory thêm ở phase sau, Propose=0 không sao.

`DispatchCommand` vẫn chạy sau telemetry như hiện tại. Lệnh Agent (opcode 1–6, 95–99) **thắng** BotBrain **tick kế** nếu đang active (ví dụ world map travel). TownQuestEngine active → không Process (INV-BRAIN-04).

---

## 7. Taxonomy thực thể

### 7.1. Giữ nguyên layout telemetry

`EntityTelemetryData` 88 bytes. Chỉ mở rộng enum `type` (uint16). Python đọc uint16 — không đổi format string nếu đã unpack type là `H`.

`src/core/memory/game_layout.hpp` và comment `protocol.hpp` phải cùng enum:

```cpp
enum EntityType : uint16_t {
    EntityTypeUnknown    = 0,
    EntityTypeMonster    = 1,
    EntityTypeItemDrop   = 2,
    EntityTypePortal     = 3,
    EntityTypeNPC        = 4,
    EntityTypeChest      = 5,  // [PLANNED P1]
    EntityTypeShrine     = 6,  // [PLANNED P1]
    EntityTypeEssence    = 7,  // [PLANNED P1]
    EntityTypeBreach     = 8,  // [PLANNED P1]
    EntityTypeRitual     = 9,  // [PLANNED P1]
    EntityTypeStrongbox  = 10, // [PLANNED P1]
    EntityTypeDelirium   = 11, // [PLANNED P1]
    EntityTypeMinimapPoi = 12  // [PLANNED P5] icon minimap đã chiếu ra world
};
```

`extraFlags` hiện tại:

- bit 0: Boss
- bit 1: Targetable
- bit 2: Dead

P1 được thêm (không phá bit cũ):

- bit 3: Interactable world object (chest/shrine/…)
- bit 4: Hidden / not on walkable grid
- bit 5: Minimap-derived (không có full LifeComponent)

### 7.2. Phân loại P1 khi chưa có metadata object type từ RAM

**Tạm** heuristic **tên** (English client), tập trung một hàm `ClassifyEntityType(name, currentType, hasLife)`:

- Giữ Monster nếu `currentType==1` và `maxHP>0`.
- ItemDrop nếu type==2.
- Portal nếu type==3.
- Không được reclassify monster thành chest.
- Nếu type==0 hoặc 4 và tên chứa (case-insensitive) `Essence` / `Shrine` / `Strongbox` / `Breach` / `Ritual` / `Chest` / `Stash` (world chest, không town stash) → gán 5–10 tương ứng.

Heuristic tên **phải** có unit test với fixture tên thật từ `Client.txt` / RAM dump, không bịa.

P1+ ưu tiên đọc metadata object từ Entity component khi RE đã có offset; heuristic chỉ fallback.

### 7.3. Minimap (P5) — chưa làm

Không đoán icon. Khi làm: đọc minimap icon array từ InGameData (offset phải vào `offsets.toml` + registry), chiếu grid → world, tạo entity type 12. Cùng AreaEventHandler.

---

## 8. Mô hình vật phẩm (ItemView)

Radar 120Hz **không** chứa mods (88 byte/entity × 256). Pickup pre-ID chỉ dùng `name`, `rarity`, `type`.

### 8.1. ItemView (cold path)

Dùng cho pickit post-ID, map IPD, dump F7. JSON file, không nhét SHM.

```json
{
  "schema": "autopoE2.item_view.v1",
  "captured_at": "11/09/2026 10:37:00",
  "pid": 0,
  "area_name": "",
  "identified": false,
  "rarity": "Rare",
  "type": "Heavy Belt",
  "unique_name": "",
  "category": "Belt",
  "weapon_category": "",
  "item_tier": 0,
  "waystone_tier": 0,
  "item_level": 0,
  "quality": 0,
  "sockets": 0,
  "gem_level": 0,
  "base_armour": 0,
  "base_evasion": 0,
  "base_energy_shield": 0,
  "computed_armour": 0,
  "computed_evasion": 0,
  "computed_energy_shield": 0,
  "mods": { "base_maximum_life": 70, "base_fire_damage_resistance_%": 12 }
}
```

`mods` key = **stat id nội bộ game** (cùng vocabulary `assets/game_data` / GGPK `dat_stats` khi có), không phải tooltip.

### 8.2. Dump inventory (F7 analog)

- Hotkey Control Center hoặc CLI Core `--dump-inventory`.
- Ghi `logs/item_dump.jsonl` (một item một dòng) + log human-readable.
- Provenance: timestamp, PID, area. Cấm dump giả.

P2: đọc inventory từ RAM. P1: dump có thể chưa đủ mods — pickit post-ID skip rule cần mods.

### 8.3. Computed fields

Tính ở Python khi evaluate post-ID:

| Field | Công thức tối thiểu |
|---|---|
| `total_resistances` | sum các mod `*_damage_resistance_%` fire+cold+lightning (không chaos trừ khi rule gọi chaos) |
| `total_additional_attributes` | str+dex+int `additional_*` |
| `computed_*` | base + local% + flat local; nếu thiếu mod → 0, **không bịa** |
| `physical_dps` / `elemental_dps` / `dps` | từ local min/max phys + attack speed; thiếu data → field vắng, rule so sánh fail closed (không stash) |

Fail closed: thiếu data để đánh giá clause numeric → clause false, không true mặc định.

---

## 9. Pickit DSL — grammar và evaluator

### 9.1. File và profile

| Vai trò | Đường dẫn | INI chọn profile |
|---|---|---|
| Loot / stash / salvage | `data/pickit/<profile>.ipd` | `data/pickit/pickit.ini` → `active_profile=default` |
| Map run / craft / tablet | `data/maps/<profile>.ipd` | `src/assistant_tool/user_settings.json` hoặc `data/maps/maps.ini` → `map_profile=default` |
| Policy không phải item | không nhét IPD | stash tab index, disenchant vs sell, caps, ignored atlas names |

Python load lúc Companion start và khi user Save. Đẩy xuống Core **bảng pre-ID compiled** (xem 9.6), không đẩy text IPD.

### 9.2. Hình dạng một dòng

```
[KEY] OP "VALUE" (&&|\|\|) ... # [KEY] OP "VALUE" ...
```

- Trái `#`: **pre-identify**.
- Phải `#`: **post-identify + actions**.
- VALUE luôn quoted. Số cũng `"75"`. Bool `"true"` / `"false"`.
- `//` đầu dòng (sau trim): comment. `//[` hoặc `//!` = rule **tắt** (parser vẫn giữ để editor).
- Trailing `// comment` sau rule: cho phép.
- Dòng trống: bỏ.
- Thiếu `#` trên rule có action post-ID: **lỗi validate** (không load profile). Rule pre-only không action: cũng lỗi. Mọi rule giữ/stash/run phải có `#`.

### 9.3. Operator

`== != > >= < <=`  
`&&` `||` `()`

Rarity: cho phép `== !=` và so sánh thứ tự Normal < Magic < Rare < Unique khi dùng `>` `>=` `<` `<=`.

### 9.4. Key

**Identity / meta:** `Type`, `Category`, `WeaponCategory`, `Rarity`, `UniqueName`, `ItemTier`, `WaystoneTier`, `ItemLevel`, `Quality`, `Sockets`, `GemLevel`, `Armour`, `Evasion`, `EnergyShield` (base, pre), `ComputedArmour`, `ComputedEvasion`, `ComputedEnergyShield` (post), `TotalResistances`, `TotalAdditionalAttributes`, `DPS`, `PhysicalDPS`, `ElementalDPS`, `TotalSpellElementalDamage`, `TotalFireSpellDamage`, `TotalColdSpellDamage`, `TotalLightningSpellDamage`.

**Mod thô:** bất kỳ `[stat_id]` không nằm danh sách trên → lookup `item.mods[stat_id]`, thiếu = 0.

**WeightedSum:**

```
[WeightedSum(stat_a:w1,stat_b:w2)] >= "N"
```

`sum = Σ value(stat) * weight`; stat vắng = 0. Weight float. Nhiều WeightedSum trên một vế = AND.

**Action loot (chỉ pickit profile):**  
`StashItem`, `StashUnid`, `Salvage`, `Disenchant`, `IgnoreRitual` — so `== "true"`.

**Action map (chỉ maps profile):**  
`RunMap`, `IgnoreMap`, `UseTablet`, `IgnoreTablet`, `UpgradeToMagic`, `UpgradeToRare`, `AugmentIfPossible`, `Corrupt` — bool.  
`ExaltIfPossible` — numeric string, số mod explicit đích.  
`UpgradeWaystoneTier` — bool, P5+.

Cấm dùng action loot trong maps file và ngược lại (validator).

**Không phải IPD:**

- Sell vs disenchant toàn cục: `pickit.ini` `disenchant_items=true|false`.
- Ignore quest item: pipe list `ignored_quest_items`.
- Stash tab indexes, caps: INI/JSON.

### 9.5. Thuật toán evaluate (Python) — implement đúng thứ tự

Module: `src/assistant_tool/pickit/evaluator.py`.

Input: `ItemView`, list `Rule`.

```
actions = empty set
matched_run = false
matched_ignore_map = false

for rule in rules:
    if not rule.enabled: continue
    if not eval_expr(rule.pre, item, phase=PRE): continue
    if item.identified:
        if not eval_expr(rule.post_conditions, item, phase=POST): continue
        merge actions from rule.post_actions
        if IgnoreMap in actions: matched_ignore_map = true
        if RunMap in actions: matched_run = true
    else:
        # unidentified: chỉ StashUnid / pickup-pre. Không UniqueName, không WeightedSum.
        if rule.requires_post_mods: skip post actions except StashUnid if present on rule
        mark pickup_candidate = true

if matched_ignore_map:
    strip RunMap, Upgrade*, Augment*, Exalt*, Corrupt
```

`eval_expr`: AST từ parser. Fail closed (mục 8.3).

**Pickup trên sàn (Core):** item chưa ID. Core dùng **compiled pre-filter** (9.6). True → được nhặt. Quyết định stash/salvage sau khi ID ở town (Python).

### 9.6. Compiled pre-filter cho Core (120Hz)

Python serialize JSON nhỏ, Core đọc lúc start / opcode reload:

```json
{
  "schema": "autopoE2.pickup_filter.v1",
  "pickup_all_above_rarity": 1,
  "name_substrings": ["gold", "orb", "waystone", "tablet"],
  "exact_types": ["Exalted Orb"],
  "reject_name_substrings": []
}
```

P0: generate từ loot_controller **hiện tại** (giữ hành vi).  
P2: generate từ pre-`#` của IPD (rarity/type/category). Category trên sàn: suy từ tên (bảng base→category từ GGPK DB nếu có; thiếu thì chỉ Type/Rarity).

Core **cấm** parser IPD.

### 9.7. Category / WeaponCategory canonical

Equipment: `BodyArmour Gloves Boots Belt Helmet Ring Amulet`  
Weapons coarse: `Weapon 1Handed 2Handed OffHand`  
Other: `Flask Charm Jewel Waystone Tablet Gem`  
WeaponCategory: `Claw Dagger Wand OneHandSword OneHandAxe OneHandMace Sceptre Spear Flail Bow Staff TwoHandSword TwoHandAxe TwoHandMace Quarterstaff Crossbow Trap FishingRod Quiver Shield Focus`

So khớp exact, case-sensitive sau normalize (giữ PascalCase như trên).

### 9.8. Rule mẫu (minh họa — không phải profile production)

```
[Type] == "Exalted Orb" # [StashItem] == "true"
[Rarity] == "Rare" # [TotalResistances] > "50" && [StashItem] == "true"
[WaystoneTier] >= "1" && [WaystoneTier] <= "16" # [RunMap] == "true"
[Rarity] == "Unique" # [IgnoreMap] == "true"
[Category] == "Waystone" && [Rarity] == "Normal" # [UpgradeToRare] == "true"
[Category] == "Tablet" # [UseTablet] == "true"
```

Quirk WaystoneTier `<`: nếu implement comparison `<` trên tier, **cấm** silent wrap; document và test. Không copy workaround `"66"` của EB2 trừ khi tái hiện đúng bug parser của mình.

---

## 10. Map policy vs MapScenario

| Lớp | Trách nhiệm |
|---|---|
| `MapScenario` (đã có, opcode 95) | **Cách đi trong một instance**: boss rush / full clear / … |
| Maps IPD (P5) | **Viên waystone nào được chạy / craft / ignore mod** |
| `ignored_atlas_nodes` (P5, JSON list) | Tên node atlas bỏ qua |
| `map_exit_behavior` (P5) | Khi nào SwitchArea về town |

P0–P4: Maps IPD có thể chưa có; ExploreHandler dùng `MapScenario` như hiện tại.

Khi Maps IPD có: trước khi `SWITCH_AREA` vào map device, Python chọn waystone inventory theo evaluate; Core nhận `targetEntityId` hoặc slot.

`IgnoreMap` > mọi craft/run.

Craft chỉ áp dụng waystone **đã chọn chạy**, không alch cả stash:
- **Chính sách Waystone Auto-Crafting (P2-1)**: Tự động dùng `Orb of Alchemy` nâng cấp Waystone Normal/Magic thành Rare khi thỏa mãn `[UpgradeToRare] == "true"`. Tuân thủ tuyệt đối `INV-CRAFT-WAYSTONE-01` (cấm đập Alchemy vào Waystone Rare/Unique).
- **Chính sách Bù đắp Currency Ange Exchange (P2-2)**: Nếu hết `Orb of Alchemy` trong túi đồ, tự động tiếp cận NPC **Ange** tại Hideout để đổi `Exalted Orb` sang Alchemy. Tuân thủ tuyệt đối `INV-ANGE-EXCHANGE-01` (khóa sàn dự trữ an toàn tối thiểu 5 Exalt, định mức 1 Exalt/lần, ngắt mạch khẩn cấp nếu kẹt).
- Chi tiết FSM và cổng nghiệm thu: Xem [`43_current_status_and_next_development_basis.md`](./43_current_status_and_next_development_basis.md) Mục 5 (P2-1, P2-2).

---

## 11. Area event memory (P1)

`src/core/brain/area_events.hpp`

```cpp
enum class AreaEventStatus : uint8_t { Candidate, Committed, Completed, Failed };

struct AreaEventRecord {
    uint32_t entityId;
    EntityType type;      // 5–11
    float x, y, z;
    AreaEventStatus status;
    uint64_t lastSeenMs;
    uint32_t failCount;
};
```

Reset khi `areaSeed` đổi (zone transition). **Không** reset khi vào boss arena cùng seed nếu LogSensor không đổi area — theo doc 22 arena: cùng area thì giữ event phía ngoài.

Vòng đời:

1. Thấy entity type 5–11 walkable → Candidate.
2. Pathfinder tới được → Commit. Propose AREA_EVENT 6500.
3. Tương tác xong (essence chết / ritual xong / chest mở / shrine buff) → Complete.
4. Path fail N lần (`fail_max=3`) hoặc timeout 120s → Failed, blacklist instance.

INV-EVENT-01: ExploreHandler không Propose complete-and-leave (`SwitchArea` về town) khi còn Committed.

Ritual mua đồ: P5. P1 chỉ tới altar / không kẹt label (zoom visual tool đã có, không bắt buộc).

---

## 12. Inventory: opcode 5 và 6 (P3)

### 12.1. Hiện trạng

```cpp
// protocol.hpp
EXECUTE_STASH_ROUTINE = 5,
EXECUTE_VENDOR_ROUTINE = 6,
```

`DispatchCommand` không có `case` 5/6 → default bỏ qua.

Python `MacroOpCode` đã khai báo — Companion **được phép** gửi từ P3, trước đó cấm gửi giả thành công.

### 12.2. Hành vi đích

Cả hai opcode **bắt đầu FSM nhiều tick** trên Core (`InventoryHandler`), không hoàn thành trong 1 frame.

`CommandPacket` reuse:

- `targetEntityId`: 0 = auto NPC/stash gần nhất type 4/chest town.
- `priority`: bỏ qua (Core dùng bảng 3500/3400).
- `timeoutMs`: deadline FSM; hết hạn → Fail, Unstuck không spam.

Chuỗi Stash (mỗi bước một hoặc vài tick):

1. Mở inventory (phím I đọc từ game config, không hardcode).
2. Mở stash (click entity stash).
3. Chọn tab: index từ settings `stash_tabs` (1-based như người chơi đếm).
4. Click item → click ô stash. Delay `stash_delay_ms` + jitter (config, default 400±150, **không** 8 CPS cứng nếu settings khác).
5. Đóng panel (phím close-all từ game config).

Vendor/salvage/disenchant tương tự; NPC name từ settings, default Hooded One / vendor act hiện tại — **fail closed** nếu không thấy NPC, log error, không click lung tung.

Tọa độ ô: `data/ui/coordinates.toml` (tương tự EB2 `coordinates.ini` nhưng TOML dự án). Hiệu theo resolution từ `poe2_production_Config.ini` (Core đã đọc resolution loot). Công thức:

```
cell_x = origin_x + col * offset_x
cell_y = origin_y + row * offset_y
```

Calibration 1920×1080 là mặc định; scale theo width/height, **cấm** giả định 100% DPI khác 100 — log warning nếu OS scale ≠ 100.

### 12.3. Policy Python

Python quyết định item nào stash/salvage/sell (pickit post-ID). Gửi xuống:

- P3 tối thiểu: opcode 5/6 “chạy routine mặc định” (nhặt hết rare vào tab 1, bán magic) — **chỉ nếu** pickit compiled có.
- P3 đầy đủ: file `logs/inventory_plan.json` Core đọc (cold, không 120Hz) danh sách slot.

Không nhét plan vào CommandPacket 36 byte.

---

## 13. Bảng skill combat (P4) — không thay ReflexManager

File `data/combat/skills.toml`. Control Center editor sau.

Mỗi skill:

```toml
[[skill]]
key = "mouse_right"          # hoặc "Q","W","E","R","T"
kind = "attack"              # attack | combat_buff | persistent
priority = 1                 # nội bộ combo, không phải bảng BotBrain
cooldown_ms = 0
min_distance = 0
max_distance = 25
target_rarity_min = 0        # 0 all, 1 magic, 2 rare, 3 unique, 4 normal-only
close_targets = 0
close_targets_range = 10
maintain = false
min_life_pct = 0.0
max_life_pct = 2.0
min_mana_pct = 0.0
max_mana_pct = 2.0
min_es_pct = 0.0
max_es_pct = 2.0
```

`persistent`: nhấn theo cooldown khi Kind Explore/Combat (Spark).  
`combat_buff`: khi Combat, bỏ qua targeting quái.  
Flask/chicken **không** nằm file này — ReflexManager.

Kiting: giữ `ReflexManager` + `KitingEngine`. P4+ có thể thêm `movement_strategy = escape|wiggle|adaptive` trong cùng TOML; mặc định escape = hành vi hiện tại.

---

## 14. Vòng farm khép kín (P5) — chỉ sau P0–P3

Thứ tự state **toàn run** (không nhầm với BotActionKind):

```
TownIdle → Stash/Vendor nếu cần → OpenMapDevice → PlaceWaystone
→ (optional tablet) → EnterMap → InMap(BotBrain) → PortalHome → TownIdle
```

InMap dùng BotBrain mục 3–5.  
Hết waystone: dừng, log, **không** spam device.  
Party/humanization lịch: ngoài P5 trừ khi có spec riêng.

---

## 15. Anti-stuck (P4)

`UnstuckHandler` Propose 8000 khi:

1. `fail_movement_check_interval` (default 10000ms) XYZ không đổi trong khi LastChosen là Explore/Combat/Pickup.
2. Cùng `BotActionKind` + cùng `entityId` lặp `max_repetitive_actions` (default map 200, town 30).
3. Pickup fail N lần cùng id → blacklist id instance + (optional) một lần nhấn highlight key đọc từ game config.

Execute Unstuck: dừng click, path sang điểm walkable lệch 30–80 unit, hoặc SwitchArea town nếu lặp quá `max_recovery_attempts` (default 1) — recovery relog dùng `AutoLogin` đã có, không bịa TCP injector.

---

## 16. IPC và Python

### 16.1. Không đổi version nếu chỉ thêm EntityType 5–12

Cập nhật comment `protocol.hpp`, `core_shm_bridge.py` docstring, `06_binary_ipc_protocol_spec.md` bảng type.

### 16.2. DispatchCommand P3

```cpp
case MacroOpCode::EXECUTE_STASH_ROUTINE:
    inventoryHandler.StartStash(cmd, nowMs);
    break;
case MacroOpCode::EXECUTE_VENDOR_ROUTINE:
    inventoryHandler.StartVendor(cmd, nowMs);
    break;
```

Khi FSM inventory active: `Propose` Stash/Vendor 3500/3400 mỗi tick cho đến xong; `Execute` bước tiếp. BotBrain Process vẫn chạy — inventory thắng explore vì 3500 > 1000 và chỉ ở town (combat 7000 không có quái town).

### 16.3. Opcode mới (chỉ khi cần, cùng bump protocol)

Trì hoãn. P2 dùng file `bin/Release/pickup_filter.json` Core watch mtime 1s (đã có pattern watch config 1s trong `main.cpp`). Không thêm opcode nếu file watch đủ.

---

## 17. Sơ đồ file (tạo đúng path)

| Path | Phase | Nội dung |
|---|---|---|
| `src/core/brain/bot_brain.hpp/.cpp` | P0 | Arbiter |
| `src/core/brain/area_events.hpp/.cpp` | P1 | Event memory |
| `src/core/inventory/inventory_handler.hpp/.cpp` | P3 | Stash/vendor FSM |
| `src/assistant_tool/pickit/parser.py` | P2 | Lexer/parser IPD |
| `src/assistant_tool/pickit/evaluator.py` | P2 | Evaluate |
| `src/assistant_tool/pickit/compile_pickup_filter.py` | P2 | JSON cho Core |
| `data/pickit/default.ipd` | P2 | Rule **của AutoPOE2**, ngắn, không copy EB2 |
| `data/pickit/pickit.ini` | P2 | profile + disenchant_items |
| `data/maps/default.ipd` | P5 | map policy |
| `data/ui/coordinates.toml` | P3 | lưới inventory |
| `data/combat/skills.toml` | P4 | skill table |
| `tests/test_core.cpp` | P0+ | BotBrain tie-break, SAFE_RANGE |
| `tests/test_pickit_evaluator.py` | P2 | parser, IgnoreMap, WeightedSum, pre/post |

Refactor: `LootController::Update` → `Propose`+`Execute`; tương tự Combo/Quest. Giữ class, đổi chỗ gọi.

---

## 18. Pha triển khai và nghiệm thu

Không nhảy pha. Mỗi pha: test CLI thật + cập nhật dòng trạng thái file này từ `[SPEC]` mục pha sang `[IMPLEMENTED yyyy-mm-dd]` **sau khi** stdout test.

### P0 — Arbiter [IMPLEMENTED 2026-09-11]

**Đã hoàn thành và kiểm chứng:**
- `src/core/brain/bot_brain.hpp/.cpp`: Lớp `BotBrain` chọn 1 action priority cao nhất mỗi tick (120Hz), tie-break khoảng cách gần hơn rồi entityId nhỏ hơn. Ghi log chuẩn INV-BRAIN-05: `Running {Kind} with priority: {Name} ({n}) entity={id}`.
- `ReflexManager`: Thêm `ConsumedExclusiveInput()` đánh dấu khi dodge/flask/chicken/portal kích hoạt.
- `main.cpp`: Tích hợp `botBrain.Process`, interlock `!reflexMgr.ConsumedExclusiveInput()`, loại bỏ hoàn toàn chuỗi gọi 3 module song song (INV-BRAIN-01 & INV-BRAIN-02).
- `LootController`: Kế thừa `IActionHandler`. Khóa cứng `INV-SAFE-01` (quái trong `safe_range` <= 50 -> Propose = 0).
- `ComboManager`: Kế thừa `IActionHandler`. Đề xuất `EMERGENCY_ATTACK` (7500) hoặc `COMBAT` (7000).
- `QuestNavigator`: Kế thừa `IActionHandler`. Đề xuất `WAITING_FOR_LOOT` (5000), `WAITING_FOR_BOSS` (4800), `EXPLORE` (4000), `SWITCH_AREA` (3000).
- `CMakeLists.txt`: Đã thêm `src/core/brain/bot_brain.cpp` vào `CORE_LIB_SOURCES`.

**Bằng chứng kiểm thử CLI thực tế (11/09/2026):**
```text
[Test 67] BotBrain Priority Arbiter & Invariants (Doc 29 P0)...
[KMBox] Chế độ mô phỏng phần mềm đã BẬT (Dành cho máy chưa cắm thiết bị KMBox).
[IframeDodge #1] Kich hoat 300ms bat tu (Space) do: Test roll
[SmartFlask] Uong Life Flask [1] #1 (Test life)
  -> BotBrain Priority Arbiter & Invariants P0 OK
=================================================
[RESULT] TẤT CẢ 1479 KIỂM TRA ĐỀU ĐẠT (PASS)
```
Python Pytest Suite: 55/55 passed (100%).

### P1 — Entity types + AreaEvent [IMPLEMENTED 2026-09-11]

**Đã hoàn thành và kiểm chứng:**
- `src/core/memory/game_layout.hpp` & `src/core/common/protocol.hpp`:
  - Mở rộng `EntityType`: `Chest = 5`, `Shrine = 6`, `Essence = 7`, `Breach = 8`, `Ritual = 9`, `Strongbox = 10`, `Delirium = 11`, `MinimapPoi = 12`.
  - Mở rộng `extraFlags`: Bit 3 (`kEntityFlagInteractable = 0x08`), Bit 4 (`kEntityFlagHiddenNotWalkable = 0x10`), Bit 5 (`kEntityFlagMinimapDerived = 0x20`).
  - Bảo toàn tuyệt đối kích thước bố cục nhị phân `sizeof(EntityTelemetryData) == 88` byte (`INV-IPC-01`).
  - Hàm phân loại heuristic `ClassifyEntityType(name, currentType, maxHp, extraFlags)`: Giữ nguyên `EntityTypeMonster` nếu `currentType == 1` và `maxHP > 0` (không bao giờ reclassify quái vật sống); phân loại chính xác các đối tượng tương tác thế giới (`Essence`, `Shrine`, `Strongbox`, `Breach`, `Ritual`, `Delirium`, `Chest`, `Waypoint`).
- `src/core/memory/entity_manager.cpp`: Gọi `ClassifyEntityType` trong `ReadEntities()` trước khi nạp vào telemetry array.
- `src/core/brain/area_events.hpp` & `area_events.cpp`:
  - `AreaEventManager`: Quản lý vòng đời `Candidate` -> `Committed` -> `Completed` / `Failed` (max 3 fails hoặc 120s timeout). Tự động reset khi chuyển `areaSeed`.
  - `AreaEventHandler`: Kế thừa `IActionHandler`. Đề xuất `AREA_EVENT` (6500) khi event đã Committed; đề xuất `OPEN_CHEST` (5500) khi có rương trong `chest_range` (<= 60.0u) VÀ thỏa mãn `INV-SAFE-01` (không có quái vật sống trong `safe_range` <= 50.0u; nếu có quái sống thì priority = 0 nhường quyền cho Combat/Reflex).
- `src/core/navigation/quest_navigator.hpp` & `quest_navigator.cpp`:
  - Khóa cứng bất biến `INV-EVENT-01`: Không được đề xuất `SWITCH_AREA` (3000) hoặc mở portal thoát map khi `AreaEventManager` còn sự kiện `Committed`.
- `src/core/main.cpp`: Tích hợp `AreaEventManager` và `AreaEventHandler` vào `BotBrain`, liên kết cập nhật candidate entities ở chu kỳ 120Hz và reset khi chuyển instance.
- `CMakeLists.txt`: Đã thêm `src/core/brain/area_events.cpp` vào `CORE_LIB_SOURCES`.

**Bằng chứng kiểm thử CLI thực tế (11/09/2026):**
```text
[Test 68] Area Event Memory & Entity Taxonomy Invariants (Doc 29 P1)...
[KMBox] Chế độ mô phỏng phần mềm đã BẬT (Dành cho máy chưa cắm thiết bị KMBox).
Running Combat with priority: COMBAT (7000) entity=3
Running AreaEvent with priority: AREA_EVENT (6500) entity=4
[AutoLoot #1] Nhat: Divine Orb (Rarity: 0, Dist: 0, Latency: 15ms)
Running Pickup with priority: PICKUP (6000) entity=1
  -> Area Event Memory & Entity Taxonomy Invariants P1 OK
=================================================
[RESULT] TẤT CẢ 1528 KIỂM TRA ĐỀU ĐẠT (PASS)
```
Python Pytest Suite: 166/166 passed (100%).

### P2 — Pickit DSL cold path & Filter-Driven Looting [IMPLEMENTED 2026-09-11]

**Đã hoàn thành và kiểm chứng:**
- `src/assistant_tool/pickit/parser.py`: Trình phân tích cú pháp grammar Pickit DSL, trích xuất AST (`Clause`, `WeightedSumClause`, `BinaryOp`), bắt buộc dấu `#` phân tách pre-ID và post-ID, hỗ trợ comment `//` và vô hiệu hóa quy tắc `//[`, mở rộng `KNOWN_ACTIONS` (`Salvage`, `Disenchant`, `Sell`, `IgnoreRitual`, v.v.).
- `src/assistant_tool/pickit/evaluator.py`: Bộ đánh giá quy tắc vật phẩm:
  - Triệt để tuân thủ quy tắc Fail-Closed: thuộc tính số bị thiếu trong mệnh đề -> kết quả so sánh là `False`.
  - Khóa cứng bất biến `INV-PRE-01`: Cấm đánh giá `UniqueName` ở pha PRE (trên sàn, đồ chưa ID không thể biết Unique Name).
  - Khóa cứng bất biến `INV-MAP-01`: Mệnh đề `IgnoreMap` tước toàn bộ quyền `RunMap` và các action nâng cấp bản đồ (`UpgradeToRare`, `Augment`, `Exalt`, `Corrupt`).
- `data/pickit/pickit.ini` & `data/pickit/default.ipd`: Cấu hình danh mục luật chuẩn POE2 (Gold, Mirror, Divine, Exalt, Chaos, Waystone T1-T16, Tablets, Gems, Cores, Essences, Charms, Uniques, Rares).
- `src/assistant_tool/pickit/compile_pickup_filter.py`: Trình biên dịch offline sinh `bin/Release/pickup_filter.json` và `data/pickup_filter.json`.
- `src/core/looting/loot_controller.hpp` & `loot_controller.cpp`:
  - Thêm cấu trúc `PickupFilter` với parser JSON nội bộ siêu nhẹ không phụ thuộc thư viện ngoài.
  - Tự động kiểm tra và Hot-Reload cấu hình filter tại tần số 120Hz (chu kỳ kiểm tra mtime tệp mỗi 1000ms).
  - **Bất biến User**: Triệt để chỉ nhặt đồ theo Filter (`ShouldLoot`):
    1. Kiểm tra `reject_name_substrings`: Nếu khớp -> `ShouldLoot = false` và `GetPriority = 0` (đồ bị ẩn / cấm nhặt).
    2. Kiểm tra `exact_types`: Nếu khớp -> `ShouldLoot = true`.
    3. Kiểm tra `pickup_all_above_rarity`: Nếu `rarity >= pickup_all_above_rarity` -> `ShouldLoot = true`.
    4. Kiểm tra `name_substrings`: Nếu khớp -> `ShouldLoot = true`.
    5. **Khóa chặn đồ bị ẩn**: Nếu không thỏa mãn bất kỳ điều kiện nào của filter -> `ShouldLoot = false` và `Propose` priority = 0 (tuyệt đối không nhặt).
- `src/core/main.cpp`: Tự động nạp `pickup_filter.json` khi Core khởi động.

**Bằng chứng kiểm thử CLI thực tế (11/09/2026):**
```text
[Test 69] Pickit Filter & Hidden Item Rejection Invariants (Doc 29 P2)...
[LootController] Da nap PickupFilter tu 'bin/Release/pickup_filter.json': 19 exact types, 35 substrings, 8 rejects, minRarity=2
[LootController] Da nap PickupFilter tu 'bin/Release/tmp_test_filter.json': 1 exact types, 1 substrings, 1 rejects, minRarity=3
[LootController] Da nap PickupFilter tu 'bin/Release/tmp_test_filter.json': 2 exact types, 2 substrings, 1 rejects, minRarity=1
  -> Pickit Filter & Hidden Item Rejection Invariants P2 OK
=================================================
[RESULT] TẤT CẢ 1550 KIỂM TRA ĐỀU ĐẠT (PASS)
```
Python Pytest Suite:
```text
172 passed, 1 warning in 96.15s (0:01:36)
```

### P3 — Stash/Vendor Opcode & Inventory Multi-Step FSM [IMPLEMENTED 2026-09-11]

**Đã hoàn thành và kiểm chứng:**
- `data/ui/coordinates.toml`:
  - Cấu hình phân đoạn chuẩn TOML (sections: `[screen]`, `[inventory]`, `[stash]`, `[vendor]`, `[delays]`).
  - Hệ tọa độ tham chiếu chuẩn 1920x1080 (16:9), hỗ trợ co giãn tỷ lệ (scale tuyến tính) theo độ phân giải màn hình thực tế của client game (`1440p`, `4K`, v.v.).
  - Lưới túi đồ cá nhân nhân vật: 12 cột x 5 hàng (`invCols = 12`, `invRows = 5`, origin 1295, 585, cell 52x52).
  - Lưới hòm đồ cá nhân / Guild: 12 cột x 12 hàng (`stashCols = 12`, `stashRows = 12`, origin 180, 190, cell 52x52).
  - Thanh Tab hòm đồ (10 tabs, bước nhảy 70px), tọa độ nút Vendor Sell (`420, 140`), Accept (`560, 820`), Close (`1860, 60`).
  - Tham số độ trễ tự nhiên chống anti-cheat: `default_step_delay_ms = 350`, `item_transfer_delay_ms = 180`, `jitter_ms = 50`.
- `src/core/brain/inventory_handler.hpp` & `inventory_handler.cpp`:
  - Trình phân tích cú pháp TOML theo từng phân đoạn (section-aware zero-dependency parser), độc lập hoàn toàn với thư viện bên thứ ba.
  - Bộ máy trạng thái hữu hạn Stash FSM: `Idle` -> `OpenPanels` (phím I) -> `SelectTab` -> `DepositItems` (Ctrl + Click với quota `m_itemsToTransfer`) -> `ClosePanels` (phím Space) -> `Completed` / `Failed`.
  - Bộ máy trạng thái hữu hạn Vendor FSM: `Idle` -> `ApproachNpc` -> `OpenVendor` -> `SellOrSalvage` -> `ClosePanels` -> `Completed` / `Failed`.
  - **Bất biến sinh tồn `INV-TOWN-01`**: Routine Stash & Vendor **chỉ được phép chạy ở Town/Hideout an toàn**. Nếu phát hiện bất kỳ quái vật sống nào xung quanh (`type == 1 && maxHP > 0 && !(extraFlags & 4)`), FSM lập tức ngắt khẩn cấp và chuyển sang `Failed` (Fail-Closed), `Propose` trả về priority = 0 để nhường toàn bộ quyền điều khiển cho Combat/Reflex.
  - Cơ chế kiểm soát Timeout: Vượt quá `timeoutMs` (mặc định 15000ms) -> FSM chuyển sang `Failed`.
- `src/core/navigation/quest_navigator.cpp`:
  - Đồng bộ thứ bậc ưu tiên BotBrain trong Town: Tại Town/Hideout, `QuestNavigator::Propose` hạ mức ưu tiên xuống `bot_priority::IDLE` (1000) để đảm bảo `STASH` (3500) và `VENDOR` (3400) luôn chiến thắng và thực thi trọn vẹn chu trình chuyển đồ trước khi khám phá.
- `src/core/main.cpp`:
  - Khởi tạo và đăng ký `invHandler` vào `botBrain`.
  - Dispatch opcode trong `DispatchCommand`:
    - `MacroOpCode::EXECUTE_STASH_ROUTINE` (5): Kích hoạt `invHandler.StartStash(cmd, nowMs)`.
    - `MacroOpCode::EXECUTE_VENDOR_ROUTINE` (6): Kích hoạt `invHandler.StartVendor(cmd, nowMs)`.
- `CMakeLists.txt`: Đã thêm `src/core/brain/inventory_handler.cpp` vào danh sách biên dịch `CORE_LIB_SOURCES`.

**Bằng chứng kiểm thử CLI thực tế (11/09/2026):**
```text
[Test 70] InventoryHandler FSM & Town-Only Invariants (Doc 29 P3)...
[InventoryHandler] Da nap coordinates tu 'data/ui/coordinates.toml' (Ref: 1920x1080)
[KMBox] Chế độ mô phỏng phần mềm đã BẬT (Dành cho máy chưa cắm thiết bị KMBox).
[InventoryHandler] Start STASH routine (Cmd #501, Tab=1, Timeout=10000ms)
[InventoryHandler] >> Quy trình Stash hoàn tất (2 items đã chuyển) <<
[InventoryHandler] Start VENDOR routine (Cmd #601, Timeout=8000ms)
[InventoryHandler] >> Quy trình Vendor hoàn tất <<
[InventoryHandler] Start STASH routine (Cmd #501, Tab=1, Timeout=10000ms)
[InventoryHandler] Start STASH routine (Cmd #501, Tab=1, Timeout=10000ms)
[InventoryHandler] Start STASH routine (Cmd #501, Tab=1, Timeout=10000ms)
Running Stash with priority: STASH (3500) entity=1
  -> InventoryHandler FSM & Town-Only Invariants P3 OK
=================================================
[RESULT] TẤT CẢ 1607 KIỂM TRA ĐỀU ĐẠT (PASS)
```
Python Pytest Suite:
```text
172 passed, 1 warning in 93.65s (0:01:33)
```

### P4 — Skills.toml + Unstuck [IMPLEMENTED 2026-09-11]

**Đã hoàn thành và kiểm chứng:**
- `data/combat/skills.toml`:
  - Cấu hình bảng kỹ năng điều khiển dữ liệu (Table-Driven Combat Rotation) hỗ trợ nhiều kỹ năng động: `Charged Staff` (buff chuột phải 40s), `Whirling Assault` (chuột trái AOE spam), `Rage Builder` (phím Q combo generator), `Stagger Finisher` (phím R combo finisher khi đạt 4-5 combo points).
  - Khai báo chi tiết: `key`, `cooldown_ms`, `min_charges`, `min_combo`, `min_monsters`, `max_distance`, `buff_duration_ms`, `is_buff`.
- `src/core/combat/skills_table.hpp` & `skills_table.cpp`:
  - Trình phân tích cú pháp TOML không phụ thuộc thư viện ngoài (zero-dependency parser) hỗ trợ array-of-tables `[[skills]]`.
  - Ánh xạ trực tiếp ký hiệu phím (`RBUTTON`, `LBUTTON`, `Q`, `W`, `E`, `R`, `T`, `1`-`5`, `SPACE`) sang mã phím ảo Windows (`VK_RBUTTON`, `VK_LBUTTON`, `0x51`, v.v.).
  - Phương thức `SelectBestSkill`: Đánh giá điều kiện thời gian thực từ `PlayerTelemetryData` và `EntityTelemetryData` (khoảng cách mục tiêu, số lượng quái vật xung quanh, điểm combo hiện tại, thời gian cooldown, thời gian tồn tại buff).
- `src/core/combat/combo_manager.hpp` & `combo_manager.cpp`:
  - Tích hợp `SkillsTable` vào chu trình chiến đấu `ComboManager::Update` và `Propose`.
  - Tự động nạp cấu hình `data/combat/skills.toml` khi khởi tạo.
  - Tương thích 100% với layout nhị phân IPC `PlayerTelemetryData` (`currentES`, `maxES`, `sizeof(EntityTelemetryData) == 88`).
- `src/core/brain/unstuck_handler.hpp` & `unstuck_handler.cpp`:
  - Triển khai interface `IActionHandler` với mức ưu tiên chống kẹt cao nhất **`bot_priority::UNSTUCK = 8000`** (vượt trên Combat 7000, Area Event 6500, Pickup 6000, Explore 4000).
  - 3 cơ chế phát hiện kẹt tự động:
    1. `MovementStuck`: Vị trí XYZ của nhân vật không đổi (độ lệch $< 3.0$ unit) trong suốt $10,000$ms (`failMovementCheckIntervalMs`) khi đang thực thi các hành động di chuyển (`Explore`, `Combat`, `Pickup`).
    2. `RepetitiveAction`: Cùng loại hành động (`BotActionKind`) và cùng mục tiêu (`entityId`) lặp lại liên tiếp quá giới hạn cho phép ($200$ lần trong Map, $30$ lần trong Town/Hideout).
    3. `PickupFailure`: Một vật phẩm bị nhặt thất bại liên tiếp $5$ lần -> tự động đưa vào Blacklist không nhặt lại (`m_blacklistedItemIds`) và kích hoạt cờ gỡ kẹt.
  - Cơ chế tự động giải phóng kẹt: Khi nhân vật đã di chuyển ra xa vị trí kẹt ban đầu $\ge 15$ unit -> `UnstuckHandler` tự động reset và nhường lại quyền điều khiển.
  - Quy trình gỡ kẹt 3 bước thích ứng (`Execute`):
    - Lần 1: Di chuyển theo hướng vuông góc lệch $50$ unit.
    - Lần 2: Kích hoạt cú lướt né Iframe Dodge Roll (phím Space) để phá vỡ kẹt hình học địa hình.
    - Lần 3: Đổi góc di chuyển xuyên tâm tiếp theo.
    - Hết số lần thử (`maxRecoveryAttempts = 3`): Nhấn phím 'T' mở khẩn cấp Town Portal thoát hiểm về Town/Hideout an toàn.
- `src/core/main.cpp`:
  - Khởi tạo `UnstuckHandler`, nạp cấu hình `data/combat/skills.toml` và đăng ký vào `botBrain`.
  - Ghi nhận hành động `unstuckHandler.RecordAction(chosen.kind, chosen.targetEntityId, nowMs, isTown)` trong vòng lặp chính.
- `CMakeLists.txt`: Đã thêm `src/core/brain/unstuck_handler.cpp` và `src/core/combat/skills_table.cpp` vào `CORE_LIB_SOURCES`.

**Bằng chứng kiểm thử CLI thực tế (11/09/2026):**
```text
[Test 71] SkillsToml & UnstuckHandler Invariants (Doc 29 P4)...
[SkillsTable] Da nap thanh cong 4 skills tu 'data/combat/skills.toml'
[SkillsTable] Da nap thanh cong 4 skills tu 'data/combat/skills.toml'
[KMBox] Chế độ mô phỏng phần mềm đã BẬT (Dành cho máy chưa cắm thiết bị KMBox).
Running Unstuck with priority: UNSTUCK (8000) entity=0
  -> SkillsToml & UnstuckHandler Invariants P4 OK
=================================================
[RESULT] TẤT CẢ 1669 KIỂM TRA ĐỀU ĐẠT (PASS)
```
Python Pytest Suite:
```text
172 passed, 1 warning in 96.87s (0:01:36)
```

### P5 — Maps IPD + Map Device FSM + Ignored Atlas Nodes & Closed Farming Loop [IMPLEMENTED 2026-09-11]

**Đã hoàn thành và kiểm chứng:**
- `data/maps/default.ipd`:
  - Chính sách phân tích Waystone và Precursor Tablet độc lập theo chuẩn ngữ pháp IPD của AutoPOE2.
  - Phân loại cấp bậc bản đồ:
    - Thẻ trắng/vàng thấp (Tier 1-13): `[UpgradeToRare]` và `[RunMap]`.
    - Thẻ Endgame (Tier 14-16): Yêu cầu chất lượng cao, kích hoạt Tablet `[UseTablet]` và `[RunMap]`.
    - Precursor Tablet: Chỉ dùng tablet có `[Tier] >= 14` và số lượng `RemainingUses >= 1`.
  - Cơ chế loại bỏ bản đồ sát thủ (Lethal Map Mod Rejection):
    - Khóa chặn các thuộc tính chết người đối với build (ví dụ: `Cannot Leech Life or Mana`, `Reflect Elemental Damage`, `Monsters reflect 18% of Elemental Damage`).
    - Nếu phát hiện mod sát thủ -> gán cờ `[IgnoreMap]` và tự động tước bỏ cờ `[RunMap]` (không chạy map này, bảo toàn an toàn tuyệt đối cho nhân vật).
- `data/maps/ignored_nodes.json`:
  - Danh sách bản đồ/Atlas node bị bỏ qua do bố cục địa hình bất lợi hoặc nguy cơ kẹt cao (ví dụ: `Vaal Factory`, `Flooded Complex`, `Maze of the Minotaur`).
- `data/ui/coordinates.toml`:
  - Bổ sung cấu hình phân đoạn `[map_device]` với hệ tọa độ tham chiếu chuẩn 1920x1080:
    - Khe cắm Waystone (`waystone_slot = [625, 420]`).
    - Khe cắm Precursor Tablet (`tablet_slot = [625, 485]`).
    - Nút kích hoạt bản đồ (`activate_button = [625, 600]`).
    - Nút đóng giao diện Map Device (`close_button = [820, 240]`).
- `src/core/common/protocol.hpp` & `src/assistant_tool/core_shm_bridge.py`:
  - Bổ sung mã lệnh macro mới: `EXECUTE_MAP_DEVICE_ROUTINE = 7` trong enum `MacroOpCode`.
  - Đồng bộ hóa 100% giữa tầng C++ Core và Python Companion qua Shared Memory.
- `src/core/brain/map_device_handler.hpp` & `map_device_handler.cpp`:
  - Triển khai interface `IActionHandler` với mức ưu tiên điều phối **`bot_priority::MAP_DEVICE = 3200`** (`BotActionKind::SwitchArea`).
  - Thứ bậc ưu tiên hoàn hảo trong Town:
    - Town Idle (`1000`) < `MAP_DEVICE` (`3200`) < `NPC_INTERACT` (`3300`) < `VENDOR` (`3400`) < `STASH` (`3500`).
    - Đảm bảo trật tự khép kín: Về Town/Hideout -> Giám định NPC The Hooded One -> Dọn hòm Stash / Bán rác Vendor hoàn tất -> Mới tiến hành nạp Waystone mở cổng bản đồ mới.
  - Máy trạng thái hữu hạn 7 bước (7-Step Map Device FSM):
    1. `ApproachDevice`: Di chuyển tiếp cận Map Device trong Hideout.
    2. `OpenDevice`: Click mở giao diện Map Device.
    3. `PlaceWaystone`: Bốc Waystone hợp lệ từ hành trang cắm vào khe `waystone_slot` (hỗ trợ tự động quét toàn bộ hàng đầu tiên gồm 12 ô xoay vòng và tịnh tiến con trỏ).
    4. `PlaceTablet`: (Tùy chọn) Bốc Precursor Tablet cắm vào khe `tablet_slot`.
    5. `Activate`: Nhấn nút `activate_button` kích hoạt Map Device mở 6 cổng không gian (Portals).
    6. `WaitForPortals`: Chờ hiệu ứng xuất hiện cổng (độ trễ tự nhiên chống anti-cheat).
    7. `EnterPortal`: Click vào cổng để dịch chuyển tức thời vào bản đồ mới.
  - **Khóa chặn bất biến an toàn sinh tồn**:
    - `INV-TOWN-01`: Map Device FSM **chỉ được phép kích hoạt khi an toàn** (không có quái vật sống quanh vị trí). Nếu quái vật xuất hiện -> FSM lập tức hủy khẩn cấp và nhường quyền cho Combat.
    - `INV-WAYSTONE-01`: Cơ chế Timeout (mặc định 15000ms) bảo đảm FSM tự động reset và không bao giờ gây kẹt vòng lặp nếu thao tác UI thất bại.
- `src/core/main.cpp`:
  - Đăng ký `mapDeviceHandler` vào `BotBrain` với priority 3200.
  - Phân phối lệnh trong `DispatchCommand` với `MacroOpCode::EXECUTE_MAP_DEVICE_ROUTINE` (7).
- `CMakeLists.txt`: Đã thêm `src/core/brain/map_device_handler.cpp` vào danh sách biên dịch `CORE_LIB_SOURCES`.
- `src/assistant_tool/pickit/parser.py` & `evaluator.py`:
  - Hỗ trợ cú pháp IPD mở rộng:
    - Nhận diện các hành động trong ngoặc vuông rút gọn: `[IgnoreMap]`, `[UseTablet]`, `[UpgradeToRare]`.
    - Nhận diện cú pháp nhiều hành động ngăn cách bởi dấu phẩy: `[UpgradeToRare], [RunMap]`.
    - Trình thẩm định `evaluator.py` hỗ trợ duyệt kiểm tra cấu trúc mảng mod (`item.get("mods")`) và dictionary, tự động tước bỏ `RunMap` khi dính mod nằm trong danh sách `IgnoreMap`.
- Khép kín toàn diện vòng lặp Closed Farming Loop:
  - **Vòng lặp**: `Map Run` (Combat, Pickup, Boss Rush) -> `Hạ gục Boss / Full Clear` -> `Mở Town Portal (phím T)` -> `Về Hideout` -> `Stash FSM (Opcode 5)` -> `Vendor FSM (Opcode 6)` -> `Map Device FSM (Opcode 7)` -> `Vào Map mới`.

**Bằng chứng kiểm thử CLI thực tế (11/09/2026):**
```text
[Test 72] MapDeviceHandler & Closed Loop Invariants (Doc 29 P5)...
Running SwitchArea with priority: MAP_DEVICE (3200) entity=0
  -> MapDeviceHandler & Closed Loop Invariants P5 OK
=================================================
[RESULT] TẤT CẢ 1713 KIỂM TRA ĐỀU ĐẠT (PASS)
```
Python Pytest Suite:
```text
tests/test_pickit_evaluator.py::test_parser_basic_and_comments PASSED    [ 14%]
tests/test_pickit_evaluator.py::test_parser_syntax_error_missing_hash PASSED [ 28%]
tests/test_pickit_evaluator.py::test_weighted_sum_evaluator PASSED       [ 42%]
tests/test_pickit_evaluator.py::test_uniquename_rejected_in_pre_phase PASSED [ 57%]
tests/test_pickit_evaluator.py::test_ignore_map_strips_run_map PASSED    [ 71%]
tests/test_pickit_evaluator.py::test_fail_closed_on_missing_stat PASSED  [ 85%]
tests/test_pickit_evaluator.py::test_maps_default_ipd_policy PASSED      [100%]
============================== 7 passed in 0.07s ==============================
Full Suite: 173 passed, 1 warning in 86.75s (0:01:26)
```

---

## 19. Log và quan sát

Format bắt buộc:

```
Running Combat with priority: COMBAT (7000) entity=1234
Running Pickup with priority: PICKUP (6000) entity=88
```

`LastChosen` expose overlay/Control Center (P0 có thể chỉ stdout + `CoreLog`).

Cấm log mỗi tick khi Idle (spam). Log khi kind đổi hoặc mỗi 1s nếu kind ≠ Idle.

---

## 20. Checklist tự đối kháng trước khi agent báo xong

- [ ] Không còn `comboMgr.Update` + `lootCtrl.Update` + `questNav.Update` cạnh nhau trong full-auto.
- [ ] `sizeof(EntityTelemetryData)==88` vẫn PASS trừ khi đã bump protocol có chủ đích.
- [ ] Opcode 5/6 không im lặng nếu đang trong P3+.
- [ ] Không copy `.ipd` ExiledBot.
- [ ] Có test mới FAIL trên code cũ (reproduction) rồi PASS sau sửa.
- [ ] Số liệu test là stdout lần chạy **phiên này**.
- [ ] Doc 29 mục pha đã đánh dấu IMPLEMENTED đúng pha, không đánh cả P5.

---

## 21. Tham chiếu chéo

- IPC layout: `docs/development/06_binary_ipc_protocol_spec.md`, `src/core/common/protocol.hpp`
- Interlock focus: `docs/development/11_foreground_hardware_interlock_and_desktop_safety_protocol.md`
- In-map scenario (không phải atlas loop): `docs/development/25_spec_endgame_atlas_map_scenarios_and_boss_rush_architecture.md`
- Control Center: `docs/development/27_control_center_companion_contract_and_ui.md`
- Pointer / EntityMap: `docs/development/23_pointer_chain_resolver_architecture.md`
- Agent 2-tier: `docs/agent/architecture.md`
- Loot hiện tại: `src/core/looting/loot_controller.cpp` (`GetItemPriority` 10–100 = thứ tự **trong** PICKUP, không phải bảng BotBrain)
- Tick hiện tại: `src/core/main.cpp` khoảng 1151–1165
- Opcode chết: `main.cpp` `DispatchCommand` default ~257

Nghiên cứu đối thủ (không phải spec implement): thư mục `ExiledBot 2 Beta v0.10.1/` (Readme, `config.ini`, `skills.ini`, `Pickit/default.ipd` grammar, WebUI). Kiến trúc họ: `BotBrain::ProcessStates` + handlers (Fight, ItemPicker, MapExplorer, Inventories, AreaEvents, Unstucker, Flask, Ritual, RecastDetour). AutoPOE2 học **arbiter + DSL + event memory**, không học injection.
