跟随版本 0.8.19
This commit is contained in:
212
docs/LootDisplay-技术要点.md
Normal file
212
docs/LootDisplay-技术要点.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# LootDisplay 拾取窗口接管 — 技术要点
|
||||
|
||||
## 最终成功方案
|
||||
|
||||
**核心原则:不替换原生按钮的交互逻辑,只替换视觉层,重新定位原生按钮。**
|
||||
|
||||
在 Turtle WoW (1.12 魔兽私服) 中,`LootSlot()` 是一个**受保护的 C 端函数**,
|
||||
只接受来自原生 `LootButton1~4`(由 FrameXML 中 `LootButtonTemplate` 创建的按钮)
|
||||
的内置 `OnClick` 处理器调用。任何 addon 自建按钮(无论是否使用模板)都**无法**成功
|
||||
调用 `LootSlot()`。
|
||||
|
||||
---
|
||||
|
||||
## 失败方案及原因
|
||||
|
||||
### 方案 1:LootButtonTemplate 自定义按钮
|
||||
|
||||
```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 计算错误 |
|
||||
Reference in New Issue
Block a user