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

7.5 KiB
Raw Blame History

LootDisplay 拾取窗口接管 — 技术要点

最终成功方案

核心原则:不替换原生按钮的交互逻辑,只替换视觉层,重新定位原生按钮。

在 Turtle WoW (1.12 魔兽私服) 中,LootSlot() 是一个受保护的 C 端函数 只接受来自原生 LootButton1~4(由 FrameXML 中 LootButtonTemplate 创建的按钮) 的内置 OnClick 处理器调用。任何 addon 自建按钮(无论是否使用模板)都无法成功 调用 LootSlot()


失败方案及原因

方案 1LootButtonTemplate 自定义按钮

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~4LootFrame 的子框架,父框架隐藏则子框架不可交互

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 计算错误