diff --git a/Extensions/MacroErrorUI.lua b/Extensions/MacroErrorUI.lua index 0d32087..8418f58 100644 --- a/Extensions/MacroErrorUI.lua +++ b/Extensions/MacroErrorUI.lua @@ -35,6 +35,15 @@ local measureFs = nil -- hidden FontString for measuring text width (wrap detec local currentErrors = nil local nameHighlight = nil -- Yellow backdrop behind macro name when name has errors +-- Whitelist GUI state +local whitelistButton = nil +local whitelistPopup = nil +local WHITELIST_ROW_POOL_SIZE = 8 +local whitelistRows = {} -- pre-allocated label+remove rows + +-- Forward declarations (defined later, referenced by whitelist GUI) +local RequestValidation + -- ============================================================================ -- Error Panel (Option A) -- ============================================================================ @@ -46,7 +55,7 @@ local function CreateErrorPanel() -- Anchor to the scroll frame (text edit area) instead of the full MacroFrame, -- which can extend far below the visible UI on extended macro clients local scrollRef = MacroFrameScrollFrame or MacroFrame - panel:SetPoint("TOPLEFT", scrollRef, "BOTTOMLEFT", 0, -27) + panel:SetPoint("TOPLEFT", scrollRef, "BOTTOMLEFT", -20, -27) panel:SetPoint("TOPRIGHT", scrollRef, "BOTTOMRIGHT", 0, -27) panel:SetHeight(1) panel:SetFrameStrata("DIALOG") @@ -290,6 +299,300 @@ local function UpdateNameHighlight(hasNameErrors) end end +-- ============================================================================ +-- Command Whitelist GUI +-- ============================================================================ + +local function RefreshWhitelistDisplay() + if not whitelistPopup then return end + + local commands = CleveRoids.GetWhitelistedCommandsList() + local count = table.getn(commands) + + -- Update rows + for i = 1, WHITELIST_ROW_POOL_SIZE do + local row = whitelistRows[i] + if row then + if i <= count then + row.label:SetText(commands[i]) + row.frame:Show() + else + row.label:SetText("") + row.frame:Hide() + end + end + end + + -- Show overflow indicator + local overflowText = whitelistPopup.overflowText + if count > WHITELIST_ROW_POOL_SIZE then + overflowText:SetText("|cff888888... and " .. (count - WHITELIST_ROW_POOL_SIZE) .. " more|r") + overflowText:Show() + else + overflowText:SetText("") + overflowText:Hide() + end + + -- Dynamic height: title(20) + gap(8) + editbox(18) + rows(18 each) + padding + local rowsShown = count + if rowsShown > WHITELIST_ROW_POOL_SIZE then rowsShown = WHITELIST_ROW_POOL_SIZE end + local height = 8 + 20 + 8 + 18 + 2 + (rowsShown * 18) + if count > WHITELIST_ROW_POOL_SIZE then + height = height + 14 + end + height = height + 8 -- bottom padding + whitelistPopup:SetHeight(height) + + -- Re-trigger error checking so results update in real time + RequestValidation() +end + +local function CreateWhitelistPopup() + if whitelistPopup then return end + + local popup = CreateFrame("Frame", "CleveRoidsWhitelistPopup", UIParent) + popup:SetWidth(220) + popup:SetHeight(120) + popup:SetPoint("CENTER", UIParent, "CENTER", 0, 100) + popup:SetFrameStrata("FULLSCREEN_DIALOG") + popup:SetMovable(true) + popup:EnableMouse(true) + + -- pfUI pixel-perfect flat style + popup:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + popup:SetBackdropColor(0.08, 0.08, 0.1, 0.95) + popup:SetBackdropBorderColor(0.2, 0.2, 0.2, 1) + popup:Hide() + + -- Drag handling + popup:RegisterForDrag("LeftButton") + popup:SetScript("OnDragStart", function() + this:StartMoving() + end) + popup:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + end) + + -- Title + local title = popup:CreateFontString(nil, "OVERLAY") + title:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE") + title:SetPoint("TOPLEFT", popup, "TOPLEFT", 8, -8) + title:SetText("Whitelisted Commands") + title:SetTextColor(0.9, 0.8, 0.5, 1) + + -- Close button (flat X) + local closeBtn = CreateFrame("Button", nil, popup) + closeBtn:SetWidth(16) + closeBtn:SetHeight(16) + closeBtn:SetPoint("TOPRIGHT", popup, "TOPRIGHT", -4, -4) + local closeLbl = closeBtn:CreateFontString(nil, "OVERLAY") + closeLbl:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE") + closeLbl:SetPoint("CENTER", 0, 0) + closeLbl:SetText("x") + closeLbl:SetTextColor(0.6, 0.6, 0.6, 1) + closeBtn:SetScript("OnEnter", function() + closeLbl:SetTextColor(1, 0.3, 0.3, 1) + end) + closeBtn:SetScript("OnLeave", function() + closeLbl:SetTextColor(0.6, 0.6, 0.6, 1) + end) + closeBtn:SetScript("OnClick", function() + popup:Hide() + end) + + -- EditBox (flat style) + local editBox = CreateFrame("EditBox", "CleveRoidsWhitelistEditBox", popup) + editBox:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8) + editBox:SetWidth(148) + editBox:SetHeight(18) + editBox:SetAutoFocus(false) + editBox:SetMaxLetters(60) + editBox:SetFont("Fonts\\FRIZQT__.TTF", 10, "") + editBox:SetTextColor(1, 1, 1, 1) + editBox:SetTextInsets(4, 4, 0, 0) + editBox:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + editBox:SetBackdropColor(0.05, 0.05, 0.07, 0.9) + editBox:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + + -- Add button (flat style) + local addBtn = CreateFrame("Button", nil, popup) + addBtn:SetPoint("LEFT", editBox, "RIGHT", 4, 0) + addBtn:SetWidth(40) + addBtn:SetHeight(18) + addBtn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + addBtn:SetBackdropColor(0.15, 0.15, 0.15, 0.9) + addBtn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + local addLbl = addBtn:CreateFontString(nil, "OVERLAY") + addLbl:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") + addLbl:SetPoint("CENTER", 0, 0) + addLbl:SetText("Add") + addLbl:SetTextColor(0.8, 0.8, 0.8, 1) + addBtn:SetScript("OnEnter", function() + this:SetBackdropColor(0.25, 0.25, 0.25, 0.95) + this:SetBackdropBorderColor(0.45, 0.45, 0.5, 1) + addLbl:SetTextColor(1, 1, 1, 1) + end) + addBtn:SetScript("OnLeave", function() + this:SetBackdropColor(0.15, 0.15, 0.15, 0.9) + this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + addLbl:SetTextColor(0.8, 0.8, 0.8, 1) + end) + + local function AddCommand() + local text = editBox:GetText() + if not text or CleveRoids.Trim(text) == "" then return end + text = CleveRoids.Trim(text) + CleveRoids.AddWhitelistedCommand(text) + editBox:SetText("") + editBox:ClearFocus() + RefreshWhitelistDisplay() + end + + addBtn:SetScript("OnClick", AddCommand) + editBox:SetScript("OnEnterPressed", function() + AddCommand() + end) + editBox:SetScript("OnEscapePressed", function() + this:ClearFocus() + end) + + -- Anchor for rows: below the editbox row + local rowAnchor = editBox + + -- Pre-allocate row pool + for i = 1, WHITELIST_ROW_POOL_SIZE do + local rowFrame = CreateFrame("Frame", nil, popup) + rowFrame:SetHeight(16) + rowFrame:SetPoint("TOPLEFT", rowAnchor, "BOTTOMLEFT", 0, -2) + rowFrame:SetPoint("RIGHT", popup, "RIGHT", -8, 0) + rowFrame:Hide() + + local label = rowFrame:CreateFontString(nil, "OVERLAY") + label:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") + label:SetPoint("LEFT", rowFrame, "LEFT", 2, 0) + label:SetJustifyH("LEFT") + label:SetTextColor(0.7, 0.7, 0.7, 1) + + -- Flat X remove button + local removeBtn = CreateFrame("Button", nil, rowFrame) + removeBtn:SetWidth(14) + removeBtn:SetHeight(14) + removeBtn:SetPoint("RIGHT", rowFrame, "RIGHT", -1, 0) + local removeLbl = removeBtn:CreateFontString(nil, "OVERLAY") + removeLbl:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") + removeLbl:SetPoint("CENTER", 0, 0) + removeLbl:SetText("x") + removeLbl:SetTextColor(0.5, 0.5, 0.5, 1) + removeBtn.removeLbl = removeLbl + removeBtn.rowIndex = i + + removeBtn:SetScript("OnEnter", function() + this.removeLbl:SetTextColor(1, 0.3, 0.3, 1) + end) + removeBtn:SetScript("OnLeave", function() + this.removeLbl:SetTextColor(0.5, 0.5, 0.5, 1) + end) + removeBtn:SetScript("OnClick", function() + local idx = this.rowIndex + local row = whitelistRows[idx] + if row and row.label then + local cmd = row.label:GetText() + if cmd and cmd ~= "" then + CleveRoids.RemoveWhitelistedCommand(cmd) + RefreshWhitelistDisplay() + end + end + end) + + whitelistRows[i] = { frame = rowFrame, label = label, removeBtn = removeBtn } + rowAnchor = rowFrame + end + + -- Overflow text + local overflowText = popup:CreateFontString(nil, "OVERLAY") + overflowText:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") + overflowText:SetPoint("TOPLEFT", whitelistRows[WHITELIST_ROW_POOL_SIZE].frame, "BOTTOMLEFT", 2, -2) + overflowText:SetJustifyH("LEFT") + overflowText:SetTextColor(0.5, 0.5, 0.5, 1) + overflowText:Hide() + popup.overflowText = overflowText + + whitelistPopup = popup +end + +local function CreateWhitelistButton() + if whitelistButton then return end + if not MacroFrame then return end + + local btn = CreateFrame("Button", "CleveRoidsWhitelistButton", MacroFrame) + btn:SetWidth(80) + btn:SetHeight(18) + btn:SetPoint("BOTTOMRIGHT", MacroFrame, "BOTTOMRIGHT", 8, 52) + btn:SetFrameStrata("DIALOG") + + -- pfUI pixel-perfect flat style + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + btn:SetBackdropColor(0.1, 0.1, 0.1, 0.85) + btn:SetBackdropBorderColor(0.3, 0.3, 0.3, 1) + + local label = btn:CreateFontString(nil, "OVERLAY") + label:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") + label:SetPoint("CENTER", btn, "CENTER", 0, 0) + label:SetText("Whitelist") + label:SetTextColor(0.8, 0.8, 0.8, 1) + btn.label = label + + -- Hover highlight + btn:SetScript("OnEnter", function() + this:SetBackdropColor(0.2, 0.2, 0.2, 0.95) + this:SetBackdropBorderColor(0.45, 0.45, 0.5, 1) + this.label:SetTextColor(1, 1, 1, 1) + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:SetText("Command Whitelist") + GameTooltip:AddLine("Add third-party addon commands so the", 1, 1, 1, true) + GameTooltip:AddLine("error checker stops flagging them.", 1, 1, 1, true) + GameTooltip:Show() + end) + btn:SetScript("OnLeave", function() + this:SetBackdropColor(0.1, 0.1, 0.1, 0.85) + this:SetBackdropBorderColor(0.3, 0.3, 0.3, 1) + this.label:SetTextColor(0.8, 0.8, 0.8, 1) + GameTooltip:Hide() + end) + + btn:SetScript("OnClick", function() + CreateWhitelistPopup() + if whitelistPopup:IsVisible() then + whitelistPopup:Hide() + else + RefreshWhitelistDisplay() + whitelistPopup:Show() + end + end) + + whitelistButton = btn +end + -- ============================================================================ -- Validation & Debounce -- ============================================================================ @@ -426,7 +729,7 @@ local function RunValidation() UpdateNameHighlight(hasNameErrors) end -local function RequestValidation() +RequestValidation = function() lastKeystroke = GetTime() pendingValidation = true end @@ -607,6 +910,10 @@ local function ClearAll() if nameHighlight then nameHighlight:Hide() end + + if whitelistPopup then + whitelistPopup:Hide() + end end -- ============================================================================ @@ -635,8 +942,9 @@ local function InstallHooks() if origOnShow then origOnShow() end - -- Create panel lazily on first show + -- Create UI lazily on first show CreateErrorPanel() + CreateWhitelistButton() -- Reset state and validate lastSelectedMacro = MacroFrame.selectedMacro RunValidation() diff --git a/Init.lua b/Init.lua index f6b2274..93e0bb1 100644 --- a/Init.lua +++ b/Init.lua @@ -123,6 +123,8 @@ CleveRoids.dynamicCmds = { ["/equip12"] = true, ["/equip13"] = true, ["/equip14"] = true, + ["/applymain"] = true, + ["/applyoff"] = true, } -- Equipment swap queue system diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index d37a146..acf5db9 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -422,6 +422,59 @@ local VALID_COMMANDS = { ["/yes"] = true, } +-- ============================================================================ +-- Command Whitelist API (for third-party addon commands) +-- ============================================================================ + +local function GetWhitelistedCommands() + if not CleveRoidMacros then CleveRoidMacros = {} end + if not CleveRoidMacros.whitelistedCommands then + CleveRoidMacros.whitelistedCommands = {} + end + return CleveRoidMacros.whitelistedCommands +end + +function CleveRoids.AddWhitelistedCommand(cmd) + if not cmd or cmd == "" then return end + cmd = string.lower(cmd) + -- Auto-prepend "/" if missing + if string.sub(cmd, 1, 1) ~= "/" then + cmd = "/" .. cmd + end + local whitelist = GetWhitelistedCommands() + whitelist[cmd] = true +end + +function CleveRoids.RemoveWhitelistedCommand(cmd) + if not cmd or cmd == "" then return end + cmd = string.lower(cmd) + if string.sub(cmd, 1, 1) ~= "/" then + cmd = "/" .. cmd + end + local whitelist = GetWhitelistedCommands() + whitelist[cmd] = nil +end + +function CleveRoids.IsWhitelistedCommand(cmd) + if not cmd or cmd == "" then return false end + cmd = string.lower(cmd) + if string.sub(cmd, 1, 1) ~= "/" then + cmd = "/" .. cmd + end + local whitelist = GetWhitelistedCommands() + return whitelist[cmd] == true +end + +function CleveRoids.GetWhitelistedCommandsList() + local whitelist = GetWhitelistedCommands() + local list = {} + for cmd, _ in pairs(whitelist) do + table.insert(list, cmd) + end + table.sort(list) + return list +end + -- Commands that can have conditionals without actions -- e.g., /petattack [harm] or /target [exists,hp:<=20] local COMMANDS_NO_ACTION_NEEDED = { @@ -679,7 +732,7 @@ local function validateLine(line, lineNum) local _, _, cmd = safeStringFind(line, "^(/[a-z]+%d*)") if cmd then local lowerCmd = string.lower(cmd) - if not VALID_COMMANDS[lowerCmd] then + if not VALID_COMMANDS[lowerCmd] and not CleveRoids.IsWhitelistedCommand(lowerCmd) then table.insert(localErrors, { type = ERROR_TYPES.INVALID_COMMAND, line = lineNum, diff --git a/NampowerAPI.lua b/NampowerAPI.lua index e28d2f3..90d24d4 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -91,7 +91,13 @@ Pass ignoreMissing=1 to skip aura-slot presence check (for overflow buffs in server slots 33-48 that have no client aura slot) - Current version: v2.34.0 + GUID String Fix & Talent Helper (v2.35+): + - GetUnitData/GetUnitField now return GUID-type fields (charm, summon, charmedBy, + summonedBy, createdBy, target, persuaded, channelObject) as hex strings + (e.g., "0x0600000012345678") instead of raw numbers to avoid Lua 64-bit precision loss + - LearnTalentRank(talentPage, talentIndex, rank) - Learn a specific talent rank directly + + Current version: v2.35.0 ]] local _G = _G or getfenv(0) @@ -252,6 +258,10 @@ API.VERSION_REQUIREMENTS = { -- v2.34+ - Aura cancel functions ["CancelPlayerAuraSlot"] = { 2, 34, 0, "CancelPlayerAuraSlot" }, ["CancelPlayerAuraSpellId"] = { 2, 34, 0, "CancelPlayerAuraSpellId" }, + + -- v2.35+ - GUID string fix and talent helper + ["GuidStringFormat"] = { 2, 35, 0 }, -- GetUnitData/GetUnitField return GUID fields as hex strings + ["LearnTalentRank"] = { 2, 35, 0, "LearnTalentRank" }, } -- Check if a specific feature is available @@ -396,6 +406,10 @@ local function InitializeFeatures() f.hasCancelPlayerAuraSlot = API.HasFeature("CancelPlayerAuraSlot") f.hasCancelPlayerAuraSpellId = API.HasFeature("CancelPlayerAuraSpellId") + -- v2.35+ GUID string format and talent helper + f.hasGuidStringFormat = API.HasFeature("GuidStringFormat") + f.hasLearnTalentRank = API.HasFeature("LearnTalentRank") + -- Runtime detection for enhanced spell functions (verify by testing) if f.hasEnhancedSpellFunctions and GetSpellTexture then local success, result = pcall(function() @@ -3021,5 +3035,25 @@ function API.CancelPlayerAuraSpellId(spellId, ignoreMissing) return true end +-------------------------------------------------------------------------------- +-- TALENT HELPER (v2.35+) +-------------------------------------------------------------------------------- + +-- Learn a specific talent rank directly (v2.35+) +-- talentPage: 1-3 (talent tab) +-- talentIndex: 1-32 (talent position within tab) +-- rank: 1-5 (rank to learn) +function API.LearnTalentRank(talentPage, talentIndex, rank) + if not API.features.hasLearnTalentRank or not _G.LearnTalentRank then + return false + end + if not talentPage or not talentIndex or not rank then return false end + if talentPage < 1 or talentPage > 3 then return false end + if talentIndex < 1 or talentIndex > 32 then return false end + if rank < 1 or rank > 5 then return false end + _G.LearnTalentRank(talentPage, talentIndex, rank) + return true +end + -- Expose API globally for other addons _G.CleveRoidsNampowerAPI = API