From 0151cfabffeedce182587af7f8841b91f588d724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andreas=20gr=C3=B8terud?= Date: Sun, 12 Jul 2026 19:28:54 +0200 Subject: [PATCH 1/5] feat: Store all session responses in the loot history When an item is awarded, everyone's response to the session is now collected from the voting frame and stored on the history entry as `sessionResponses`. Only actual (numeric) responses are included - passes and status texts are skipped, and the winner only contributes ilvl and roll since the rest of their data is already on the entry. The Loot History gains a dice button on entries with session responses, opening a popup showing each candidate's response, note, ilvl, roll and votes, with the winner marked and sorted first. Co-Authored-By: Claude Fable 5 --- Locale/enUS.lua | 1 + Modules/History/lootHistory.lua | 111 ++++++++++++++++++++++++++++++++ ml_core.lua | 32 +++++++++ 3 files changed, 144 insertions(+) diff --git a/Locale/enUS.lua b/Locale/enUS.lua index d2af3a0c..ec023f67 100644 --- a/Locale/enUS.lua +++ b/Locale/enUS.lua @@ -541,6 +541,7 @@ L.chat_command_start_error_onlyUseInRaids = "Cannot start: you're in a party and L.chat_restrictions_enabled = "Not currently possible due to Addon Restrictions." L.history_export_sheets_tip = "Tab delimited export for Google Sheets and English version of Excel that uses ';' as formula delimiter." L.history_export_excel_international_tip = "Tab delimited export for international version of Excel that uses ',' as formula delimiter." +L.history_sessionResponses_tip = "Click to show everyone's response to this item." L.response_NOTELIGIBLE = "Not eligible for this item" L["opt_addButton_desc"] = "Add a new button group for the selected slot." L.opt_announceAward_WHISPER_WINNER = "/w winner" diff --git a/Modules/History/lootHistory.lua b/Modules/History/lootHistory.lua index 058aa2f8..bd89c077 100644 --- a/Modules/History/lootHistory.lua +++ b/Modules/History/lootHistory.lua @@ -62,6 +62,7 @@ function LootHistory:OnInitialize() {name = L["Item"], width = 250, comparesort = self.ItemSort, defaultsort = 1, sortnext = 2}, -- Item string {name = L["Reason"], width = 220, comparesort = self.ResponseSort, defaultsort = 1, sortnext = 2}, -- Response aka the text supplied to lootDB...response { name = L["Notes"], width = 40, }, + {name = "", width = ROW_HEIGHT}, -- Session responses button {name = "", width = ROW_HEIGHT}, -- Delete button } filterMenu = _G.MSA_DropDownMenu_Create("RCLootCouncil_LootHistory_FilterMenu", UIParent) @@ -107,6 +108,7 @@ function LootHistory:Hide() self.frame:Hide() self.moreInfo:Hide() moreInfo = false + if self.sessionResponsesFrame then self.sessionResponsesFrame:Hide() end end function LootHistory:SubscribeToPermanentComms () @@ -226,6 +228,7 @@ function LootHistory:BuildData() {value = i.lootWon}, {DoCellUpdate = self.SetCellResponse, args = {color = i.color, response = i.response, responseID = i.responseID or 0, isAwardReason = i.isAwardReason}}, { DoCellUpdate = self.SetCellNote }, + {DoCellUpdate = self.SetCellSessionResponses}, {DoCellUpdate = self.SetCellDelete}, } } @@ -486,6 +489,114 @@ function LootHistory.SetCellNote(rowFrame, frame, data, cols, row, realrow, colu frame.noteBtn = f end +function LootHistory.SetCellSessionResponses(rowFrame, frame, data, cols, row, realrow, column, fShow, table, ...) + if not data then return end + local row = data[realrow] + local entry = lootDB[row.name] and lootDB[row.name][row.num] + local f = frame.sessionBtn or CreateFrame("Button", nil, frame) + f:SetSize(ROW_HEIGHT, ROW_HEIGHT) + f:SetPoint("CENTER", frame, "CENTER") + if entry and entry.sessionResponses then + f:SetNormalTexture("Interface/Buttons/UI-GroupLoot-Dice-Up") + f:SetHighlightTexture("Interface/Buttons/UI-GroupLoot-Dice-Highlight") + f:SetScript("OnEnter", function() addon:CreateTooltip(L["Responses"], L["history_sessionResponses_tip"]) end) + f:SetScript("OnLeave", function() addon:HideTooltip() end) + f:SetScript("OnClick", function() LootHistory:ShowSessionResponses(row.name, entry) end) + f:Show() + else + f:Hide() + end + frame.sessionBtn = f +end + +function LootHistory.SetCellWinnerIndicator(rowFrame, frame, data, cols, row, realrow, column, fShow, table, ...) + if not frame.winnerTex then + frame.winnerTex = frame:CreateTexture(nil, "OVERLAY") + frame.winnerTex:SetPoint("CENTER", frame, "CENTER") + frame.winnerTex:SetSize(ROW_HEIGHT - 6, ROW_HEIGHT - 6) + frame.winnerTex:SetTexture("Interface/RaidFrame/ReadyCheck-Ready") + end + frame.winnerTex:SetShown(data[realrow].cols[column].args.isWinner or false) +end + +-- Same as SetCellNote, except the note is delivered in args instead of being fetched from the lootDB. +function LootHistory.SetCellSessionNote(rowFrame, frame, data, cols, row, realrow, column, fShow, table, ...) + local note = data[realrow].cols[column].args.note + local f = frame.noteBtn or CreateFrame("Button", nil, frame) + f:SetSize(ROW_HEIGHT, ROW_HEIGHT) + f:SetPoint("CENTER", frame, "CENTER") + if note then + f:SetNormalTexture("Interface/BUTTONS/UI-GuildButton-PublicNote-Up.png") + f:SetScript("OnEnter", function() addon:CreateTooltip(_G.LABEL_NOTE, note) end) + f:SetScript("OnLeave", function() addon:HideTooltip() end) + else + f:SetScript("OnEnter", nil) + f:SetNormalTexture("Interface/BUTTONS/UI-GuildButton-PublicNote-Disabled.png") + end + frame.noteBtn = f +end + +function LootHistory:GetSessionResponsesFrame() + if self.sessionResponsesFrame then return self.sessionResponsesFrame end + local f = addon.UI:NewNamed("RCFrame", self.frame, "RCLootHistorySessionResponsesFrame", L["Responses"], nil, 260) + f:SetFrameStrata("DIALOG") + addon.UI:RegisterForEscapeClose(f, function() f:Hide() end) + local st = LibStub("ScrollingTable"):CreateST({ + {name = "", width = ROW_HEIGHT,}, -- Class icon + {name = "", width = ROW_HEIGHT,}, -- Winner indicator + {name = _G.NAME, width = 110, sort = 1,}, + {name = L["Reason"], width = 140,}, + {name = L["Notes"], width = 40,}, + {name = _G.ITEM_LEVEL_ABBR, width = 45,}, + {name = _G.ROLL, width = 40,}, + {name = L["Votes"], width = 45,}, + }, 10, ROW_HEIGHT, nil, f.content) + st.frame:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -30) + f:SetWidth(st.frame:GetWidth() + 20) + f.st = st + self.sessionResponsesFrame = f + return f +end + +--- Displays everyone's response to a specific history entry, as collected on award. +---@param winner string Name of the history entry's owner. +---@param entry table The history entry containing sessionResponses. +function LootHistory:ShowSessionResponses(winner, entry) + local f = self:GetSessionResponsesFrame() + local rows = {} + for name, v in pairs(entry.sessionResponses) do + local isWinner = addon:UnitIsUnit(name, winner) + local class, response, color, responseID, note, votes + if isWinner then + -- The winner's data lives on the history entry itself + class, response, color, responseID, note, votes = + entry.class, entry.response, entry.color, entry.responseID, entry.note, entry.votes + else + local r = addon:GetResponse(entry.typeCode or "default", v.response) + class, response, color, responseID, note, votes = v.class, r.text, r.color, v.response, v.note, v.votes + end + tinsert(rows, { + cols = { + {DoCellUpdate = addon.SetCellClassIcon, args = {class}, value = class or ""}, + {DoCellUpdate = self.SetCellWinnerIndicator, args = {isWinner = isWinner}, value = isWinner and 1 or 0}, + {value = addon.Ambiguate(name), color = addon:GetClassColor(class)}, + {DoCellUpdate = self.SetCellResponse, args = {color = color, response = response, responseID = responseID or 0, isAwardReason = isWinner and entry.isAwardReason}}, + {DoCellUpdate = self.SetCellSessionNote, args = {note = note}, value = note and 1 or 0}, + {value = v.ilvl or ""}, + {value = v.roll or ""}, + {value = votes or 0}, + }, + }) + end + -- Winner on top, then by votes + table.sort(rows, function(a, b) + if a.cols[2].value ~= b.cols[2].value then return a.cols[2].value > b.cols[2].value end + return (a.cols[8].value or 0) > (b.cols[8].value or 0) + end) + f.st:SetData(rows) + f:Show() +end + function LootHistory.SetCellDelete(rowFrame, frame, data, cols, row, realrow, column, fShow, table, ...) if not frame.created then frame:SetNormalTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up") diff --git a/ml_core.lua b/ml_core.lua index 49bca9a4..b2257d3c 100644 --- a/ml_core.lua +++ b/ml_core.lua @@ -1242,6 +1242,37 @@ end local history_table = {} local historyCounter = 0 -- Used to generate history table entry unique id + +--- Collects everyone's response to a session for storage in the loot history. +--- Only candidates with an actual (numeric) response are included - passes and status texts are not. +--- The winner's response, note and votes are already on the history entry, so only their ilvl and roll is stored. +---@param session integer The session to collect responses from. +---@param winner string The winner of the session. +---@return table? #Candidate name -> response data, or nil if there's none. +function RCLootCouncilML:GetSessionResponses(session, winner) + if not session then return end + local lootTable = addon:GetActiveModule("votingframe"):GetLootTable() + local sessionData = lootTable and lootTable[session] + if not (sessionData and sessionData.candidates) then return end + local sessionResponses = {} + for name, data in pairs(sessionData.candidates) do + -- On re-awards the winner's response has been replaced with "AWARDED"; their real response is kept seperately. + local response = data.response == "AWARDED" and data.real_response or data.response + if addon:UnitIsUnit(name, winner) then + sessionResponses[name] = { ilvl = data.ilvl, roll = data.roll, } + elseif type(response) == "number" then -- An actual response, i.e. not a pass or status text + sessionResponses[name] = { + response = response, + class = data.class, + ilvl = data.ilvl, + roll = data.roll, + votes = data.votes ~= 0 and data.votes or nil, + note = data.note, + } + end + end + return next(sessionResponses) and sessionResponses or nil +end -- REVIEW Updated with recent changes in v2.9+. -- This should be refactored in v3.0 as several of the sources are no longer viable, and were ment to be used with ML. -- v2.19.0: Boss is included in lootTable, but kept as arg for backwards compatibility. @@ -1289,6 +1320,7 @@ function RCLootCouncilML:TrackAndLogLoot(winner, link, responseID, boss, reason, history_table["id"] = GetServerTime().."-"..historyCounter -- New in v2.7+. A unique id for the history entry. history_table["owner"] = owner or self.lootTable[session] and self.lootTable[session].owner or winner -- New in v2.9+. history_table["typeCode"] = self.lootTable[session] and self.lootTable[session].typeCode -- New in v2.15+. + history_table["sessionResponses"] = self:GetSessionResponses(session, winner) -- New in v3.22+. historyCounter = historyCounter + 1 From dd736d5c79b4a41435c309a9efd849edadc9495d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andreas=20gr=C3=B8terud?= Date: Sun, 12 Jul 2026 19:47:52 +0200 Subject: [PATCH 2/5] fix: Parent session responses frame to UIParent and add close button LibWindow calculates drag and save positions relative to the frame's parent, causing erratic snapping when parented to the (movable, scaled) history frame, and saved positions could resolve off-screen. The frame is also raised on show so it isn't stuck behind the history window, and gains a close button (escape works too). Co-Authored-By: Claude Fable 5 --- Modules/History/lootHistory.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Modules/History/lootHistory.lua b/Modules/History/lootHistory.lua index bd89c077..4e70f61e 100644 --- a/Modules/History/lootHistory.lua +++ b/Modules/History/lootHistory.lua @@ -538,8 +538,7 @@ end function LootHistory:GetSessionResponsesFrame() if self.sessionResponsesFrame then return self.sessionResponsesFrame end - local f = addon.UI:NewNamed("RCFrame", self.frame, "RCLootHistorySessionResponsesFrame", L["Responses"], nil, 260) - f:SetFrameStrata("DIALOG") + local f = addon.UI:NewNamed("RCFrame", UIParent, "RCLootHistorySessionResponsesFrame", L["Responses"], nil, 260) addon.UI:RegisterForEscapeClose(f, function() f:Hide() end) local st = LibStub("ScrollingTable"):CreateST({ {name = "", width = ROW_HEIGHT,}, -- Class icon @@ -553,6 +552,10 @@ function LootHistory:GetSessionResponsesFrame() }, 10, ROW_HEIGHT, nil, f.content) st.frame:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -30) f:SetWidth(st.frame:GetWidth() + 20) + f:SetHeight(st.frame:GetHeight() + 75) + local close = addon:CreateButton(_G.CLOSE, f.content) + close:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -10, 10) + close:SetScript("OnClick", function() f:Hide() end) f.st = st self.sessionResponsesFrame = f return f @@ -595,6 +598,7 @@ function LootHistory:ShowSessionResponses(winner, entry) end) f.st:SetData(rows) f:Show() + f:Raise() end function LootHistory.SetCellDelete(rowFrame, frame, data, cols, row, realrow, column, fShow, table, ...) From 8c27b2a7885aad674d7c90b16a49c84a937cb350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andreas=20gr=C3=B8terud?= Date: Mon, 13 Jul 2026 19:26:25 +0200 Subject: [PATCH 3/5] feat: Make session response storage opt-in Session responses can grow the loot history considerably over time, so collection is now gated behind a new 'Save Session Responses' toggle in the Loot History options, disabled by default. Co-Authored-By: Claude Fable 5 --- Core/Defaults.lua | 1 + Locale/enUS.lua | 2 ++ Modules/options.lua | 6 ++++++ ml_core.lua | 1 + 4 files changed, 10 insertions(+) diff --git a/Core/Defaults.lua b/Core/Defaults.lua index 3349293c..17992395 100644 --- a/Core/Defaults.lua +++ b/Core/Defaults.lua @@ -82,6 +82,7 @@ addon.defaults = { sendHistoryToGuildChannel = false, savePersonalLoot = true, saveBonusRolls = true, + saveSessionResponses = false, -- ML - General - Usage usage = { -- State of enabledness diff --git a/Locale/enUS.lua b/Locale/enUS.lua index ec023f67..46153945 100644 --- a/Locale/enUS.lua +++ b/Locale/enUS.lua @@ -542,6 +542,8 @@ L.chat_restrictions_enabled = "Not currently possible due to Addon Restrictions. L.history_export_sheets_tip = "Tab delimited export for Google Sheets and English version of Excel that uses ';' as formula delimiter." L.history_export_excel_international_tip = "Tab delimited export for international version of Excel that uses ',' as formula delimiter." L.history_sessionResponses_tip = "Click to show everyone's response to this item." +L.opt_saveSessionResponses_name = "Save Session Responses" +L.opt_saveSessionResponses_desc = "Check to store everyone's response to items you award as Master Looter in the loot history, viewable through the history's dice button. This will increase the storage used by the loot history." L.response_NOTELIGIBLE = "Not eligible for this item" L["opt_addButton_desc"] = "Add a new button group for the selected slot." L.opt_announceAward_WHISPER_WINNER = "/w winner" diff --git a/Modules/options.lua b/Modules/options.lua index 97a05d0f..2694b794 100644 --- a/Modules/options.lua +++ b/Modules/options.lua @@ -553,6 +553,12 @@ function addon:OptionsTable() desc = L.opt_saveBonusRolls_Desc, type = "toggle", }, + saveSessionResponses = { + order = 3.4, + name = L.opt_saveSessionResponses_name, + desc = L.opt_saveSessionResponses_desc, + type = "toggle", + }, header = { order = 4, type = "header", diff --git a/ml_core.lua b/ml_core.lua index b2257d3c..a3dd77b0 100644 --- a/ml_core.lua +++ b/ml_core.lua @@ -1250,6 +1250,7 @@ local historyCounter = 0 -- Used to generate history table entry unique id ---@param winner string The winner of the session. ---@return table? #Candidate name -> response data, or nil if there's none. function RCLootCouncilML:GetSessionResponses(session, winner) + if not db.saveSessionResponses then return end -- Opt-in due to the storage increase if not session then return end local lootTable = addon:GetActiveModule("votingframe"):GetLootTable() local sessionData = lootTable and lootTable[session] From fa684304915476943860216eaee8fb0fcd664415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andreas=20gr=C3=B8terud?= Date: Mon, 13 Jul 2026 19:27:54 +0200 Subject: [PATCH 4/5] feat: Require personal opt-in for storing session responses The Save Session Responses setting now also applies on the receiving end: session responses are stripped from incoming history entries unless the user has enabled it, so the ML controls collection while each user controls their own storage. Co-Authored-By: Claude Fable 5 --- Locale/enUS.lua | 2 +- Modules/History/lootHistory.lua | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Locale/enUS.lua b/Locale/enUS.lua index 46153945..6b8202ca 100644 --- a/Locale/enUS.lua +++ b/Locale/enUS.lua @@ -543,7 +543,7 @@ L.history_export_sheets_tip = "Tab delimited export for Google Sheets and Englis L.history_export_excel_international_tip = "Tab delimited export for international version of Excel that uses ',' as formula delimiter." L.history_sessionResponses_tip = "Click to show everyone's response to this item." L.opt_saveSessionResponses_name = "Save Session Responses" -L.opt_saveSessionResponses_desc = "Check to store everyone's response to items you award as Master Looter in the loot history, viewable through the history's dice button. This will increase the storage used by the loot history." +L.opt_saveSessionResponses_desc = "Check to store everyone's response to awarded items in the loot history, viewable through the history's dice button. As Master Looter this also controls whether responses are collected and sent to the group at all. This will increase the storage used by the loot history." L.response_NOTELIGIBLE = "Not eligible for this item" L["opt_addButton_desc"] = "Add a new button group for the selected slot." L.opt_announceAward_WHISPER_WINNER = "/w winner" diff --git a/Modules/History/lootHistory.lua b/Modules/History/lootHistory.lua index 4e70f61e..23ab2bca 100644 --- a/Modules/History/lootHistory.lua +++ b/Modules/History/lootHistory.lua @@ -134,6 +134,9 @@ function LootHistory:OnHistoryReceived (name, history) if not db.saveBonusRolls and history.responseID == "BONUS_ROLL" then return addon.Log:D("Not storing bonus rolls", history.lootWon) end + if not db.saveSessionResponses then -- Both the ML (collection) and each receiver (storage) must opt in + history.sessionResponses = nil + end -- v3.15.4 check for old date formats local d, m, y = strsplit("/", history.date, 3) if #tostring(d) < 4 then From 4900e5387d100952924e2d5ca2bd26e00b847dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andreas=20gr=C3=B8terud?= Date: Mon, 13 Jul 2026 19:28:40 +0200 Subject: [PATCH 5/5] feat: Enable session response storage by default Making the setting opt-out rather than opt-in - users concerned about storage can disable it in the Loot History options. Co-Authored-By: Claude Fable 5 --- Core/Defaults.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Defaults.lua b/Core/Defaults.lua index 17992395..5bbb56dd 100644 --- a/Core/Defaults.lua +++ b/Core/Defaults.lua @@ -82,7 +82,7 @@ addon.defaults = { sendHistoryToGuildChannel = false, savePersonalLoot = true, saveBonusRolls = true, - saveSessionResponses = false, + saveSessionResponses = true, -- ML - General - Usage usage = { -- State of enabledness