local _, addon = ... local API = addon.API; local L = addon.L; local CallbackRegistry = addon.CallbackRegistry; local tonumber = tonumber; local match = string.match; local format = string.format; local gsub = string.gsub; local tinsert = table.insert; local tremove = table.remove; local floor = math.floor; local sqrt = math.sqrt; local time = time; local unpack = unpack; local GetCVarBool = C_CVar.GetCVarBool; local CreateFrame = CreateFrame; local securecallfunction = securecallfunction; local function Nop(...) end API.Nop = Nop; do -- Table local function Mixin(object, ...) for i = 1, select("#", ...) do local mixin = select(i, ...) for k, v in pairs(mixin) do object[k] = v; end end return object end API.Mixin = Mixin; local function CreateFromMixins(...) return Mixin({}, ...) end API.CreateFromMixins = CreateFromMixins; local function RemoveValueFromList(tbl, v) for i = 1, #tbl do if tbl[i] == v then tremove(tbl, i); return true end end end API.RemoveValueFromList = RemoveValueFromList; local function ReverseList(list) if not list then return end; local tbl = {}; local n = 0; for i = #list, 1, -1 do n = n + 1; tbl[n] = list[i]; end return tbl end API.ReverseList = ReverseList; local function CopyTable(tbl) --Blizzard TableUtil.lua if not tbl then return; end; local copy = {}; for k, v in pairs(tbl) do if type(v) == "table" then copy[k] = CopyTable(v); else copy[k] = v; end end return copy; end API.CopyTable = CopyTable; end do -- String local function GetCreatureIDFromGUID(guid) local id = match(guid, "Creature%-0%-%d*%-%d*%-%d*%-(%d*)"); if id then return tonumber(id) end end API.GetCreatureIDFromGUID = GetCreatureIDFromGUID; local function GetVignetteIDFromGUID(guid) local id = match(guid, "Vignette%-0%-%d*%-%d*%-%d*%-(%d*)"); if id then return tonumber(id) end end API.GetVignetteIDFromGUID = GetVignetteIDFromGUID; local function GetWaypointFromText(text) local uiMapID, x, y = match(text, "|Hworldmap:(%d*):(%d*):(%d*)|h"); if uiMapID and x and y then return tonumber(uiMapID), tonumber(x), tonumber(y) end end API.GetWaypointFromText = GetWaypointFromText; local UnitGUID = UnitGUID; local function GetUnitCreatureID(unit) local guid = UnitGUID(unit); if guid then return GetCreatureIDFromGUID(guid) end end API.GetUnitCreatureID = GetUnitCreatureID; local ValidUnitTypes = { Creature = true, Pet = true, GameObject = true, Vehicle = true, }; local function GetUnitIDGeneral(unit) local guid = UnitGUID(unit); if guid then local unitType, id = match(guid, "(%a+)%-0%-%d*%-%d*%-%d*%-(%d*)"); if id and unitType and ValidUnitTypes[unitType] then return tonumber(id) end end end API.GetUnitIDGeneral = GetUnitIDGeneral; local function GetGlobalObject(objNameKey) --Get object via string "FrameName.Key1.Key2" local obj = _G; for k in string.gmatch(objNameKey, "%w+") do obj = obj[k]; if not obj then return end end return obj end API.GetGlobalObject = GetGlobalObject; local function JoinText(delimiter, l, r) if l and r then return l..delimiter..r else return l or r end end API.JoinText = JoinText; function API.StringTrim(text) if text then text = gsub(text, "^(%s+)", ""); text = gsub(text, "(%s+)$", ""); if text ~= "" then return text end end end end do -- DEBUG local function CreateSaveDB(key) if not PlumberDevOutput then PlumberDevOutput = {}; end if not PlumberDevOutput[key] then PlumberDevOutput[key] = {}; end end local function SaveLocalizedText(localizedText, englishText) local locale = GetLocale(); CreateSaveDB(locale); PlumberDevOutput[locale][localizedText] = englishText or true; end API.SaveLocalizedText = SaveLocalizedText; local function SaveDataUnderKey(key, ...) CreateSaveDB(key); PlumberDevOutput[key] = {...} end API.SaveDataUnderKey = SaveDataUnderKey; end do -- Math local function Clamp(value, min, max) if value > max then return max elseif value < min then return min end return value end API.Clamp = Clamp; local function Lerp(startValue, endValue, amount) return (1 - amount) * startValue + amount * endValue; end API.Lerp = Lerp; local function GetPointsDistance2D(x1, y1, x2, y2) return sqrt( (x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)) end API.GetPointsDistance2D = GetPointsDistance2D; local function Round(n) return floor(n + 0.5); end API.Round = Round; local function RoundCoord(n) return floor(n * 1000 + 0.5) * 0.001 end API.RoundCoord = RoundCoord; local function Saturate(value) return Clamp(value, 0.0, 1.0); end local function DeltaLerp(startValue, endValue, amount, timeSec) return Lerp(startValue, endValue, Saturate(amount * timeSec * 60.0)); end API.DeltaLerp = DeltaLerp; end do -- Color local ColorSwatches = { SelectionBlue = {12, 105, 216}, SmoothGreen = {124, 197, 118}, WarningRed = {212, 100, 28}, --228, 13, 14 248, 81, 73 }; for _, swatch in pairs(ColorSwatches) do swatch[1] = swatch[1]/255; swatch[2] = swatch[2]/255; swatch[3] = swatch[3]/255; end local function GetColorByName(colorName) if ColorSwatches[colorName] then return unpack(ColorSwatches[colorName]) else return 1, 1, 1 end end API.GetColorByName = GetColorByName; -- Make Rare and Epic brighter (use the color in Narcissus) local ITEM_QUALITY_COLORS = ITEM_QUALITY_COLORS; local QualityColors = {}; QualityColors[1] = CreateColor(0.92, 0.92, 0.92, 1); QualityColors[3] = CreateColor(105/255, 158/255, 255/255, 1); QualityColors[4] = CreateColor(185/255, 83/255, 255/255, 1); local function GetItemQualityColor(quality) if QualityColors[quality] then return QualityColors[quality] else return ITEM_QUALITY_COLORS[quality].color end end API.GetItemQualityColor = GetItemQualityColor; local function IsWarningColor(r, g, b) --Used to determine if the tooltip fontstring is red, which indicates there is a requirement you don't meet return (r > 0.99 and r <= 1) and (g > 0.1254 and g < 0.1255) and (b > 0.1254 and b < 0.1255) end API.IsWarningColor = IsWarningColor; local function SetTextColorByGlobal(fontString, colorMixin) local r, g, b; if colorMixin then r, g, b = colorMixin:GetRGB(); else r, g, b = 1, 1, 1; end fontString:SetTextColor(r, g, b); end API.SetTextColorByGlobal = SetTextColorByGlobal; end do -- Time local D_DAYS = D_DAYS or "%d |4Day:Days;"; local D_HOURS = D_HOURS or "%d |4Hour:Hours;"; local D_MINUTES = D_MINUTES or "%d |4Minute:Minutes;"; local D_SECONDS = D_SECONDS or "%d |4Second:Seconds;"; local DAYS_ABBR = DAYS_ABBR or "%d |4Day:Days;" local HOURS_ABBR = HOURS_ABBR or "%d |4Hr:Hr;"; local MINUTES_ABBR = MINUTES_ABBR or "%d |4Min:Min;"; local SECONDS_ABBR = SECONDS_ABBR or "%d |4Sec:Sec;"; local SHOW_HOUR_BELOW_DAYS = 3; local SHOW_MINUTE_BELOW_HOURS = 12; local SHOW_SECOND_BELOW_MINUTES = 10; local COLOR_RED_BELOW_SECONDS = 43200; local function BakePlural(number, singularPlural) singularPlural = gsub(singularPlural, ";", ""); if number > 1 then return format(gsub(singularPlural, "|4[^:]*:", ""), number); else singularPlural = gsub(singularPlural, ":.*", ""); singularPlural = gsub(singularPlural, "|4", ""); return format(singularPlural, number); end end local function FormatTime(t, pattern, bakePluralEscapeSequence) if bakePluralEscapeSequence then return BakePlural(t, pattern); else return format(pattern, t); end end local function SecondsToTime(seconds, abbreviated, partialTime, bakePluralEscapeSequence, colorized) --partialTime: Stop processing if the remaining units don't really matter. e.g. to display the remaining time of an event when there are still days left --bakePluralEscapeSequence: Convert EcsapeSequence like "|4Sec:Sec;" to its result so it can be sent to chat local intialSeconds = seconds; local timeString = ""; local isComplete = false; local days = 0; local hours = 0; local minutes = 0; if seconds >= 86400 then days = floor(seconds / 86400); seconds = seconds - days * 86400; local dayText = FormatTime(days, (abbreviated and DAYS_ABBR) or D_DAYS, bakePluralEscapeSequence); timeString = dayText; if partialTime and days >= SHOW_HOUR_BELOW_DAYS then isComplete = true; end end if not isComplete then hours = floor(seconds / 3600); seconds = seconds - hours * 3600; if hours > 0 then local hourText = FormatTime(hours, (abbreviated and HOURS_ABBR) or D_HOURS, bakePluralEscapeSequence); if timeString == "" then timeString = hourText; else timeString = timeString.." "..hourText; end if partialTime and hours >= SHOW_MINUTE_BELOW_HOURS then isComplete = true; end else if timeString ~= "" and partialTime then isComplete = true; end end end if partialTime and days > 0 then isComplete = true; end if not isComplete then minutes = floor(seconds / 60); seconds = seconds - minutes * 60; if minutes > 0 then local minuteText = FormatTime(minutes, (abbreviated and MINUTES_ABBR) or D_MINUTES, bakePluralEscapeSequence); if timeString == "" then timeString = minuteText; else timeString = timeString.." "..minuteText; end if partialTime and (minutes >= SHOW_SECOND_BELOW_MINUTES or hours > 0) then isComplete = true; end else if timeString ~= "" and partialTime then isComplete = true; end end end if (not isComplete) and seconds > 0 then seconds = floor(seconds); local secondText = FormatTime(seconds, (abbreviated and SECONDS_ABBR) or D_SECONDS, bakePluralEscapeSequence); if timeString == "" then timeString = secondText; else timeString = timeString.." "..secondText; end end if colorized and partialTime and intialSeconds < COLOR_RED_BELOW_SECONDS and not bakePluralEscapeSequence then --WARNING_FONT_COLOR timeString = "|cffff4800"..timeString.."|r"; end return timeString end API.SecondsToTime = SecondsToTime; local function SecondsToClock(seconds) --Clock: 00:00 return format("%s:%02d", floor(seconds / 60), floor(seconds % 60)) end API.SecondsToClock = SecondsToClock; --Unix Epoch is in UTC local MonthDays = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, }; local function IsLeapYear(year) return year % 400 == 0 or (year % 4 == 0 and year % 100 ~= 0) end local function GetFebruaryDays(year) if IsLeapYear(year) then return 29 else return 28 end end local function IsLeftTimeFuture(time1, time2, i) if not time1[i] then return false end; if time1[i] > time2[i] then return true; elseif time1[i] == time2[i] then return IsLeftTimeFuture(time1, time2, i + 1); else return false end end local function GetNumDaysToDate(year, month, day) local numDays = day; for yr = 1, (year -1) do if IsLeapYear(yr) then numDays = numDays + 366; else numDays = numDays + 365; end end for m = 1, (month - 1) do if m == 2 then numDays = numDays + GetFebruaryDays(year); else numDays = numDays + MonthDays[m]; end end return numDays end local function GetNumSecondsToDate(year, month, day, hour, minute, second) hour = hour or 0; minute = minute or 0; second = second or 0; local numDays = GetNumDaysToDate(year, month, day); local numSeconds = second; numSeconds = numSeconds + numDays * 86400; numSeconds = numSeconds + hour * 3600 + minute * 60; return numSeconds end local function ConvertCalendarTime(calendarTime) --WoW's CalendarTime See https://warcraft.wiki.gg/wiki/API_C_DateAndTime.GetCurrentCalendarTime local year = calendarTime.year; local month = calendarTime.month; local day = calendarTime.monthDay; local hour = calendarTime.hour; local minute = calendarTime.minute; local second = calendarTime.second or 0; --the original calendarTime does not contain second return {year, month, day, hour, minute, second} end local function GetCalendarTimeDifference(lhsCalendarTime, rhsCalendarTime) --time = {year, month, day, hour, minute, second} local time1 = ConvertCalendarTime(lhsCalendarTime); local time2 = ConvertCalendarTime(rhsCalendarTime); local second1 = GetNumSecondsToDate(unpack(time1)); local second2 = GetNumSecondsToDate(unpack(time2)); return second2 - second1 end API.GetCalendarTimeDifference = GetCalendarTimeDifference; local function WrapNumberWithBrackets(text) text = gsub(text, "%%d%+", "%%d"); text = gsub(text, "%%d", "%(%%d%+%)"); return text end local PATTERN_DAYS = WrapNumberWithBrackets(DAYS_ABBR); local PATTERN_HOURS = WrapNumberWithBrackets(HOURS_ABBR); local PATTERN_MINUTES = WrapNumberWithBrackets(MINUTES_ABBR); local PATTERN_SECONDS = WrapNumberWithBrackets(SECONDS_ABBR); local function ConvertTextToSeconds(durationText) if not durationText then return 0 end; if not match(durationText, "%d") then return 0 end; local hours = tonumber(match(durationText, PATTERN_HOURS) or 0); local minutes = tonumber(match(durationText, PATTERN_MINUTES) or 0); local seconds = tonumber(match(durationText, PATTERN_SECONDS) or 0); return 3600 * hours + 60 * minutes + seconds; end API.TimeLeftTextToSeconds = ConvertTextToSeconds; end do -- Item local C_Item = C_Item; local GetItemSpell = GetItemSpell; local function ColorizeTextByQuality(text, quality, allowColorBlind) if not (text and quality) then return text end local color = API.GetItemQualityColor(quality); text = color:WrapTextInColorCode(text); if allowColorBlind and GetCVarBool("colorblindMode") then text = text.." |cffffffff[".._G[format("ITEM_QUALITY%s_DESC", quality)].."]|r"; end return text end API.ColorizeTextByQuality = ColorizeTextByQuality; local function GetColorizedItemName(itemID) local name = C_Item.GetItemNameByID(itemID); local quality = C_Item.GetItemQualityByID(itemID); return ColorizeTextByQuality(name, quality, true); end API.GetColorizedItemName = GetColorizedItemName; local function GetItemSpellID(item) local spellName, spellID = GetItemSpell(item); return spellID end API.GetItemSpellID = GetItemSpellID; function API.IsToyItem(item) return C_ToyBox.GetToyInfo(item) ~= nil end function API.GetItemSellPrice(item) if item then local sellPrice = select(11, C_Item.GetItemInfo(item)); if sellPrice and sellPrice > 0 then return sellPrice end end end function API.IsMountCollected(mountID) local isCollected = select(11, C_MountJournal.GetMountInfoByID(mountID)); return isCollected end end do -- Tooltip Parser local GetInfoByHyperlink = C_TooltipInfo and C_TooltipInfo.GetHyperlink; local function GetLineText(lines, index) if lines[index] then return lines[index].leftText; end end local function GetCreatureName(creatureID) if not creatureID then return end; local tooltipData = GetInfoByHyperlink("unit:Creature-0-0-0-0-"..creatureID); if tooltipData then return GetLineText(tooltipData.lines, 1); end end API.GetCreatureName = GetCreatureName; end do -- Holiday local CalendarTextureXHolidayKey = { --Only the important ones :) [235469] = "lunarfestival", [235470] = "lunarfestival", [235471] = "lunarfestival", [235466] = "loveintheair", [235467] = "loveintheair", [235468] = "loveintheair", [235475] = "noblegarden", [235476] = "noblegarden", [235477] = "noblegarden", [235443] = "childrensweek", [235444] = "childrensweek", [235445] = "childrensweek", [235472] = "midsummer", [235473] = "midsummer", [235474] = "midsummer", [235439] = "brewfest", [235440] = "brewfest", [235441] = "brewfest", [235442] = "brewfest", [235460] = "hallowsendend", [235461] = "hallowsendend", [235462] = "hallowsendend", [235482] = "winterveil", [235483] = "winterveil", [235484] = "winterveil", [235485] = "winterveil", }; local HolidayInfoMixin = {}; function HolidayInfoMixin:GetRemainingSeconds() if self.endTime then local presentTime = time(); return self.endTime - presentTime else return 0 end end function HolidayInfoMixin:GetEndTimeString() --MM/DD 00:00 return self.endTimeString end function HolidayInfoMixin:GetRemainingTimeString() --DD/HH/MM/SS local seconds = self:GetRemainingSeconds(); return API.SecondsToTime(seconds, false, true); end function HolidayInfoMixin:GetName() return self.name end function HolidayInfoMixin:GetKey() return self.key end local function GetActiveMajorHolidayInfo() local currentCalendarTime = C_DateAndTime.GetCurrentCalendarTime(); local presentDay = currentCalendarTime.monthDay; local monthOffset = 0; local holidayInfo; local holidayKey, holidayName; local endTimeString; local eventEndTimeMixin; --{} local endTime; --number time() local activeHolidayData; --{ {holiday1}, {holiday2} } for i = 1, C_Calendar.GetNumDayEvents(monthOffset, presentDay) do --Need to request data first with C_Calendar.OpenCalendar() holidayInfo = C_Calendar.GetHolidayInfo(monthOffset, presentDay, i); --print(i, holidayInfo.name) if holidayInfo and holidayInfo.texture and CalendarTextureXHolidayKey[holidayInfo.texture] then holidayKey = CalendarTextureXHolidayKey[holidayInfo.texture]; holidayName = holidayInfo.name; if holidayInfo.startTime and holidayInfo.endTime then endTimeString = FormatShortDate(holidayInfo.endTime.monthDay, holidayInfo.endTime.month) .." ".. GameTime_GetFormattedTime(holidayInfo.endTime.hour, holidayInfo.endTime.minute, true); eventEndTimeMixin = holidayInfo.endTime; end local isEventActive = true; if eventEndTimeMixin then local presentTime = time(); local remainingSeconds = API.GetCalendarTimeDifference(currentCalendarTime, eventEndTimeMixin); endTime = presentTime + remainingSeconds; if remainingSeconds <= 0 then isEventActive = false; end end if isEventActive and holidayName then local mixin = API.CreateFromMixins(HolidayInfoMixin); mixin.name = holidayName; mixin.key = holidayKey; mixin.endTimeString = endTimeString; mixin.endTime = endTime; if not activeHolidayData then activeHolidayData = {}; end tinsert(activeHolidayData, mixin); end end end return activeHolidayData end API.GetActiveMajorHolidayInfo = GetActiveMajorHolidayInfo; end do -- Fade Frame local abs = math.abs; local tinsert = table.insert; local wipe = wipe; local fadeInfo = {}; local fadingFrames = {}; local f = CreateFrame("Frame"); local function OnUpdate(self, elpased) local i = 1; local frame, info, timer, alpha; local isComplete = true; while fadingFrames[i] do frame = fadingFrames[i]; info = fadeInfo[frame]; if info then timer = info.timer + elpased; if timer >= info.duration then alpha = info.toAlpha; fadeInfo[frame] = nil; if info.alterShownState and alpha <= 0 then frame:Hide(); end else alpha = info.fromAlpha + (info.toAlpha - info.fromAlpha) * timer/info.duration; info.timer = timer; end frame:SetAlpha(alpha); isComplete = false; end i = i + 1; end if isComplete then f:Clear(); end end function f:Clear() self:SetScript("OnUpdate", nil); wipe(fadingFrames); wipe(fadeInfo); end function f:Add(frame, fullDuration, fromAlpha, toAlpha, alterShownState, useConstantDuration) local alpha = frame:GetAlpha(); if alterShownState then if toAlpha > 0 then frame:Show(); end if toAlpha == 0 then if not frame:IsShown() then frame:SetAlpha(0); alpha = 0; end if alpha == 0 then frame:Hide(); end end end if fromAlpha == toAlpha or alpha == toAlpha then if fadeInfo[frame] then fadeInfo[frame] = nil; end return; end local duration; if useConstantDuration then duration = fullDuration; else if fromAlpha then duration = fullDuration * (alpha - toAlpha)/(fromAlpha - toAlpha); else duration = fullDuration * abs(alpha - toAlpha); end end if duration <= 0 then frame:SetAlpha(toAlpha); if toAlpha == 0 then frame:Hide(); end return; end fadeInfo[frame] = { fromAlpha = alpha, toAlpha = toAlpha, duration = duration, timer = 0, alterShownState = alterShownState, }; for i = 1, #fadingFrames do if fadingFrames[i] == frame then return; end end tinsert(fadingFrames, frame); self:SetScript("OnUpdate", OnUpdate); end function f:SimpleFade(frame, toAlpha, alterShownState, speedMultiplier) --Use a constant fading speed: 1.0 in 0.25s --alterShownState: if true, run Frame:Hide() when alpha reaches zero / run Frame:Show() at the beginning speedMultiplier = speedMultiplier or 1; local alpha = frame:GetAlpha(); local duration = abs(alpha - toAlpha) * 0.25 * speedMultiplier; if duration <= 0 then return; end self:Add(frame, duration, alpha, toAlpha, alterShownState, true); end function f:Snap() local i = 1; local frame, info; while fadingFrames[i] do frame = fadingFrames[i]; info = fadeInfo[frame]; if info then frame:SetAlpha(info.toAlpha); end i = i + 1; end self:Clear(); end local function UIFrameFade(frame, duration, toAlpha, initialAlpha) if initialAlpha then frame:SetAlpha(initialAlpha); f:Add(frame, duration, initialAlpha, toAlpha, true, false); else f:Add(frame, duration, nil, toAlpha, true, false); end end local function UIFrameFadeIn(frame, duration) frame:SetAlpha(0); f:Add(frame, duration, 0, 1, true, false); end API.UIFrameFade = UIFrameFade; --from current alpha API.UIFrameFadeIn = UIFrameFadeIn; --from 0 to 1 end do -- Map ---- Create Module that will be activated in specific zones: ---- 1. Soridormi Auto-report ---- 2. Dreamseed local C_Map = C_Map; local GetMapInfo = C_Map.GetMapInfo; local GetBestMapForUnit = C_Map.GetBestMapForUnit; local CreateVector2D = CreateVector2D; local controller; local modules; local lastMapID, total; local ZoneTriggeredModuleMixin = {}; ZoneTriggeredModuleMixin.validMaps = {}; ZoneTriggeredModuleMixin.inZone = false; ZoneTriggeredModuleMixin.enabled = true; local function DoNothing(arg) end ZoneTriggeredModuleMixin.enterZoneCallback = DoNothing; ZoneTriggeredModuleMixin.leaveZoneCallback = DoNothing; function ZoneTriggeredModuleMixin:IsZoneValid(uiMapID) return self.validMaps[uiMapID] end function ZoneTriggeredModuleMixin:SetValidZones(...) self.validMaps = {}; local map; for i = 1, select("#", ...) do map = select(i, ...); if type(map) == "table" then for _, uiMapID in ipairs(map) do self.validMaps[uiMapID] = true; end else self.validMaps[map] = true; end end end function ZoneTriggeredModuleMixin:PlayerEnterZone(mapID) if not self.inZone then self.inZone = true; self.enterZoneCallback(mapID); end if mapID ~= self.currentMapID then self.currentMapID = mapID; self:OnCurrentMapChanged(mapID); end end function ZoneTriggeredModuleMixin:PlayerLeaveZone() if self.inZone then self.inZone = false; self.currentMapID = nil; self.leaveZoneCallback(); end end function ZoneTriggeredModuleMixin:SetEnterZoneCallback(callback) self.enterZoneCallback = callback; end function ZoneTriggeredModuleMixin:SetLeaveZoneCallback(callback) self.leaveZoneCallback = callback; end function ZoneTriggeredModuleMixin:OnCurrentMapChanged(newMapID) end function ZoneTriggeredModuleMixin:SetEnabled(state) self.enabled = state or false; if not self.enabled then self:PlayerLeaveZone(); end end function ZoneTriggeredModuleMixin:Update() if not self.enabled then return end; local mapID = GetBestMapForUnit("player"); if self:IsZoneValid(mapID) then self:PlayerEnterZone(mapID); else self:PlayerLeaveZone(); end end local function AddZoneModules(module) if not controller then controller = CreateFrame("Frame"); modules = {}; total = 0; controller:SetScript("OnEvent", function(f, event, ...) local mapID = GetBestMapForUnit("player"); if mapID and mapID ~= lastMapID then lastMapID = mapID; else return end for i = 1, total do if modules[i].enabled then if modules[i]:IsZoneValid(mapID) then modules[i]:PlayerEnterZone(mapID); else modules[i]:PlayerLeaveZone(); end end end end); --Note: if the player enters an unmapped area like The Great Sea then re-enter a regular zone --GetBestMapForUnit will still return the continent mapID when ZONE_CHANGED_NEW_AREA triggers controller:RegisterEvent("ZONE_CHANGED_NEW_AREA"); controller:RegisterEvent("PLAYER_ENTERING_WORLD"); end table.insert(modules, module); total = total + 1; end local function CreateZoneTriggeredModule(tag) local module = { tag = tag, validMaps = {}, }; for k, v in pairs(ZoneTriggeredModuleMixin) do module[k] = v; end AddZoneModules(module); return module end API.CreateZoneTriggeredModule = CreateZoneTriggeredModule; --Get Player Coord Less RAM cost local UnitPosition = UnitPosition; local GetPlayerMapPosition = C_Map.GetPlayerMapPosition; local _posY, _posX, _data; local lastUiMapID; local MapData = {}; local function CacheMapData(uiMapID) if MapData[uiMapID] then return end; local instance, topLeft = C_Map.GetWorldPosFromMapPos(uiMapID, {x=0, y=0}); local width, height = C_Map.GetMapWorldSize(uiMapID); if topLeft then local top, left = topLeft:GetXY() MapData[uiMapID] = {width, height, left, top}; end end local function GetPlayerMapCoord_Fallback(uiMapID) local position = GetPlayerMapPosition(uiMapID, "player"); if position then return position.x, position.Y end end local function GetPlayerMapCoord(uiMapID) _posY, _posX = UnitPosition("player"); if not (_posX and _posY) then return GetPlayerMapCoord_Fallback(uiMapID) end; if uiMapID ~= lastUiMapID then lastUiMapID = uiMapID; CacheMapData(uiMapID); end _data = MapData[uiMapID] if not _data or _data[1] == 0 or _data[2] == 0 then return GetPlayerMapCoord_Fallback(uiMapID) end; return (_data[3] - _posX) / _data[1], (_data[4] - _posY) / _data[2] end API.GetPlayerMapCoord = GetPlayerMapCoord; local function ConvertMapPositionToContinentPosition(uiMapID, x, y, poiID) local info = GetMapInfo(uiMapID); if not info then return end; local continentMapID; --uiMapID while info do if info.mapType == Enum.UIMapType.Continent then continentMapID = info.mapID; break elseif info.parentMapID then info = GetMapInfo(info.parentMapID); else return end end if not continentMapID then print(string.format("Map %s doesn't belong to any continent.", uiMapID)); end local point = { uiMapID = uiMapID, position = CreateVector2D(x, y); }; C_Map.SetUserWaypoint(point); C_Timer.After(0, function() local posVector = C_Map.GetUserWaypointPositionForMap(continentMapID); if posVector then x, y = posVector:GetXY(); print(continentMapID, x, y); if not PlumberDevData then PlumberDevData = {}; end if not PlumberDevData.POIPositions then PlumberDevData.POIPositions = {}; end if poiID then x = API.RoundCoord(x); y = API.RoundCoord(y); PlumberDevData.POIPositions[poiID] = { poiID = poiID, uiMapID = uiMapID, continent = continentMapID, x = x, y = y, }; end C_Map.ClearUserWaypoint(); else print("No user waypoint found.") end end); end API.ConvertMapPositionToContinentPosition = ConvertMapPositionToContinentPosition; function API.GetPlayerMap() return GetBestMapForUnit("player"); end --Calculate a list of map positions (cache data) and run callback local Converter; local function Converter_OnUpdate(self, elapsed) self.t = self.t + elapsed; if self.t > self.delay then if self.t > 1 then --The delay is always much shorter than 1s, thie line is to prevent error looping self.t = nil; self:SetScript("OnUpdate", nil); return end self.t = 0; else return end self.index = self.index + 1; if self.calls[self.index] then self.calls[self.index](); else self:SetScript("OnUpdate", nil); self.t = nil; self.calls = nil; self.index = nil; self.oldWaypoint = nil; if self.onFinished then self.onFinished(); self.onFinished = nil; end end end local function ConvertAndCacheMapPositions(positions, onCoordReceivedFunc, onFinishedFunc) --Convert Zone position to Continent position if not Converter then Converter = CreateFrame("Frame"); print("Plumber Request ConvertAndCacheMapPositions"); end local MAPTYPE_CONTINENT = Enum.UIMapType.Continent; if not MAPTYPE_CONTINENT then print("Plumber WoW API Changed"); return end local calls, n, oldWaypoint; if Converter.t then --still processing calls = Converter.calls; n = #calls; oldWaypoint = Converter.oldWaypoint; else calls = {}; n = 0; oldWaypoint = C_Map.GetUserWaypoint(); Converter.oldWaypoint = oldWaypoint; Converter.index = 0; end for _, data in ipairs(positions) do local info = GetMapInfo(data.uiMapID); if info then local continentMapID; --uiMapID while info do if info.mapType == MAPTYPE_CONTINENT then continentMapID = info.mapID; break elseif info.parentMapID then info = GetMapInfo(info.parentMapID); else info = nil; end end if continentMapID then local uiMapID = data.uiMapID; local poiID = data.poiID; local point = { uiMapID = uiMapID, position = CreateVector2D(data.x, data.y); }; n = n + 1; local function SetWaypoint() C_Map.SetUserWaypoint(point); Converter.t = 0; end calls[n] = SetWaypoint; n = n + 1; local function ProcessWaypoint() local posVector = C_Map.GetUserWaypointPositionForMap(continentMapID); if posVector then local x, y = posVector:GetXY(); local positionData = { uiMapID = uiMapID, continent = continentMapID, x = API.RoundCoord(x), y = API.RoundCoord(y), poiID = poiID, }; onCoordReceivedFunc(positionData) C_Map.ClearUserWaypoint(); --Debug Save Position --[[ if not PlumberDevData then PlumberDevData = {}; end if not PlumberDevData.Waypoints then PlumberDevData.Waypoints = {}; end PlumberDevData.Waypoints[poiID] = positionData; --]] end Converter.t = 0.033; end calls[n] = ProcessWaypoint; end end end Converter.onFinished = function() if Converter.oldWaypoint then C_Map.SetUserWaypoint(oldWaypoint); Converter.oldWaypoint = nil; end if onFinishedFunc then onFinishedFunc(); end end Converter.calls = calls; Converter.t = 0; Converter.delay = -0.1; Converter:SetScript("OnUpdate", Converter_OnUpdate); return true end API.ConvertAndCacheMapPositions = ConvertAndCacheMapPositions; --[[ function YeetPos() local uiMapID = C_Map.GetBestMapForUnit("player"); local x, y = GetPlayerMapCoord(uiMapID); print(x, y); local position = C_Map.GetPlayerMapPosition(uiMapID, "player"); local x0, y0 = position:GetXY(); print(x0, y0); end --]] local MARGIN_X = 0.02; local MARGIN_Y = MARGIN_X * 1.42; local function AreWaypointsClose(userX, userY, preciseX, preciseY) --Examine if the left coords (user set) is roughly the same as the precise position --We don't calculate the exact distance (e.g. in yards) --We assume the user waypoint falls into a square around around their target, cuz manually placed pin cannot reach that precision --The margin of Y is larger than that of X, due to map ratio return (userX > preciseX - MARGIN_X) and (userX < preciseX + MARGIN_X) and (userY > preciseY - MARGIN_Y) and (userY < preciseY + MARGIN_Y) end API.AreWaypointsClose = AreWaypointsClose; local MAP_PIN_HYPERLINK = MAP_PIN_HYPERLINK or "|A:Waypoint-MapPin-ChatIcon:13:13:0:0|a Map Pin Location"; local FORMAT_USER_WAYPOINT = "|cffffff00|Hworldmap:%d:%.0f:%.0f|h["..MAP_PIN_HYPERLINK.."]|h|r"; --Message will be blocked by the server if you changing the map pin's name local function CreateWaypointHyperlink(uiMapID, normalizedX, normalizedY) if uiMapID and normalizedX and normalizedY then return format(FORMAT_USER_WAYPOINT, uiMapID, 10000*normalizedX, 10000*normalizedY); end end API.CreateWaypointHyperlink = CreateWaypointHyperlink; local function GetZoneName(areaID) return C_Map.GetAreaInfo(areaID) or ("Area:"..areaID) end API.GetZoneName = GetZoneName; local HasActiveDelve = C_DelvesUI and C_DelvesUI.HasActiveDelve or Nop; local function IsInDelves() --See Blizzard InstanceDifficulty.lua local _, _, _, mapID = UnitPosition("player"); return HasActiveDelve(mapID); end API.IsInDelves = IsInDelves; function API.GetMapName(uiMapID) local info = GetMapInfo(uiMapID); if info then return info.name end end end do -- Instance -- Map local GetInstanceInfo = GetInstanceInfo; local function GetMapID() local instanceID = select(8, GetInstanceInfo()); return instanceID end API.GetMapID = GetMapID; end do -- Pixel local GetPhysicalScreenSize = GetPhysicalScreenSize; local function GetPixelForScale(scale, pixelSize) local SCREEN_WIDTH, SCREEN_HEIGHT = GetPhysicalScreenSize(); if pixelSize then return pixelSize * (768/SCREEN_HEIGHT)/scale else return (768/SCREEN_HEIGHT)/scale end end API.GetPixelForScale = GetPixelForScale; local function GetPixelForWidget(widget, pixelSize) local scale = widget:GetEffectiveScale(); return GetPixelForScale(scale, pixelSize); end API.GetPixelForWidget = GetPixelForWidget; end do -- Easing local EasingFunctions = {}; addon.EasingFunctions = EasingFunctions; local sin = math.sin; local cos = math.cos; local pow = math.pow; local pi = math.pi; --t: total time elapsed --b: beginning position --e: ending position --d: animation duration function EasingFunctions.linear(t, b, e, d) return (e - b) * t / d + b end function EasingFunctions.outSine(t, b, e, d) return (e - b) * sin(t / d * (pi / 2)) + b end function EasingFunctions.inOutSine(t, b, e, d) return -(e - b) / 2 * (cos(pi * t / d) - 1) + b end function EasingFunctions.outQuart(t, b, e, d) t = t / d - 1; return (b - e) * (pow(t, 4) - 1) + b end function EasingFunctions.outQuint(t, b, e, d) t = t / d return (b - e)* (pow(1 - t, 5) - 1) + b end function EasingFunctions.inQuad(t, b, e, d) t = t / d return (e - b) * pow(t, 2) + b end end do -- Currency local GetCurrencyInfo = C_CurrencyInfo.GetCurrencyInfo; local CurrencyDataProvider = CreateFrame("Frame"); CurrencyDataProvider.cache = {}; CurrencyDataProvider.icons = {}; local RelevantKeys = {"name", "quantity", "iconFileID", "maxQuantity", "quality"}; CurrencyDataProvider:SetScript("OnEvent", function(self, event, currencyID, quantity, quantityChange) if currencyID and self.cache[currencyID] then self.cache[currencyID] = nil; end end); function CurrencyDataProvider:CacheAndGetCurrencyInfo(currencyID) if not self.cache[currencyID] then local info = GetCurrencyInfo(currencyID); if not info then return end; local vital = {}; end if not self.registered then self.registered = true; self:RegisterEvent("CURRENCY_DISPLAY_UPDATE"); end return self.cache[currencyID] end function CurrencyDataProvider:GetIcon(currencyID) if not self.icons[currencyID] then self:CacheAndGetCurrencyInfo(currencyID); end end local IGNORED_OVERFLOW_ID = { [3068] = true, --Delver's Journey [3143] = true, --Delver's Journey }; local function WillCurrencyRewardOverflow(currencyID, rewardQuantity) if IGNORED_OVERFLOW_ID[currencyID] then return false, 0 end local currencyInfo = GetCurrencyInfo(currencyID); local quantity = currencyInfo and (currencyInfo.useTotalEarnedForMaxQty and currencyInfo.totalEarned or currencyInfo.quantity); local overflow = quantity and currencyInfo.maxQuantity > 0 and rewardQuantity + quantity > currencyInfo.maxQuantity; return overflow, quantity, currencyInfo.useTotalEarnedForMaxQty, currencyInfo.maxQuantity end API.WillCurrencyRewardOverflow = WillCurrencyRewardOverflow; local CoinUtil = {}; addon.CoinUtil = CoinUtil; CoinUtil.patternGold = L["Match Pattern Gold"]; CoinUtil.patternSilver = L["Match Pattern Silver"]; CoinUtil.patternCopper = L["Match Pattern Copper"]; function CoinUtil:GetCopperFromCoinText(coinText) local rawCopper = 0; local gold = match(coinText, self.patternGold); local silver = match(coinText, self.patternSilver); local copper = match(coinText, self.patternCopper); if gold then rawCopper = rawCopper + 10000 * (tonumber(gold) or 0); end if silver then rawCopper = rawCopper + 100 * (tonumber(silver) or 0); end if copper then rawCopper = rawCopper + (tonumber(copper) or 0); end return rawCopper end end do -- Chat Message --Check if the a rare spawn info has been announced by other players local function SearchChatHistory(searchFunc) local pool = ChatFrame1 and ChatFrame1.fontStringPool; if pool then local text, uiMapID, x, y; for fontString in pool:EnumerateActive() do text = fontString:GetText(); if text ~= nil then if searchFunc(text) then return true end end end end return false end API.SearchChatHistory = SearchChatHistory; end do -- Cursor Position local UI_SCALE_RATIO = 1; local UIParent = UIParent; local EL = CreateFrame("Frame"); local GetCursorPosition = GetCursorPosition; EL:RegisterEvent("UI_SCALE_CHANGED"); EL:SetScript("OnEvent", function(self, event) UI_SCALE_RATIO = 1 / UIParent:GetEffectiveScale(); end); local function GetScaledCursorPosition() local x, y = GetCursorPosition(); return x*UI_SCALE_RATIO, y*UI_SCALE_RATIO end API.GetScaledCursorPosition = GetScaledCursorPosition; function API.GetScaledCursorPositionForFrame(frame) local uiScale = frame:GetEffectiveScale(); local x, y = GetCursorPosition(); return x / uiScale, y / uiScale; end end do -- TomTom Compatibility local TomTomUtil = {}; addon.TomTomUtil = TomTomUtil; TomTomUtil.waypointUIDs = {}; TomTomUtil.pauseCrazyArrowUpdate = false; local TT; function TomTomUtil:IsTomTomAvailable() if self.available == nil then self.available = (TomTom and TomTom.AddWaypoint and TomTom.RemoveWaypoint and TomTom.SetClosestWaypoint and TomTomCrazyArrow and true) or false if self.available then TT = TomTom; end end return self.available end function TomTomUtil:AddWaypoint(uiMapID, x, y, desc, plumberTag, plumberArg1, plumberArg2) --x, y: 0-1 if self:IsTomTomAvailable() then plumberTag = plumberTag or "plumber"; local opts = { title = desc or "TomTom Waypoint via Plumber", from = "Plumber", persistent = false, --waypoint will not be saved crazy = true, cleardistance = 8, arrivaldistance = 15, world = false, --don't show on WorldMap minimap = false, plumberTag = plumberTag, plumberArg1 = plumberArg1, plumberArg2 = plumberArg2, }; local uid = securecallfunction(TT.AddWaypoint, TT, uiMapID, x, y, opts); if uid then if not self.waypointUIDs[uid] then self.waypointUIDs[uid] = {plumberTag, plumberArg1, plumberArg2}; end return uid else return end end end function TomTomUtil:SelectClosestWaypoint() local announceInChat = false; securecallfunction(TT.SetClosestWaypoint, TT, announceInChat); end function TomTomUtil:RemoveWaypoint(uid) if self:IsTomTomAvailable() then securecallfunction(TT.RemoveWaypoint, TT, uid); end end function TomTomUtil:RemoveWaypointsByTag(tag) for uid, data in pairs(self.waypointUIDs) do if data[1] == tag then self.waypointUIDs[uid] = nil; self:RemoveWaypoint(uid); end end end function TomTomUtil:RemoveWaypointsByRule(rule) for uid, data in pairs(self.waypointUIDs) do if rule(unpack(data)) then self.waypointUIDs[uid] = nil; self:RemoveWaypoint(uid); end end end function TomTomUtil:RemoveAllPlumberWaypoints() for uid, tag in pairs(self.waypointUIDs) do self:RemoveWaypoint(uid); end self.waypointUIDs = {}; end function TomTomUtil:GetDistanceToWaypoint(uid) return securecallfunction(TT.GetDistanceToWaypoint, TT, uid) end end do -- Game UI local function IsInEditMode() return EditModeManagerFrame and EditModeManagerFrame:IsEditModeActive(); end API.IsInEditMode = IsInEditMode; end do -- Reputation local C_Reputation = C_Reputation; local C_MajorFactions = C_MajorFactions; local GetFriendshipReputation = C_GossipInfo.GetFriendshipReputation; local GetFriendshipReputationRanks = C_GossipInfo.GetFriendshipReputationRanks; local GetFactionParagonInfo = C_Reputation.GetFactionParagonInfo or Nop; local UnitSex = UnitSex; local GetText = GetText; local GetFactionDataByID; if C_Reputation.GetFactionDataByID then GetFactionDataByID = C_Reputation.GetFactionDataByID; else --Classic function GetFactionDataByID(factionID) local name, description, standingID, barMin, barMax, barValue = GetFactionInfoByID(factionID); if name then local tbl = { name = name, factionID = factionID, reaction = standingID, currentStanding = barValue, currentReactionThreshold = barMin, nextReactionThreshold = barMax, } return tbl end end end local function GetReputationProgress(factionID) if not factionID then return end; local level, isFull, currentValue, maxValue, name, reputationType, isUnlocked, reaction; local repInfo = GetFriendshipReputation(factionID); local paragonRepEarned, paragonThreshold, rewardQuestID, hasRewardPending = GetFactionParagonInfo(factionID); if repInfo and repInfo.friendshipFactionID and repInfo.friendshipFactionID > 0 then reputationType = 2; name = repInfo.name; reaction = repInfo.reaction; isUnlocked = true; if repInfo.nextThreshold then currentValue = repInfo.standing - repInfo.reactionThreshold; maxValue = repInfo.nextThreshold - repInfo.reactionThreshold; if maxValue == 0 then currentValue = 1; maxValue = 1; end else currentValue = 1; maxValue = 1; end local rankInfo = GetFriendshipReputationRanks(repInfo.friendshipFactionID); level = rankInfo.currentLevel; isFull = level >= rankInfo.maxLevel; end if C_Reputation.IsMajorFaction and C_Reputation.IsMajorFaction(factionID) then local majorFactionData = C_MajorFactions.GetMajorFactionData(factionID); if majorFactionData then reputationType = 3; maxValue = majorFactionData.renownLevelThreshold; local isCapped = C_MajorFactions.HasMaximumRenown(factionID); currentValue = isCapped and majorFactionData.renownLevelThreshold or majorFactionData.renownReputationEarned or 0; level = majorFactionData.renownLevel; name = majorFactionData.name; isUnlocked = majorFactionData.isUnlocked; end end if not reputationType then repInfo = GetFactionDataByID(factionID); if repInfo then reputationType = 1; name = repInfo.name; isUnlocked = true; if repInfo.currentReactionThreshold then currentValue = repInfo.currentStanding - repInfo.currentReactionThreshold; maxValue = repInfo.nextReactionThreshold - repInfo.currentReactionThreshold; if maxValue == 0 then currentValue = 1; maxValue = 1; end else currentValue = 1; maxValue = 1; end local zeroLevel = 4; --Neutral reaction = repInfo.reaction; level = reaction - zeroLevel; isFull = level >= 8; --TEMP DEBUG end end if C_Reputation.IsFactionParagon and C_Reputation.IsFactionParagon(factionID) then isFull = true; if paragonRepEarned and paragonThreshold and paragonThreshold ~= 0 then local paragonLevel = floor(paragonRepEarned / paragonThreshold); currentValue = paragonRepEarned - paragonLevel * paragonThreshold; maxValue = paragonThreshold; level = paragonLevel; end end if reputationType then local tbl = { level = level, currentValue = currentValue, maxValue = maxValue, isFull = isFull, name = name, reputationType = reputationType, --1:Standard, 2:Friendship rewardPending = hasRewardPending, isUnlocked = isUnlocked, reaction = reaction, }; return tbl end end API.GetReputationProgress = GetReputationProgress; local function GetParagonValuesAndLevel(factionID) local totalEarned, threshold = GetFactionParagonInfo(factionID); if totalEarned and threshold and threshold ~= 0 then local paragonLevel = floor(totalEarned / threshold); --How many times the player has reached paragon local currentValue = totalEarned - paragonLevel * threshold; return currentValue, threshold, paragonLevel end return 0, 1, 0 end API.GetParagonValuesAndLevel = GetParagonValuesAndLevel; local function GetReputationStandingText(reaction) if type(reaction) == "string" then --Friendship return reaction end local gender = UnitSex("player"); local reputationStandingtext = GetText("FACTION_STANDING_LABEL"..reaction, gender); --GetText: Game API that returns localized texts return reputationStandingtext end API.GetReputationStandingText = GetReputationStandingText; local function GetFactionStatusText(factionID, simplified) --Derived from Blizzard ReputationFrame_InitReputationRow in ReputationFrame.lua if not factionID then return end; local factionName; local p1, description, standingID, barMin, barMax, barValue = GetFactionDataByID(factionID); if type(p1) == "table" then standingID = p1.reaction; barMin = p1.currentReactionThreshold; barMax = p1.nextReactionThreshold; barValue = p1.currentStanding; factionName = p1.name; else factionName = p1; end local isParagon = C_Reputation.IsFactionParagon and C_Reputation.IsFactionParagon(factionID); local isMajorFaction = C_Reputation.IsMajorFaction and C_Reputation.IsMajorFaction(factionID); local repInfo = GetFriendshipReputation(factionID); local isCapped; local factionStandingtext; --Revered/Junior/Renown 1 local cappedAlert; local isFriendship; if repInfo and repInfo.friendshipFactionID > 0 then --Friendship isFriendship = true; factionStandingtext = repInfo.reaction; if repInfo.nextThreshold then barMin, barMax, barValue = repInfo.reactionThreshold, repInfo.nextThreshold, repInfo.standing; else barMin, barMax, barValue = 0, 1, 1; isCapped = true; end local rankInfo = GetFriendshipReputationRanks(repInfo.friendshipFactionID); if rankInfo then factionStandingtext = factionStandingtext .. format(" (Lv. %s/%s)", rankInfo.currentLevel, rankInfo.maxLevel); end elseif isMajorFaction then local majorFactionData = C_MajorFactions.GetMajorFactionData(factionID); if majorFactionData then barMin, barMax = 0, majorFactionData.renownLevelThreshold; isCapped = C_MajorFactions.HasMaximumRenown(factionID); barValue = isCapped and majorFactionData.renownLevelThreshold or majorFactionData.renownReputationEarned or 0; factionStandingtext = L["Renown Level Label"] .. majorFactionData.renownLevel; if isParagon then local totalEarned, threshold, rewardQuestID, hasRewardPending = GetFactionParagonInfo(factionID); if totalEarned and threshold and threshold ~= 0 then local paragonLevel = floor(totalEarned / threshold); local currentValue = totalEarned - paragonLevel * threshold; factionStandingtext = ("|cff00ccff"..L["Paragon Reputation"].."|r %d/%d"):format(currentValue, threshold); end if hasRewardPending then cappedAlert = "|cffff4800"..L["Unclaimed Reward Alert"].."|r"; end else if isCapped then factionStandingtext = factionStandingtext.." "..L["Level Maxed"]; end end end elseif (standingID and standingID > 0) then isCapped = standingID == 8; --MAX_REPUTATION_REACTION factionStandingtext = GetReputationStandingText(standingID); end local rolloverText; --(0/24000) if barMin and barValue and barMax and (not isCapped) then rolloverText = format("(%s/%s)", barValue - barMin, barMax - barMin); if simplified then factionStandingtext = isFriendship and repInfo.reaction or factionStandingtext or ""; return (factionStandingtext.." "..rolloverText), factionName end end local text; if factionStandingtext then if not text then text = L["Current Colon"] end; factionStandingtext = " |cffffffff"..factionStandingtext.."|r"; text = text .. factionStandingtext; end if rolloverText then if not text then text = L["Current Colon"] end; rolloverText = " |cffffffff"..rolloverText.."|r"; text = text .. rolloverText; end if text then text = " \n"..text; if cappedAlert then text = text.."\n"..cappedAlert; end end return text, factionName end API.GetFactionStatusText = GetFactionStatusText; local function GetReputationChangeFromText(text) local name, amount; name, amount = match(text, L["Match Pattern Rep 1"]); if not name then name, amount = match(text, L["Match Pattern Rep 2"]); end if name then if amount then amount = gsub(amount, ",", ""); amount = tonumber(amount); end return name, amount end end API.GetReputationChangeFromText = GetReputationChangeFromText; function API.GetMaxRenownLevel(factionID) local renownLevelsInfo = C_MajorFactions.GetRenownLevels(factionID); if renownLevelsInfo then return renownLevelsInfo[#renownLevelsInfo].level end end end do -- Spell local GetSpellInfo_Table = C_Spell.GetSpellInfo; local SPELL_INFO_KEYS = {"name", "rank", "iconID", "castTime", "minRange", "maxRange", "spellID", "originalIconID"}; local function GetSpellInfo_Flat(spellID) local info = spellID and GetSpellInfo_Table(spellID); if info then local tbl = {}; local n = 0; for _, key in ipairs(SPELL_INFO_KEYS) do n = n + 1; tbl[n] = info[key]; end return unpack(tbl) end end API.GetSpellInfo = GetSpellInfo_Flat; if C_Spell.GetSpellCooldown then API.GetSpellCooldown = C_Spell.GetSpellCooldown; else local GetSpellCooldown = GetSpellCooldown; function API.GetSpellCooldown(spell) local startTime, duration, isEnabled, modRate = GetSpellCooldown(spell); if startTime ~= nil then local tbl = { startTime = startTime, duration = duration, isEnabled = isEnabled, modRate = modRate, }; return tbl end end end if C_Spell.GetSpellCharges then API.GetSpellCharges = C_Spell.GetSpellCharges; else local GetSpellCharges = GetSpellCharges; function API.GetSpellCharges(spell) local currentCharges, maxCharges, cooldownStartTime, cooldownDuration, chargeModRate = GetSpellCharges(spell); if currentCharges then local tbl = { currentCharges = currentCharges, maxCharges = maxCharges, cooldownStartTime = cooldownStartTime, cooldownDuration = cooldownDuration, chargeModRate = chargeModRate, }; return tbl end end end end do -- System if true then --IS_TWW local GetMouseFoci = GetMouseFoci; local function GetMouseFocus() local objects = GetMouseFoci(); return objects and objects[1] end API.GetMouseFocus = GetMouseFocus; else API.GetMouseFocus = GetMouseFocus; end local ModifierKeyName = { LSHIFT = "Shift", LCTRL = "Ctrl", LALT = "Alt", }; if IsMacClient and IsMacClient() then --Mac OS ModifierKeyName.LCTRL = "Command"; ModifierKeyName.LALT = "Option"; end ModifierKeyName.RSHIFT = ModifierKeyName.LSHIFT; ModifierKeyName.RCTRL = ModifierKeyName.LCTRL; ModifierKeyName.RALT = ModifierKeyName.LALT; function API.GetModifierKeyName(key) if key and ModifierKeyName[key] then return ModifierKeyName[key] end end function API.HandleModifiedItemClick(link, itemLocation) if InCombatLockdown() then return false end; if IsModifiedClick("CHATLINK") then if ( ChatEdit_InsertLink(link) ) then return true elseif SocialPostFrame and Social_IsShown() then Social_InsertLink(link); return true end end if IsModifiedClick("DRESSUP") then if itemLocation then return DressUpItemLocation(itemLocation) end return DressUpLink(link) end end function API.ToggleBlizzardTokenUIIfWarbandCurrency(currencyID) if InCombatLockdown() then return end; local info = currencyID and C_CurrencyInfo.GetCurrencyInfo(currencyID); if not (info and info.isAccountTransferable) then return end; local onlyShow = false; --If true, don't hide the frame when shown ToggleCharacter("TokenFrame", onlyShow); end function API.AddButtonToAddonCompartment(identifier, name, icon, onClickFunc, onEnterFunc, onLeaveFunc) local f = AddonCompartmentFrame; if not f then return end; for index, addonData in ipairs(f.registeredAddons) do if addonData.identifier == identifier then return end end local addonData = { identifier = identifier, text = name, icon = icon, func = onClickFunc, funcOnEnter = onEnterFunc, funcOnLeave = onLeaveFunc, }; f:RegisterAddon(addonData) end function API.RemoveButtonFromAddonCompartment(identifier) local f = AddonCompartmentFrame; if not f then return end; for index, addonData in ipairs(f.registeredAddons) do if addonData.identifier == identifier then table.remove(f.registeredAddons, index); f:UpdateDisplay(); return end end end function API.TriggerExpansionMinimapButtonAlert(text) if ExpansionLandingPageMinimapButton then ExpansionLandingPageMinimapButton:TriggerAlert(text); end end function API.CloseBossBanner() local banner = BossBanner; if not banner then return end; banner:StopAnimating(); banner:Hide(); banner.lootShown = 0; banner.pendingLoot = {}; if banner.baseHeight then banner:SetHeight(banner.baseHeight); end if banner.LootFrames then for _, f in ipairs(banner.LootFrames) do f:Hide(); end end local textureKeys = { "BannerTop", "BannerBottom", "BannerMiddle", "BottomFillagree", "SkullSpikes", "RightFillagree", "LeftFillagree", "Title", "SubTitle", "FlashBurst", "FlashBurstLeft", "FlashBurstCenter", "RedFlash", }; for _, key in ipairs(textureKeys) do if banner[key] then banner[key]:SetAlpha(0); end end TopBannerManager_BannerFinished(); end end do -- Player local function GetPlayerMaxLevel() local serverExpansionLevel = GetServerExpansionLevel(); local maxLevel = GetMaxLevelForExpansionLevel(serverExpansionLevel); return maxLevel or 80 end API.GetPlayerMaxLevel = GetPlayerMaxLevel; local function IsPlayerAtMaxLevel() local maxLevel = GetPlayerMaxLevel(); local playerLevel = UnitLevel("player"); return playerLevel >= maxLevel end API.IsPlayerAtMaxLevel = IsPlayerAtMaxLevel; function API.IsGreatVaultFeatureAvailable() return IsPlayerAtMaxLevel() and C_WeeklyRewards ~= nil; end end do -- Scenario --[[ local SCENARIO_DELVES = addon.L["Scenario Delves"] or "Delves"; local GetScenarioInfo = C_ScenarioInfo.GetScenarioInfo; local function IsInDelves() local scenarioInfo = GetScenarioInfo(); return scenarioInfo and scenarioInfo.name == SCENARIO_DELVES end API.IsInDelves = IsInDelves; --]] end do -- ObjectPool local ObjectPoolMixin = {}; function ObjectPoolMixin:RemoveObject(obj) obj:Hide(); obj:ClearAllPoints(); if obj.OnRemoved then obj:OnRemoved(); end end function ObjectPoolMixin:RecycleObject(obj) local isActive; for i, activeObject in ipairs(self.activeObjects) do if activeObject == obj then tremove(self.activeObjects, i); isActive = true; break end end if isActive then self:RemoveObject(obj); self.numUnused = self.numUnused + 1; self.unusedObjects[self.numUnused] = obj; end end function ObjectPoolMixin:CreateObject() local obj = self.createObjectFunc(); tinsert(self.objects, obj); obj.Release = self.Object_Release; return obj end function ObjectPoolMixin:Acquire() local obj; if self.numUnused > 0 then obj = tremove(self.unusedObjects, self.numUnused); self.numUnused = self.numUnused - 1; end if not obj then obj = self:CreateObject(); end tinsert(self.activeObjects, obj); obj:Show(); return obj end function ObjectPoolMixin:ReleaseAll() if #self.activeObjects == 0 then return end; for _, obj in ipairs(self.activeObjects) do self:RemoveObject(obj); end self.activeObjects = {}; self.unusedObjects = {}; for index, obj in ipairs(self.objects) do self.unusedObjects[index] = obj; end self.numUnused = #self.objects; end function ObjectPoolMixin:GetTotalObjects() return #self.objects end function ObjectPoolMixin:CallAllObjects(method, ...) for i, obj in ipairs(self.objects) do obj[method](obj, ...); end end function ObjectPoolMixin:Object_Release() --Override end function ObjectPoolMixin:GetActiveObjects() return self.activeObjects end local function CreateObjectPool(createObjectFunc) local pool = {}; API.Mixin(pool, ObjectPoolMixin); local function Object_Release(f) pool:RecycleObject(f); end pool.Object_Release = Object_Release; pool.objects = {}; pool.activeObjects = {}; pool.unusedObjects = {}; pool.numUnused = 0; pool.createObjectFunc = createObjectFunc; return pool end API.CreateObjectPool = CreateObjectPool; end do -- Transmog local GetItemInfo = C_TransmogCollection.GetItemInfo; local PlayerKnowsSource = C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance; local function IsUncollectedTransmogByItemInfo(itemInfo) --C_TransmogCollection.PlayerHasTransmogByItemInfo isn't reliable local visualID, sourceID =GetItemInfo(itemInfo); if sourceID and sourceID ~= 0 and (not PlayerKnowsSource(sourceID)) then return true end end API.IsUncollectedTransmogByItemInfo = IsUncollectedTransmogByItemInfo if not addon.IsToCVersionEqualOrNewerThan(40000) then API.IsUncollectedTransmogByItemInfo = Nop; end end do -- Quest local GetRegularQuestTitle = C_QuestLog.GetTitleForQuestID or C_QuestLog.GetQuestInfo; local RequestLoadQuest = C_QuestLog.RequestLoadQuestByID or Nop; local GetLogIndexForQuestID = C_QuestLog.GetLogIndexForQuestID or GetQuestLogIndexByID or Nop; local function GetQuestName(questID) local questName = C_TaskQuest.GetQuestInfoByQuestID(questID); if not questName then questName = GetRegularQuestTitle(questID); end if questName and questName ~= "" then return questName else RequestLoadQuest(questID); end end API.GetQuestName = GetQuestName; function API.IsQuestRewardCached(questID) --We use this to query Faction Paragon rewards, so numQuestRewards should always > 0 --May be 0 during the first query local numQuestRewards = GetNumQuestLogRewards(questID); if numQuestRewards > 0 then local getterFunc = GetQuestLogRewardInfo; local itemName, itemTexture, quantity, quality, isUsable, itemID; for i = 1, numQuestRewards do itemName, itemTexture, quantity, quality, isUsable, itemID = getterFunc(i, questID); if not itemName then return false end end return true else return false end end if addon.IS_CLASSIC then --Classic function API.GetQuestProgressPercent(questID, asText) local value, max = 0, 0; local questLogIndex = questID and GetLogIndexForQuestID(questID); if questLogIndex and questLogIndex ~= 0 then local numObjectives = GetNumQuestLeaderBoards(questLogIndex); local text, objectiveType, finished, fulfilled, required; for objectiveIndex = 1, numObjectives do text, objectiveType, finished = GetQuestLogLeaderBoard(objectiveIndex, questLogIndex); --print(questID, GetQuestName(questID), numObjectives, finished, fulfilled, required) if objectiveType ~= "spell" and objectiveType ~= "log" then fulfilled, required = match(text, "(%d+)/(%d+)"); if not (fulfilled and required) then fulfilled = 0; required = 1; end if fulfilled > required then fulfilled = required; end value = value + fulfilled; max = max + required; end end else return end if max == 0 then value = 0; max = 1; end if asText then return floor(100 * value / max).."%" else return value / max end end function API.GetQuestProgressTexts(questID, hideFinishedObjectives) local questLogIndex = questID and GetLogIndexForQuestID(questID); if questLogIndex and questLogIndex ~= 0 then local texts = {}; if IsQuestComplete(questID) then texts[1] = QUEST_PROGRESS_TOOLTIP_QUEST_READY_FOR_TURN_IN or ("|cff20ff20"..L["Ready To Turn In Tooltip"].."|r"); return texts end local numObjectives = GetNumQuestLeaderBoards(questLogIndex); local text, objectiveType, finished, fulfilled, required; for objectiveIndex = 1, numObjectives do text, objectiveType, finished = GetQuestLogLeaderBoard(objectiveIndex, questLogIndex); text = text or ""; if (objectiveType ~= "spell" and objectiveType ~= "log") and ((not finished) or not hideFinishedObjectives) then if finished then tinsert(texts, format("|cff808080- %s|r", text)); else tinsert(texts, format("- %s", text)); end end end return texts else if not C_QuestLog.IsOnQuest(questID) then local texts = {}; if C_QuestLog.IsQuestFlaggedCompleted(questID) then texts[1] = format("|cff808080%s|r", QUEST_COMPLETE); else texts[1] = format("|cffff2020%s|r", L["Not On Quest"]); local description = API.GetDescriptionFromTooltip(questID); if description and description ~= QUEST_TOOLTIP_REQUIREMENTS then tinsert(texts, " "); tinsert(texts, description); end end return texts; end end end else --Retail function API.GetQuestProgressPercent(questID, asText) --Unify progression text and bar --C_QuestLog.GetNumQuestObjectives local value, max = 0, 0; local questLogIndex = questID and GetLogIndexForQuestID(questID); if questLogIndex and questLogIndex ~= 0 then local numObjectives = GetNumQuestLeaderBoards(questLogIndex); local text, objectiveType, finished, fulfilled, required; for objectiveIndex = 1, numObjectives do text, objectiveType, finished, fulfilled, required = GetQuestObjectiveInfo(questID, objectiveIndex, false); --print(questID, GetQuestName(questID), numObjectives, finished, fulfilled, required) if fulfilled > required then fulfilled = required; end if objectiveType == "progressbar" then fulfilled = 0.01 * GetQuestProgressBarPercent(questID); required = 1; else if not finished then if fulfilled == required then --"Complete the scenario Nightfall" fulfilled = required = 1 when accepting the quest fulfilled = 0; end end end value = value + fulfilled; max = max + required; end else return end if max == 0 then value = 0; max = 1; end if asText then return floor(100 * value / max).."%" else return value / max end end function API.GetQuestProgressTexts(questID, hideFinishedObjectives) local questLogIndex = questID and GetLogIndexForQuestID(questID); if questLogIndex and questLogIndex ~= 0 then local texts = {}; if C_QuestLog.ReadyForTurnIn(questID) then texts[1] = QUEST_PROGRESS_TOOLTIP_QUEST_READY_FOR_TURN_IN; return texts end local numObjectives = GetNumQuestLeaderBoards(questLogIndex); local text, objectiveType, finished, fulfilled, required; for objectiveIndex = 1, numObjectives do text, objectiveType, finished, fulfilled, required = GetQuestObjectiveInfo(questID, objectiveIndex, false); text = text or ""; if (not finished) or not hideFinishedObjectives then if objectiveType == "progressbar" then fulfilled = GetQuestProgressBarPercent(questID); fulfilled = floor(fulfilled); if finished then tinsert(texts, format("|cff808080- %s%% %s|r", fulfilled, text)); else tinsert(texts, format("- %s", text)); end else if finished then tinsert(texts, format("|cff808080- %s|r", text)); else tinsert(texts, format("- %s", text)); end end end end return texts else if not C_QuestLog.IsOnQuest(questID) then local texts = {}; if C_QuestLog.IsQuestFlaggedCompleted(questID) then texts[1] = format("|cff808080%s|r", QUEST_COMPLETE); else texts[1] = format("|cffff2020%s|r", L["Not On Quest"]); local description = API.GetDescriptionFromTooltip(questID); if description and description ~= QUEST_TOOLTIP_REQUIREMENTS then tinsert(texts, " "); tinsert(texts, description); end end return texts; end end end end function API.GetQuestRewards(questID) --Ignore XP, Money --GetQuestLogRewardXP() local rewards; local missingData = false; local function SortFunc_QualityID(a, b) if a.quality ~= b.quality then return a.quality > b.quality end if a.id ~= b.id then return a.id > b.id end if a.quantity ~= b.quantity then return a.quantity > b.quantity end return true end if C_QuestLog.GetQuestRewardCurrencies and C_QuestInfoSystem.HasQuestRewardCurrencies(questID) then local currencies = {}; local currencyRewards = C_QuestLog.GetQuestRewardCurrencies(questID); local currencyID, quality; local info; for index, currencyReward in ipairs(currencyRewards) do currencyID = currencyReward.currencyID; quality = C_CurrencyInfo.GetCurrencyInfo(currencyID).quality; info = { name = currencyReward.name, texture = currencyReward.texture, quantity = currencyReward.totalRewardAmount, id = currencyID, questRewardContextFlags = currencyReward.questRewardContextFlags, quality = quality, }; tinsert(currencies, info); end table.sort(currencies, SortFunc_QualityID); if not rewards then rewards = {}; end rewards.currencies = currencies; if #currencyRewards == 0 then missingData = true; end elseif GetQuestLogRewardCurrencyInfo then local numCurrencies = GetNumQuestLogRewardCurrencies(questID) or 0; local name, texture, quantity, currencyID, quality; local currencies; for i = 1, numCurrencies do name, texture, quantity, currencyID, quality = GetQuestLogRewardCurrencyInfo(i, questID); if name then if not currencies then currencies = {}; end local info = { name = name, texture = texture, quantity = quantity, id = currencyID, questRewardContextFlags = 0, quality = quality, }; tinsert(currencies, info); else missingData = true; end end if currencies then table.sort(currencies, SortFunc_QualityID); if not rewards then rewards = {}; end rewards.currencies = currencies; end end if C_QuestInfoSystem.GetQuestRewardSpells and C_QuestInfoSystem.HasQuestRewardSpells(questID) then local spells = {}; local spellRewards = C_QuestInfoSystem.GetQuestRewardSpells(questID); local info; for index, spellID in ipairs(spellRewards) do info = C_QuestInfoSystem.GetQuestRewardSpellInfo(questID, spellID); info.id = spellID; tinsert(spells, info); end table.sort(spells, function(a, b) if a.id ~= b.id then return a.id > b.id end return true end ); if not rewards then rewards = {}; end rewards.spells = spells; end local numItems = GetNumQuestLogRewards(questID); if numItems > 0 then local items = {}; local name, texture, quantity, quality, isUsable, itemID, itemLevel; local info; for index = 1, numItems do name, texture, quantity, quality, isUsable, itemID, itemLevel = GetQuestLogRewardInfo(index, questID); if name and itemID then info = { name = name, texture = texture, quantity = quantity, quality = quality, id = itemID, }; tinsert(items, info); else missingData = true; end end table.sort(items, SortFunc_QualityID); if not rewards then rewards = {}; end rewards.items = items; end local honor = GetQuestLogRewardHonor(questID); if honor > 0 then if not rewards then rewards = {}; end rewards.honor = honor; end return rewards, missingData end --[[ function YeetQuestForMap(uiMapID) --Only contains quests with visible marker on the map if not uiMapID then uiMapID = C_Map.GetBestMapForUnit("player"); end local function PrintQuests(quests) if not quests then return end; local questID, name; for k, v in ipairs(quests) do questID = v.questID; name = GetQuestName(questID); if name then print(questID, name); else CallbackRegistry:LoadQuest(questID, function(_questID) print(questID, GetQuestName(questID)); end); end end end PrintQuests(C_TaskQuest.GetQuestsOnMap(uiMapID)); print(" "); PrintQuests(C_QuestLog.GetQuestsOnMap(uiMapID)); end --]] end do -- Tooltip if C_TooltipInfo then addon.TooltipAPI = C_TooltipInfo; else --For Classic where C_TooltipInfo doesn't exist: local TooltipAPI = {}; local CreateColor = CreateColor; local TOOLTIP_NAME = "PlumberClassicVirtualTooltip"; local TP = CreateFrame("GameTooltip", TOOLTIP_NAME, nil, "GameTooltipTemplate"); local UIParent = UIParent; TP:SetOwner(UIParent, 'ANCHOR_NONE'); TP:SetClampedToScreen(false); TP:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", 0, -128); TP:Show(); TP:SetScript("OnUpdate", nil); local UpdateFrame = CreateFrame("Frame"); local function UpdateTooltipInfo_OnUpdate(self, elapsed) self.t = self.t + elapsed; if self.t > 0.2 then self.t = 0; self:SetScript("OnUpdate", nil); addon.CallbackRegistry:Trigger("SharedTooltip.TOOLTIP_DATA_UPDATE", 0); end end function UpdateFrame:OnItemChanged(numLines) self.t = 0; self.numLines = numLines; self:SetScript("OnUpdate", UpdateTooltipInfo_OnUpdate); end local function GetTooltipHyperlink() local name, link = TP:GetItem(); if link then return link end name, link = TP:GetSpell(); if link then return "spell:"..link end end local function GetTooltipTexts() local numLines = TP:NumLines(); if numLines == 0 then return end; local tooltipData = {}; tooltipData.dataInstanceID = 0; local addItemLevel; local itemLink = GetTooltipHyperlink(); if itemLink then if itemLink ~= TP.hyperlink then UpdateFrame:OnItemChanged(numLines); end if API.IsEquippableItem(itemLink) then addItemLevel = API.GetItemLevel(itemLink); end end TP.hyperlink = itemLink; tooltipData.hyperlink = itemLink; local lines = {}; local n = 0; local fs, text; for i = 1, numLines do if i == 2 and addItemLevel then n = n + 1; lines[n] = { leftText = L["Format Item Level"]:format(addItemLevel); leftColor = CreateColor(1, 0.82, 0), }; end fs = _G[TOOLTIP_NAME.."TextLeft"..i]; if fs then n = n + 1; local r, g, b = fs:GetTextColor(); text = fs:GetText(); local lineData = { leftText = text, leftColor = CreateColor(r, g, b), rightText = nil, wrapText = true, leftOffset = 0, }; fs = _G[TOOLTIP_NAME.."TextRight"..i]; if fs then text = fs:GetText(); if text and text ~= "" then r, g, b = fs:GetTextColor(); lineData.rightText = text; lineData.rightColor = CreateColor(r, g, b); end end lines[n] = lineData; end end local sellPrice = API.GetItemSellPrice(itemLink); if sellPrice then n = n + 1; lines[n] = { leftText = "", --this will be ignored by our tooltip price = sellPrice, }; end tooltipData.lines = lines; return tooltipData end do local accessors = { SetItemByID = "GetItemByID", SetCurrencyByID = "GetCurrencyByID", SetQuestItem = "GetQuestItem", SetQuestCurrency = "GetQuestCurrency", SetSpellByID = "GetSpellByID", SetItemByGUID = "GetItemByGUID", SetHyperlink = "GetHyperlink", }; for accessor, getterName in pairs(accessors) do if TP[accessor] then local function GetterFunc(...) TP:ClearLines(); TP:SetOwner(UIParent, "ANCHOR_PRESERVE"); TP[accessor](TP, ...); return GetTooltipTexts(); end TooltipAPI[getterName] = GetterFunc; end end end addon.TooltipAPI = TooltipAPI; end local function SetTooltipWithPostCall(tooltip, tooltipPostCall, getterName, ...) local tooltipInfo = { getterName = getterName, getterArgs = { ... }; }; tooltipInfo.tooltipPostCall = tooltipPostCall; tooltip:ProcessInfo(tooltipInfo); end API.SetTooltipWithPostCall = SetTooltipWithPostCall; function API.GetDescriptionFromTooltip(questID) if questID then local hyperlink = "|Hquest:"..questID.."|h"; local data = addon.TooltipAPI.GetHyperlink(hyperlink); if data then return data.lines[3] and data.lines[3].leftText or nil end end end local PseudoTooltipInfoMixin = {}; do function PseudoTooltipInfoMixin:AddBlankLine() tinsert(self.tooltipData.lines, { leftText = " ", wrapText = true, }); end function PseudoTooltipInfoMixin:AddLine(text, r, g, b, wrapText) local color; if type(r) == "table" then color = r; else color = CreateColor(r, g, b); end tinsert(self.tooltipData.lines, { leftText = text, leftColor = color, wrapText = true, }); end function PseudoTooltipInfoMixin:AddDoubleLine(leftText, rightText, leftR, leftG, leftB, rightR, rightG, rightB) local leftColor, rightColor; if type(leftR) == "table" then leftColor = leftR; rightColor = leftG; else leftColor = CreateColor(leftR, leftG, leftB); rightColor = CreateColor(rightR, rightG, rightB); end tinsert(self.tooltipData.lines, { leftText = leftText, leftColor = leftColor, rightText = rightText, rightColor = rightColor, }); end end function API.CreateAppendTooltipInfo() local info = {}; info.append = true; info.tooltipData = {}; info.tooltipData.lines = {}; API.Mixin(info, PseudoTooltipInfoMixin); info:AddBlankLine(); return info end local TextureInfoTable = { width = 14, height = 14, margin = { left = 0, right = 4, top = 0, bottom = 0 }, texCoords = { left = 0.0625, right = 0.9375, top = 0.0625, bottom = 0.9375 }, }; function API.AddCraftingReagentToTooltip(tooltip, item, quantityRequired) local name = C_Item.GetItemNameByID(item) or ("item:"..item); local count = C_Item.GetItemCount(item, true, false, true, true); local icon = C_Item.GetItemIconByID(item); local rightText; local isRed; if quantityRequired then rightText = count.."/"..quantityRequired; isRed = count < quantityRequired; else rightText = count; end if isRed then tooltip:AddDoubleLine(name, rightText, 1, 0.125, 0.125, 1, 0.125, 0.125); else tooltip:AddDoubleLine(name, rightText, 1, 1, 1, 1, 1, 1); end tooltip:AddTexture(icon, TextureInfoTable); return true end end do -- AsyncCallback local AsyncCallback = CreateFrame("Frame"); --LoadQuestAPI is not available in 60 Classic --In this case we will run all callbacks when the time is up AsyncCallback.WoWAPI_LoadQuest = C_QuestLog.RequestLoadQuestByID; AsyncCallback.WoWAPI_LoadItem = C_Item.RequestLoadItemDataByID; AsyncCallback.WoWAPI_LoadSpell = C_Spell.RequestLoadSpellData; local CreatureNameCache = {}; function AsyncCallback:RunAllCallbacks(list) for id, callbacks in pairs(list) do for _, callbackInfo in ipairs(callbacks) do if (callbackInfo.oneTime and not callbackInfo.processed) or (callbackInfo.oneTime == false) then callbackInfo.processed = true; callbackInfo.func(id); end end end end function AsyncCallback:OnEvent(event, ...) local id, success = ... local list; if event == "QUEST_DATA_LOAD_RESULT" then list = self.questCallbacks; elseif event == "ITEM_DATA_LOAD_RESULT" then list = self.itemCallbacks; elseif event == "SPELL_DATA_LOAD_RESULT" then list = self.spellCallbacks; end if list and id and success then if list[id] then for _, callbackInfo in ipairs(list[id]) do if (callbackInfo.oneTime and not callbackInfo.processed) or (callbackInfo.oneTime == false) then callbackInfo.processed = true; callbackInfo.func(id); end end end end self.t = 0; end AsyncCallback:SetScript("OnEvent", AsyncCallback.OnEvent); function AsyncCallback:OnUpdate(elapsed) self.t = self.t + elapsed; if self.t > 0.5 then self.t = nil; self:SetScript("OnUpdate", nil); if self.questCallbacks then if self.LoadQuest then self:UnregisterEvent("QUEST_DATA_LOAD_RESULT"); end if self.runCallbackAfter then self:RunAllCallbacks(self.questCallbacks); end self.questCallbacks = nil; end if self.itemCallbacks then if self.LoadItem then self:UnregisterEvent("ITEM_DATA_LOAD_RESULT"); end self:RunAllCallbacks(self.itemCallbacks); self.itemCallbacks = nil; end if self.spellCallbacks then if self.LoadSpell then self:UnregisterEvent("SPELL_DATA_LOAD_RESULT"); end self:RunAllCallbacks(self.spellCallbacks); self.spellCallbacks = nil; end if self.creatureCallbacks then for id, callbacks in pairs(self.creatureCallbacks) do local name = API.GetCreatureName(id); if name and name ~= "" then CreatureNameCache[id] = name; else name = nil; end if name then for _, callbackInfo in ipairs(callbacks) do if not callbackInfo.processed then callbackInfo.processed = true; callbackInfo.func(id, name); end end end end self.creatureCallbacks = nil; end end end function AsyncCallback:AddCallback(key, id, callback, oneTime) if not self[key] then self[key] = {}; end if not self[key][id] then self[key][id] = {}; end if oneTime == nil then oneTime = true; end local callbackInfo = { func = callback, oneTime = oneTime, processed = false, }; tinsert(self[key][id], callbackInfo); end function CallbackRegistry:LoadQuest(id, callback, oneTime) AsyncCallback:AddCallback("questCallbacks", id, callback, oneTime); if AsyncCallback.WoWAPI_LoadQuest then AsyncCallback:RegisterEvent("QUEST_DATA_LOAD_RESULT"); AsyncCallback.WoWAPI_LoadQuest(id); else AsyncCallback.runCallbackAfter = true; end AsyncCallback.t = 0; AsyncCallback:SetScript("OnUpdate", AsyncCallback.OnUpdate); end function CallbackRegistry:LoadItem(id, callback, oneTime) AsyncCallback:AddCallback("itemCallbacks", id, callback, oneTime); if AsyncCallback.WoWAPI_LoadItem then AsyncCallback:RegisterEvent("ITEM_DATA_LOAD_RESULT"); AsyncCallback.WoWAPI_LoadItem(id); else AsyncCallback.runCallbackAfter = true; end AsyncCallback.t = 0; AsyncCallback:SetScript("OnUpdate", AsyncCallback.OnUpdate); end function CallbackRegistry:LoadSpell(id, callback, oneTime) AsyncCallback:AddCallback("spellCallbacks", id, callback, oneTime); if AsyncCallback.WoWAPI_LoadSpell then AsyncCallback:RegisterEvent("SPELL_DATA_LOAD_RESULT"); AsyncCallback.WoWAPI_LoadSpell(id); else AsyncCallback.runCallbackAfter = true; end AsyncCallback.t = 0; AsyncCallback:SetScript("OnUpdate", AsyncCallback.OnUpdate); end function CallbackRegistry:LoadCreature(id, callback) --Usually used to get npc name AsyncCallback:AddCallback("creatureCallbacks", id, callback); AsyncCallback.t = 0; AsyncCallback:SetScript("OnUpdate", AsyncCallback.OnUpdate); end function API.GetAndCacheCreatureName(creatureID) if CreatureNameCache[creatureID] then return CreatureNameCache[creatureID] end local name = API.GetCreatureName(creatureID); if name and name ~= "" then CreatureNameCache[creatureID] = name; else name = nil; end return name end end do -- Container Item Processor local GetItemCount = C_Item.GetItemCount; local GetContainerNumSlots = C_Container.GetContainerNumSlots; local GetContainerItemID = C_Container.GetContainerItemID; local GetItemInfoInstant = C_Item.GetItemInfoInstant; local GetBagItem = C_TooltipInfo and C_TooltipInfo.GetBagItem; local function GetItemBagPosition(itemID) local count = GetItemCount(itemID); --unused arg2: Include banks if count and count > 0 then for bagID = 0, 4 do for slotID = 1, GetContainerNumSlots(bagID) do if(GetContainerItemID(bagID, slotID) == itemID) then return bagID, slotID end end end end end API.GetItemBagPosition = GetItemBagPosition; local Processor = CreateFrame("Frame"); local ITEM_OPENABLE = ITEM_OPENABLE or ""; local OPENABLE_ITEM = {}; function Processor:OnUpdate_Queue(elapsed) self.t = self.t + elapsed; if self.t > 0.1 then self.t = 0; self:SetScript("OnUpdate", nil); local itemID; local anyMatch; for bagID = 0, 4 do for slotID = 1, GetContainerNumSlots(bagID) do itemID = GetContainerItemID(bagID, slotID); if self.queue[itemID] ~= nil then if self.queue[itemID].bagPosition == nil then anyMatch = true; self.queue[itemID].bagPosition = {bagID, slotID}; if OPENABLE_ITEM[itemID] == nil then GetBagItem(bagID, slotID); end end end end end if anyMatch then self:SetScript("OnUpdate", self.OnUpdate_Tooltip); end end end function Processor:OnUpdate_Tooltip(elapsed) self.t = self.t + elapsed; if self.t > 0.1 then self.t = 0; self:SetScript("OnUpdate", nil); local tooltipData; local lines; local leftText; local openable; local bag, slot; for itemID, v in pairs(self.queue) do if v.bagPosition then bag = v.bagPosition[1]; slot = v.bagPosition[2]; tooltipData = GetBagItem(bag, slot); if OPENABLE_ITEM[itemID] then openable = true; else openable = false; if tooltipData then lines = tooltipData.lines; leftText = lines[#lines].leftText; openable = leftText and leftText == ITEM_OPENABLE OPENABLE_ITEM[itemID] = openable; end end if openable then for callback in pairs(v) do if callback ~= "bagPosition" then callback(bag, slot) end end end end end self.queue = nil; end end function API.InquiryOpenableItem(itemID, callback) --Pre-exclude invalid item types if OPENABLE_ITEM[itemID] == false then return false end if not Processor.queue then Processor.queue = {}; end if not Processor.queue[itemID] then Processor.queue[itemID] = {}; end callback = callback or Nop; Processor.queue[itemID][callback] = true; Processor.t = 0; Processor:SetScript("OnUpdate", Processor.OnUpdate_Queue); end function API.DoesItemReallyExist(item) local a = item and GetItemInfoInstant(item); return a ~= nil end function API.IsItemContextToken(item) local _, _, _, _, _, classID, subClassID = GetItemInfoInstant(item); return classID == 5 and subClassID == 2 end end do -- Chat Message local ADDON_ICON = "|TInterface\\AddOns\\Plumber\\Art\\Logo\\PlumberLogo32:0:0|t"; local function PrintMessage(msg) if not msg then msg = ""; end print(ADDON_ICON.." |cffb8c8d1Plumber:|r "..msg); end API.PrintMessage = PrintMessage; function API.DisplayErrorMessage(msg) if not msg then return end; local messageType = 0; UIErrorsFrame:TryDisplayMessage(messageType, (ADDON_ICON.." |cffb8c8d1Plumber:|r ")..msg, RED_FONT_COLOR:GetRGB()); end function API.CheckAndDisplayErrorIfInCombat() if InCombatLockdown() then API.DisplayErrorMessage(L["Error Show UI In Combat"]); return true else return false end end end do -- Custom Hyperlink ItemRef --[[--Example local CustomLink = {}; CustomLink.typeName = "Test"; CustomLink.colorCode = "66bbff"; --LINK_FONT_COLOR function CustomLink.callback(arg1, arg2, arg3) print(arg1, arg2, arg3); end API.AddCustomLinkType(CustomLink.typeName, CustomLink.callback, CustomLink.colorCode); function CustomLink.GenerateLink(arg1, arg2, arg3) return API.GenerateCustomLink(CustomLink.typeName, L["Click To See Details"], arg1, arg2, arg3); end --]] local CustomLinkUtil = {}; function API.AddCustomLinkType(typeName, callback, colorCode) CustomLinkUtil[typeName] = { callback = callback, colorCode = colorCode, }; end function API.GenerateCustomLink(typeName, displayedText, ...) if CustomLinkUtil[typeName] then if not CustomLinkUtil.registered then CustomLinkUtil.registered = true; EventRegistry:RegisterCallback("SetItemRef", function(_, link, text, button, chatFrame) if link then local _typeName, subText = match(link, "plumber:([^:]+):([^|]+)"); if _typeName and CustomLinkUtil[_typeName] then local args = {}; for arg in string.gmatch(subText, "[^:]+") do tinsert(args, arg); end CustomLinkUtil[_typeName].callback(unpack(args)); end end end); end --|cffxxxxxx|Htype:payload|h[text]|h|r local args = {...}; local link = "|Haddon:plumber:"..typeName; for i, v in ipairs(args) do link = link..":"..v; end link = format("|cff%s%s|h[%s]|h|r", CustomLinkUtil[typeName].colorCode or "ffd100", link, displayedText); return link end end end do -- 11.0 Menu Formatter function API.ShowBlizzardMenu(ownerRegion, schematic, contextData) contextData = contextData or {}; local menu = MenuUtil.CreateContextMenu(ownerRegion, function(ownerRegion, rootDescription) rootDescription:SetTag(schematic.tag, contextData); for _, info in ipairs(schematic.objects) do local elementDescription; if info.type == "Title" then elementDescription = rootDescription:CreateTitle(); elementDescription:AddInitializer(function(f, description, menu) f.fontString:SetText(info.name); end); elseif info.type == "Divider" then elementDescription = rootDescription:CreateDivider(); elseif info.type == "Spacer" then elementDescription = rootDescription:CreateSpacer(); elseif info.type == "Button" then elementDescription = rootDescription:CreateButton(info.name, info.OnClick); elseif info.type == "Checkbox" then elementDescription = rootDescription:CreateCheckbox(info.name, info.IsSelected, info.ToggleSelected); end if info.IsEnabledFunc then local enabled = info.IsEnabledFunc(); elementDescription:SetEnabled(enabled); end if info.tooltip then elementDescription:SetTooltip(function(tooltip, elementDescription) --GameTooltip_AddInstructionLine(tooltip, "Test Tooltip Instruction"); --GameTooltip_AddErrorLine(tooltip, "Test Tooltip Colored Line"); if info.DynamicTooltipFunc then local text, r, g, b = info.DynamicTooltipFunc(); if text then GameTooltip_SetTitle(tooltip, MenuUtil.GetElementText(elementDescription)); tooltip:AddLine(text, r, g, b, true); end else GameTooltip_SetTitle(tooltip, MenuUtil.GetElementText(elementDescription)); GameTooltip_AddNormalLine(tooltip, info.tooltip); end end); end if info.rightText or info.rightTexture then local rightText; if type(info.rightText) == "function" then rightText = info.rightText(); else rightText = info.rightText; end elementDescription:AddInitializer(function(button, description, menu) local rightWidth = 0; if info.rightTexture then local iconSize = 18; local rightTexture = button:AttachTexture(); rightTexture:SetSize(iconSize, iconSize); rightTexture:SetPoint("RIGHT"); rightTexture:SetTexture(info.rightTexture); rightWidth = rightWidth + iconSize; rightWidth = 20; end local fontString = button.fontString; fontString:SetTextColor(NORMAL_FONT_COLOR:GetRGB()); local fontString2; if info.rightText then fontString2 = button:AttachFontString(); fontString2:SetHeight(20); fontString2:SetPoint("RIGHT", button, "RIGHT", 0, 0); fontString2:SetJustifyH("RIGHT"); fontString2:SetText(rightText); fontString2:SetTextColor(0.5, 0.5, 0.5); rightWidth = fontString2:GetWrappedWidth() + 20; end local width = fontString:GetWrappedWidth() + rightWidth; local height = 20; return width, height; end); end end end); if schematic.onMenuClosedCallback then menu:SetClosedCallback(schematic.onMenuClosedCallback); end return menu end end do -- Slash Commands local SlashCmdUtil = {}; SlashCmdUtil.functions = {}; SlashCmdUtil.alias = "plmr"; SlashCmdUtil.cmdID = { DrawerMacro = 1, }; function SlashCmdUtil.Process(input) if input and type(input) == "string" then input = " "..input; local token; local args = {}; for arg in string.gmatch(input, "%s+([%S]+)") do if not token then token = arg; else tinsert(args, arg); end end if token and SlashCmdUtil.functions[token] then SlashCmdUtil.functions[token](unpack(args)); end end end function SlashCmdUtil.CreateSlashCommand(func, alias1, alias2) local name = "PLUMBERCMD"; if alias1 then _G["SLASH_"..name.."1"] = "/"..alias1; end if alias2 then _G["SLASH_"..name.."2"] = "/"..alias2; end SlashCmdList[name] = func; end function API.AddSlashSubcommand(name, func) if not SlashCmdUtil.cmdID[name] then return end; if not SlashCmdUtil.cmdAdded then SlashCmdUtil.CreateSlashCommand(SlashCmdUtil.Process, SlashCmdUtil.alias); end local token = tostring(SlashCmdUtil.cmdID[name]); SlashCmdUtil.functions[token] = func; end function API.GetSlashSubcommand(name) if SlashCmdUtil.cmdID[name] then return string.format("/%s %s", SlashCmdUtil.alias, SlashCmdUtil.cmdID[name]); end end end do -- Macro Util local WoWAPI = { IsSpellKnown = C_SpellBook.IsSpellKnown or IsSpellKnownOrOverridesKnown or IsSpellKnown, IsPlayerSpell = IsPlayerSpell, PlayerHasToy = PlayerHasToy or Nop, GetItemCount = C_Item.GetItemCount, GetItemCraftedQualityByItemInfo = C_TradeSkillUI and C_TradeSkillUI.GetItemCraftedQualityByItemInfo or Nop, GetItemReagentQualityByItemInfo = C_TradeSkillUI and C_TradeSkillUI.GetItemReagentQualityByItemInfo or Nop, --IsConsumableItem = C_Item.IsConsumableItem or Nop, --This is not what we thought it is GetItemInfoInstant = C_Item.GetItemInfoInstant, FindPetIDByName = C_PetJournal and C_PetJournal.FindPetIDByName or Nop, GetPetInfoBySpeciesID = C_PetJournal and C_PetJournal.GetPetInfoBySpeciesID or Nop, }; function API.CanPlayerPerformAction(actionType, arg1, arg2) if actionType == "spell" then return WoWAPI.IsSpellKnown(arg1) or WoWAPI.IsPlayerSpell(arg1) elseif actionType == "item" then if API.IsToyItem(arg1) then return WoWAPI.PlayerHasToy(arg1) else local _, _, _, _, _, classID, subClassID = WoWAPI.GetItemInfoInstant(arg1); --always return true for conumable items in case player needs to restock --if classID == 0 then -- return true --end local isConsumable = classID == 0; local count = WoWAPI.GetItemCount(arg1, true, true, true, true); return count > 0, isConsumable end end return true --always return true for unrecognized action end function API.GetItemCraftingQuality(item) local quality = WoWAPI.GetItemCraftedQualityByItemInfo(item); if not quality then quality = WoWAPI.GetItemReagentQualityByItemInfo(item); end return quality end function API.GetPetNameAndUsability(speciesID, checkUsability) local name = WoWAPI.GetPetInfoBySpeciesID(speciesID); if checkUsability then local _, petGUID = WoWAPI.FindPetIDByName(name); return name, petGUID ~= nil else return name end end end do --Professions --/dump ProfessionsBook_GetSpellBookItemSlot(GetMouseFoci()[1]) --Used on ProfessionsBookFrame SpellButton local GetProfessions = GetProfessions; local GetProfessionInfo = GetProfessionInfo; local GetSpellBookItemType = (C_SpellBook and C_SpellBook.GetSpellBookItemType) or GetSpellBookItemInfo; function API.GetProfessionSpellInfo(professionOrderIndex) local prof1, prof2, archaeology, fishing, cooking = GetProfessions(); local index; if professionOrderIndex == 2 then index = prof2; else index = prof1; end if not index then return end; local name, texture, rank, maxRank, numSpells, spellOffset, skillLine, rankModifier, specializationIndex, specializationOffset, skillLineName = GetProfessionInfo(index); if not spellOffset then return end; local buttonID = 1; --PrimaryProfessionSpellButtonBottom local slotIndex = spellOffset + buttonID; local activeSpellBank = 0; --Enum.SpellBookSpellBank.Player local itemType, actionID, spellID = GetSpellBookItemType(slotIndex, activeSpellBank); --Classic if not spellID then spellID = actionID; end local tbl = { spellID = spellID, texture = texture, name = name, slotIndex = slotIndex, activeSpellBank = activeSpellBank, skillLine = skillLine, }; return tbl end if C_TradeSkillUI and C_TradeSkillUI.OpenTradeSkill then --Retail function API.OpenProfessionFrame(professionOrderIndex) local info = API.GetProfessionSpellInfo(professionOrderIndex); if info then local currBaseProfessionInfo = C_TradeSkillUI.GetBaseProfessionInfo(); if (not currBaseProfessionInfo) or (currBaseProfessionInfo.professionID ~= info.skillLine) then C_TradeSkillUI.OpenTradeSkill(info.skillLine); --C_SpellBook.CastSpellBookItem(info.slotIndex, info.activeSpellBank); else C_TradeSkillUI.CloseTradeSkill(); end end end else --Classic function API.OpenProfessionFrame(professionOrderIndex) local info = API.GetProfessionSpellInfo(professionOrderIndex); if info then CastSpell(info.slotIndex, "professions"); end end end PlumberGlobals.OpenProfessionFrame = API.OpenProfessionFrame; end do --Addon Skin local AddOnSkinHandler = { ElvUI = { global = "ElvUI", root = function() local E = ElvUI[1]; return E:GetModule("Skins") end, handlerKey = { editbox = "HandleEditBox"; }; }, }; function API.SetupSkinExternal(object) local objectType = object:GetObjectType(); objectType = string.lower(objectType); for addOnName, v in pairs(AddOnSkinHandler) do if _G[v.global] then local root = v.root(); if v.handlerKey[objectType] then root[v.handlerKey[objectType]](root, object); return true, addOnName end end end end end do --FrameUtil function API.RegisterFrameForEvents(frame, events) for i, event in ipairs(events) do frame:RegisterEvent(event); end end function API.UnregisterFrameForEvents(frame, events) for i, event in ipairs(events) do frame:UnregisterEvent(event); end end end do --Locale-dependent API local locale = GetLocale(); if locale == "ruRU" then function API.GetItemCountFromText(text) --"r%s*[xх](%d+)" doesn't work local count = match(text, "r%s*x(%d+)"); if not count then count = match(text, "r%s*х(%d+)"); end if count then return tonumber(count) else return 1 end end elseif locale == "zhCN" or locale == "zhTW" then function API.GetItemCountFromText(text) local count = match(text, "r%s*x(%d+)"); if count then return tonumber(count) else return 1 end end else function API.GetItemCountFromText(text) local count = match(text, "|r%s*x(%d+)"); if count then return tonumber(count) else return 1 end end end end --[[ local DEBUG = CreateFrame("Frame"); DEBUG:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", "player"); DEBUG:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", "player"); DEBUG:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player"); DEBUG:SetScript("OnEvent", function(self, event, ...) print(event); if event == "UNIT_SPELLCAST_SUCCEEDED" then local name, text, texture, startTime, endTime, isTradeSkill = UnitChannelInfo("player"); self.endTime = endTime; elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" then local t = GetTime(); t = t * 1000; if self.endTime then local diff = t - self.endTime; if diff < 200 and diff > -200 then print("Natural Complete") else print("Interrupted") end end end end); --]]