7.5 KiB
LootDisplay 拾取窗口接管 — 技术要点
最终成功方案
核心原则:不替换原生按钮的交互逻辑,只替换视觉层,重新定位原生按钮。
在 Turtle WoW (1.12 魔兽私服) 中,LootSlot() 是一个受保护的 C 端函数,
只接受来自原生 LootButton1~4(由 FrameXML 中 LootButtonTemplate 创建的按钮)
的内置 OnClick 处理器调用。任何 addon 自建按钮(无论是否使用模板)都无法成功
调用 LootSlot()。
失败方案及原因
方案 1:LootButtonTemplate 自定义按钮
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
local btn = CreateFrame("Button", nil, lootFrame)
btn:SetScript("OnClick", function()
LootSlot(this.slot)
end)
失败原因:与方案 1 相同。LootSlot() 只信任原生按钮的事件上下文。
截图可验证 GameTooltip:SetLootItem() 正常工作(tooltip 能显示),说明
拾取会话本身是活跃的,纯粹是 LootSlot() 拒绝执行。
方案 3:完全禁用 LootFrame + 自定义按钮
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 引用
origLootFrameUpdate = LootFrame_Update
不能将 LootFrame_Update 替换为空函数。这个函数负责为 LootButton1~4
设置 SetID()、slot 数据、以及关键的内置 OnClick 处理器。
2. 让原生 LootFrame 保持"活跃但不可见"
-- 清除 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 — 设置原生按钮并重定位:
-- 同步页码
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 保持一致性
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 计算错误 |