Files
Nanami-UI/docs/LootDisplay-技术要点.md
2026-03-24 15:56:28 +08:00

213 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LootDisplay 拾取窗口接管 — 技术要点
## 最终成功方案
**核心原则:不替换原生按钮的交互逻辑,只替换视觉层,重新定位原生按钮。**
在 Turtle WoW (1.12 魔兽私服) 中,`LootSlot()` 是一个**受保护的 C 端函数**
只接受来自原生 `LootButton1~4`(由 FrameXML 中 `LootButtonTemplate` 创建的按钮)
的内置 `OnClick` 处理器调用。任何 addon 自建按钮(无论是否使用模板)都**无法**成功
调用 `LootSlot()`
---
## 失败方案及原因
### 方案 1LootButtonTemplate 自定义按钮
```lua
local btn = CreateFrame("Button", "NanamiLootBtn1", lootFrame, "LootButtonTemplate")
btn:SetScript("OnClick", function()
LootSlot(this.slot)
end)
```
**失败原因**:虽然使用了 `LootButtonTemplate`,但按钮是 addon 动态创建的,
不是 FrameXML 在加载期创建的原生 `LootButton1~4`。Turtle WoW 的 C 端可能检查
调用来源是否为受信任的原生按钮,导致 `LootSlot()` 静默失败。
### 方案 2纯自定义 Button + 直接调用 LootSlot
```lua
local btn = CreateFrame("Button", nil, lootFrame)
btn:SetScript("OnClick", function()
LootSlot(this.slot)
end)
```
**失败原因**:与方案 1 相同。`LootSlot()` 只信任原生按钮的事件上下文。
截图可验证 `GameTooltip:SetLootItem()` 正常工作tooltip 能显示),说明
拾取会话本身是活跃的,纯粹是 `LootSlot()` 拒绝执行。
### 方案 3完全禁用 LootFrame + 自定义按钮
```lua
LootFrame:UnregisterAllEvents()
LootFrame:Hide()
```
**失败原因**:在禁用 LootFrame 的基础上使用自定义按钮调 `LootSlot()`
同样因为 C 端保护而失败。另外隐藏 LootFrame 后 C 端可能也认为拾取会话无效。
---
## 成功方案:原生按钮重定位
### 架构概览
```
┌─ NanamiLootFrame (自定义视觉框架) ──────┐
│ ┌─ 视觉行 row1 (EnableMouse=false) ──┐ │
│ │ icon + name + quality bar │ │ ← 玩家看到的
│ │ ┌─ LootButton1 (alpha=0) ───────┐ │ │
│ │ │ 原生 OnClick → LootSlot() │ │ │ ← 玩家点击的
│ │ └───────────────────────────────┘ │ │
│ └────────────────────────────────────┘ │
│ ┌─ 视觉行 row2 ... ──┐ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
┌─ LootFrame (原生, alpha=0, 不可交互) ───┐
│ (存在但不可见,维持拾取会话) │
└─────────────────────────────────────────┘
```
### 关键步骤
#### 1. 保存原始 `LootFrame_Update` 引用
```lua
origLootFrameUpdate = LootFrame_Update
```
**不能**将 `LootFrame_Update` 替换为空函数。这个函数负责为 `LootButton1~4`
设置 `SetID()`、slot 数据、以及关键的内置 `OnClick` 处理器。
#### 2. 让原生 LootFrame 保持"活跃但不可见"
```lua
-- 清除 OnHide 防止 CloseLoot() 被意外调用
LootFrame:SetScript("OnHide", function() end)
-- Show hook每次 Show 后强制 alpha=0
local origShow = LootFrame.Show
LootFrame.Show = function(self)
origShow(self)
self:SetAlpha(0)
self:EnableMouse(false)
end
-- Hide hook我们的框架显示期间阻止隐藏
local origHide = LootFrame.Hide
LootFrame.Hide = function(self)
if lootFrame and lootFrame:IsShown() then return end
origHide(self)
end
```
**为什么不能 Hide/UnregisterAllEvents**
- `LootFrame:Hide()` 的 XML OnHide 会调用 `CloseLoot()`,立即终止拾取会话
- C 端可能检查 `LootFrame:IsShown()` 来判断拾取是否合法
- 原生 `LootButton1~4``LootFrame` 的子框架,父框架隐藏则子框架不可交互
#### 3. ShowLootPage 的双阶段流程
**阶段 A — 构建视觉层**:设置自定义行的图标、名称、品质颜色。
视觉行 `EnableMouse(false)`,不拦截任何鼠标事件。
**阶段 B — 设置原生按钮并重定位**
```lua
-- 同步页码
LootFrame.page = page
if not LootFrame:IsShown() then LootFrame:Show() end
-- 让原生代码完整设置按钮状态ID、OnClick 等)
origLootFrameUpdate()
-- 将原生按钮移到我们的视觉行上
for i = 1, 4 do
local nb = _G["LootButton" .. i]
local row = lootRows[i]
if nb and row and row:IsShown() and row._qualColor then
nb:ClearAllPoints()
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
nb:SetFrameStrata("FULLSCREEN_DIALOG")
nb:SetFrameLevel(row:GetFrameLevel() + 10)
nb:SetAlpha(0) -- 不可见
nb:EnableMouse(true) -- 可点击
nb:Show()
end
end
```
#### 4. Hook 全局 `LootFrame_Update` 保持一致性
```lua
LootFrame_Update = function()
origLootFrameUpdate() -- 原生设置
-- 如果我们的框架在显示,重定位按钮
if lootFrame and lootFrame:IsShown() then
for i = 1, 4 do
-- 同样的重定位逻辑
end
end
end
```
这确保**任何来源**的 `LootFrame_Update` 调用(包括 `LOOT_SLOT_CLEARED` 事件后
引擎的自动调用)都会以按钮在正确位置结束,解决了"拾取一个物品后无法继续拾取"的问题。
---
## 事件流程梳理
### 打开拾取
```
玩家右键尸体
→ C 引擎创建拾取会话
→ LOOT_OPENED 事件
→ 原生 LootFrame_OnEvent → LootFrame:Show() [hook: alpha=0]
→ LootFrame_Update [hook: 原生设置 + 重定位]
→ 我们的 LOOT_OPENED handler → UpdateLootFrame → ShowLootPage
→ 设置视觉行
→ origLootFrameUpdate() → 重定位按钮
```
### 拾取物品
```
玩家点击 LootButton1 (alpha=0, 覆盖在视觉行上)
→ 原生 LootButton_OnClick → LootSlot(this:GetID()) ← 受信任的调用
→ 物品拾取成功
→ LOOT_SLOT_CLEARED 事件
→ 原生 handler → LootFrame_Update [hook: 原生重设按钮 + 重定位]
→ 我们的 handler → UpdateLootFrame → ShowLootPage → 刷新视觉 + 重定位
```
### 关闭拾取
```
玩家走开 / 按 ESC / 点关闭按钮
→ CloseLoot() 或 LOOT_CLOSED 事件
→ CloseLootFrame() → 隐藏自定义框架 (设 _closingLoot 标志)
→ 隐藏原生按钮
→ 允许 LootFrame:Hide()
```
---
## 关键技术教训
| 教训 | 说明 |
|------|------|
| `LootSlot()` 是受保护的 | Turtle WoW 中只有原生按钮的内置 OnClick 能成功调用 |
| 不能隐藏 LootFrame | OnHide (XML) 会调用 `CloseLoot()` 终止会话 |
| 不能禁用 LootFrame_Update | 这个函数负责设置按钮的 ID 和交互能力 |
| 视觉与交互分离 | 自定义行负责视觉 (EnableMouse=false),原生按钮负责交互 (alpha=0) |
| Hook 先调原始再改位置 | `LootFrame_Update` hook 先跑原生逻辑,再重定位按钮到自定义行上 |
| OnHide 需要防重入 | `_closingLoot` 标志防止 OnHide → CloseLoot → LOOT_CLOSED → CloseLootFrame 循环 |
| 页码必须同步 | `LootFrame.page` 必须与自定义分页同步,否则原生按钮 ID 计算错误 |