local _, addon = ... local API = addon.API; local IS_TWW = addon.IsGame_11_0_0; 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 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; 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 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; 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; 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[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 = 172800; 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) --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 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 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", math.floor(seconds / 60), math.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; 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; end do -- Tooltip Parser local GetInfoByHyperlink = 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); 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 = floor(x*10000 + 0.5)/10000; y = floor(y*10000 + 0.5)/10000; PlumberDevData.POIPositions[poiID] = { id = poiID, mapID = uiMapID, continent = continentMapID, cx = x, cy = y, }; end C_Map.ClearUserWaypoint(); else print("No user waypoint found.") end end); end API.ConvertMapPositionToContinentPosition = ConvertMapPositionToContinentPosition; --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) if not Converter then Converter = CreateFrame("Frame"); 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 = x, y = y, poiID = poiID, }; onCoordReceivedFunc(positionData) C_Map.ClearUserWaypoint(); 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; 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 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; 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:IsShown(); end API.IsInEditMode = IsInEditMode; end do --Reputation local GetFriendshipReputation = C_GossipInfo.GetFriendshipReputation; local GetFriendshipReputationRanks = C_GossipInfo.GetFriendshipReputationRanks; local function GetFriendshipProgress(factionID) local repInfo = factionID and GetFriendshipReputation(factionID); if repInfo and repInfo.friendshipFactionID and repInfo.friendshipFactionID > 0 then local currentValue, maxValue; 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); local level = rankInfo.currentLevel; local isFull = level >= rankInfo.maxLevel; return level, isFull, currentValue, maxValue end end API.GetFriendshipProgress = GetFriendshipProgress; end do --Spell if IS_TWW then 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; else API.GetSpellInfo = GetSpellInfo; end end do --System if IS_TWW then local GetMouseFoci = GetMouseFoci; local function GetMouseFocus() local objects = GetMouseFoci(); return objects and objects[1] end API.GetMouseFocus = GetMouseFocus; else API.GetMouseFocus = GetMouseFocus; 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; 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 --[[ 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); --]]